Skip to content

Commit

Permalink
Merge pull request #36 from MOONSHOT-Team/feature/#26
Browse files Browse the repository at this point in the history
[Feat] #26 - 소셜로그인 API 구현
  • Loading branch information
0lynny authored Jan 7, 2024
2 parents 14050e7 + 5e28b27 commit 8e27112
Show file tree
Hide file tree
Showing 53 changed files with 907 additions and 18 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ dependencies {

//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

// JWT
implementation group: "io.jsonwebtoken", name: "jjwt-api", version: "0.11.2"
implementation group: "io.jsonwebtoken", name: "jjwt-impl", version: "0.11.2"
implementation group: "io.jsonwebtoken", name: "jjwt-jackson", version: "0.11.2"

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.moonshot.server.domain.user.controller;

import lombok.RequiredArgsConstructor;
import org.moonshot.server.domain.user.dto.request.SocialLoginRequest;
import org.moonshot.server.domain.user.dto.response.SocialLoginResponse;
import org.moonshot.server.domain.user.service.UserService;
import org.moonshot.server.global.auth.jwt.JwtTokenProvider;
import org.moonshot.server.global.auth.jwt.TokenResponse;
import org.moonshot.server.global.common.response.ApiResponse;
import org.moonshot.server.global.common.response.SuccessType;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.security.Principal;

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/user")
public class UserController {

private final UserService userService;

@PostMapping("/login")
public ApiResponse<SocialLoginResponse> login(@RequestHeader("Authorization") String authorization,
@RequestBody SocialLoginRequest socialLoginRequest) throws IOException {
return ApiResponse.success(SuccessType.POST_LOGIN_SUCCESS, userService.login(SocialLoginRequest.of(socialLoginRequest.socialPlatform(), authorization)));
}

@PostMapping("/reissue")
public ApiResponse<TokenResponse> reissue(@RequestHeader("Authorization") String refreshToken) {
return ApiResponse.success(SuccessType.POST_REISSUE_SUCCESS, userService.reissue(refreshToken));
}

@PostMapping("/log-out")
public ApiResponse<?> logout(Principal principal) {
userService.logout(JwtTokenProvider.getUserIdFromPrincipal(principal));
return ApiResponse.success(SuccessType.POST_LOGOUT_SUCCESS);
}

@DeleteMapping("/withdrawal")
public ApiResponse<?> withdrawal(Principal principal) {
userService.withdrawal(JwtTokenProvider.getUserIdFromPrincipal(principal));
return ApiResponse.success(SuccessType.DELETE_USER_SUCCESS);
}

// @GetMapping("/login/oauth2/code/kakao")
// public String kakaoSuccess(@RequestParam String code) {
// return code;
// }
//
// @GetMapping("/login/oauth2/code/google")
// public String googleSuccess(@RequestParam String code, @RequestParam String scope, @RequestParam String prompt) { return code; }

}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.moonshot.server.domain.user.dto.request;

import org.moonshot.server.domain.user.model.SocialPlatform;

public record SocialLoginRequest(
SocialPlatform socialPlatform,
String code
) {
public static SocialLoginRequest of(SocialPlatform socialPlatform, String code) {
return new SocialLoginRequest(socialPlatform, code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.moonshot.server.domain.user.dto.response;

import org.moonshot.server.global.auth.jwt.TokenResponse;

public record SocialLoginResponse(
Long userId,
String userName,
TokenResponse token
) {
public static SocialLoginResponse of(Long userId, String userName, TokenResponse token) {
return new SocialLoginResponse(userId, userName, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.moonshot.server.domain.user.dto.response.google;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record GoogleInfoResponse(
String sub,
String name,
String givenName,
String familyName,
String picture,
String email,
Boolean emailVerified,
String locale
) {
public static GoogleInfoResponse of(String sub, String name, String givenName, String familyName, String picture, String email, Boolean emailVerified, String locale) {
return new GoogleInfoResponse(sub, name, givenName, familyName, picture, email, emailVerified, locale);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.moonshot.server.domain.user.dto.response.google;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record GoogleTokenResponse(
String accessToken,
String refreshToken
) {
public static GoogleTokenResponse of(String accessToken, String refreshToken) {
return new GoogleTokenResponse(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.moonshot.server.domain.user.dto.response.kakao;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoAccount(
KakaoUserProfile profile
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.moonshot.server.domain.user.dto.response.kakao;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoTokenResponse(
String accessToken,
String refreshToken
) {
public static KakaoTokenResponse of(String accessToken, String refreshToken) {
return new KakaoTokenResponse(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.moonshot.server.domain.user.dto.response.kakao;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoUserProfile(
String nickname,
String profileImageUrl
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.moonshot.server.domain.user.dto.response.kakao;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoUserResponse(
String id,
KakaoAccount kakaoAccount
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
public enum SocialPlatform {

KAKAO("kakao"),
GOOGLE("google");
GOOGLE("google"),
WITHDRAWAL("withdrawal");

private final String value;

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/moonshot/server/domain/user/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,19 @@ public class User {

private String description;

@Builder(builderMethodName = "builderWithSignIn")
public static User of(String socialId, SocialPlatform socialPlatform, String name, String profileImage, String email) {
return User.builder()
.socialId(socialId)
.socialPlatform(socialPlatform)
.name(name)
.profileImage(profileImage)
.email(email)
.build();
}

public void modifySocialPlatform(SocialPlatform socialPlatform) {
this.socialPlatform = socialPlatform;
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.moonshot.server.domain.user.repository;

import java.util.Optional;
import org.moonshot.server.domain.user.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findUserBySocialId(String socialId);
Optional<User> findUserByNickname(String nickname);

}

153 changes: 153 additions & 0 deletions src/main/java/org/moonshot/server/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package org.moonshot.server.domain.user.service;

import lombok.RequiredArgsConstructor;
import org.moonshot.server.domain.user.dto.request.SocialLoginRequest;
import org.moonshot.server.domain.user.dto.response.SocialLoginResponse;
import org.moonshot.server.domain.user.dto.response.google.GoogleInfoResponse;
import org.moonshot.server.domain.user.dto.response.google.GoogleTokenResponse;
import org.moonshot.server.domain.user.dto.response.kakao.KakaoTokenResponse;
import org.moonshot.server.domain.user.dto.response.kakao.KakaoUserResponse;
import org.moonshot.server.domain.user.exception.UserNotFoundException;
import org.moonshot.server.domain.user.model.SocialPlatform;
import org.moonshot.server.domain.user.model.User;
import org.moonshot.server.domain.user.repository.UserRepository;
import org.moonshot.server.global.auth.feign.google.GoogleApiClient;
import org.moonshot.server.global.auth.feign.google.GoogleAuthApiClient;
import org.moonshot.server.global.auth.feign.kakao.KakaoApiClient;
import org.moonshot.server.global.auth.feign.kakao.KakaoAuthApiClient;
import org.moonshot.server.global.auth.jwt.JwtTokenProvider;
import org.moonshot.server.global.auth.jwt.TokenResponse;
import org.moonshot.server.global.auth.security.UserAuthentication;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

@Value("${google.client-id}")
private String googleClientId;

@Value("${google.client-secret}")
private String googleClientSecret;

@Value("${google.redirect-url}")
private String googleRedirectUrl;

@Value("${kakao.client-id}")
private String kakaoClientId;

@Value("${kakao.redirect-url}")
private String kakaoRedirectUrl;

private final UserRepository userRepository;
private final GoogleAuthApiClient googleAuthApiClient;
private final GoogleApiClient googleApiClient;
private final KakaoAuthApiClient kakaoAuthApiClient;
private final KakaoApiClient kakaoApiClient;
private final JwtTokenProvider jwtTokenProvider;

@Transactional
public SocialLoginResponse login(SocialLoginRequest request) throws IOException {
switch (request.socialPlatform().getValue()){
case "google":
return gooleLogin(request);
case "kakao":
return kakaoLogin(request);
}
return null;
}

@Transactional
public SocialLoginResponse gooleLogin(SocialLoginRequest request) throws IOException {
GoogleTokenResponse tokenResponse = googleAuthApiClient.googleAuth(
request.code(),
googleClientId,
googleClientSecret,
googleRedirectUrl,
"authorization_code"
);
GoogleInfoResponse userResponse = googleApiClient.googleInfo("Bearer " + tokenResponse.accessToken());
Optional<User> findUser = userRepository.findUserBySocialId(userResponse.sub());
User user;
if (findUser.isEmpty()) {
User newUser = userRepository.save(User.builderWithSignIn()
.socialId(userResponse.sub())
.socialPlatform(request.socialPlatform())
.name(userResponse.name())
.profileImage(userResponse.picture())
.email(userResponse.email())
.build());

user = newUser;
} else {
user = findUser.get();
if (user.getSocialPlatform().equals(SocialPlatform.WITHDRAWAL)) {
user.modifySocialPlatform(SocialPlatform.GOOGLE);
}
}
UserAuthentication userAuthentication = new UserAuthentication(user.getId(), null, null);
TokenResponse token = new TokenResponse(jwtTokenProvider.generateAccessToken(userAuthentication), jwtTokenProvider.generateRefreshToken(userAuthentication));
return SocialLoginResponse.of(user.getId(), user.getName(), token);
}

@Transactional
public SocialLoginResponse kakaoLogin(SocialLoginRequest request) throws IOException {
KakaoTokenResponse tokenResponse = kakaoAuthApiClient.getOAuth2AccessToken(
"authorization_code",
kakaoClientId,
kakaoRedirectUrl,
request.code()
);
KakaoUserResponse userResponse = kakaoApiClient.getUserInformation(
"Bearer " + tokenResponse.accessToken());
Optional<User> findUser = userRepository.findUserBySocialId(userResponse.id());
User user;
if (findUser.isEmpty()) {
User newUser = userRepository.save(User.builderWithSignIn()
.socialId(userResponse.id())
.socialPlatform(request.socialPlatform())
.name(userResponse.kakaoAccount().profile().nickname())
.profileImage(userResponse.kakaoAccount().profile().profileImageUrl())
.email("")
.build());

user = newUser;
} else {
user = findUser.get();
if (user.getSocialPlatform().equals(SocialPlatform.WITHDRAWAL)) {
user.modifySocialPlatform(SocialPlatform.KAKAO);
}
}
UserAuthentication userAuthentication = new UserAuthentication(user.getId(), null, null);
TokenResponse token = new TokenResponse(jwtTokenProvider.generateAccessToken(userAuthentication), jwtTokenProvider.generateRefreshToken(userAuthentication));
return SocialLoginResponse.of(user.getId(), user.getName(), token);
}

@Transactional
public TokenResponse reissue(String refreshToken) {
String token = refreshToken.substring("Bearer ".length());
Long userId = jwtTokenProvider.validateRefreshToken(token);
jwtTokenProvider.deleteRefreshToken(userId);
UserAuthentication userAuthentication = new UserAuthentication(userId, null, null);
return jwtTokenProvider.reissuedToken(userAuthentication);
}

@Transactional
public void logout(Long userId) {
jwtTokenProvider.deleteRefreshToken(userId);
}

@Transactional
public void withdrawal(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
user.modifySocialPlatform(SocialPlatform.WITHDRAWAL);
}

}
Empty file.
Loading

0 comments on commit 8e27112

Please sign in to comment.