Skip to content

Commit

Permalink
Merge pull request #15 from TeamUStory/feat/google
Browse files Browse the repository at this point in the history
Feat: OAuth 구글 소셜로그인
  • Loading branch information
GyungA authored Jul 15, 2024
2 parents b78d948 + 303fb79 commit 6fb4fa8
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.elice.ustory.global.exception.dto.ErrorResponse;
import com.elice.ustory.global.jwt.JwtAuthorization;
import com.elice.ustory.global.jwt.JwtUtil;
import com.elice.ustory.global.oauth.google.GoogleService;
import com.elice.ustory.global.oauth.kakao.KakaoService;
import com.elice.ustory.global.oauth.naver.NaverService;
import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -37,10 +38,12 @@ public class UserController {
private final EmailService emailService;
private final KakaoService kakaoService;
private final NaverService naverService;
private final GoogleService googleService;
private final JwtUtil jwtUtil;

private static final String KAKAO_LOGIN_TYPE = "KAKAO";
private static final String NAVER_LOGIN_TYPE = "NAVER";
private static final String GOOGLE_LOGIN_TYPE = "GOOGLE";

@Operation(summary = "Create User API", description = "기본 회원가입 후 유저를 생성한다." +
"<br>비밀번호는 **숫자, 영문, 특수문자 각 1개를 포함한 8~16자** 이며," +
Expand Down Expand Up @@ -110,10 +113,12 @@ public ResponseEntity<LogoutResponse> logoutBasic(HttpServletRequest request) {
String accessToken = jwtUtil.getTokenFromRequest(request);
String loginType = jwtUtil.getLoginType(accessToken);

if(loginType.equals(KAKAO_LOGIN_TYPE)){
if (loginType.equals(KAKAO_LOGIN_TYPE)) {
kakaoService.kakaoLogout(accessToken);
}else if(loginType.equals(NAVER_LOGIN_TYPE)){
} else if (loginType.equals(NAVER_LOGIN_TYPE)) {
naverService.naverLogout(accessToken);
} else if (loginType.equals(GOOGLE_LOGIN_TYPE)) {
googleService.googleLogout(accessToken);
}

LogoutResponse logoutResponse = userService.logout(accessToken, loginType);
Expand Down Expand Up @@ -160,7 +165,7 @@ public ResponseEntity<ValidateNicknameResponse> validateNickname(@Valid @Request

@Operation(summary = "Send Mail To Validate Email For Sign-Up API",
description = "회원가입 시 이메일 검증을 위한 인증코드를 해당 메일로 발송한다. 이미 가입된 이메일인 경우 예외 발생." +
"<br>detailMessage는 둘 중 하나: '사용중인\\_이메일' 또는 '탈퇴된\\_이메일'")
"<br>detailMessage는 둘 중 하나: '사용중인\\_이메일' 또는 '탈퇴된\\_이메일'")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AuthCodeCreateResponse.class))),
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/elice/ustory/global/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.elice.ustory.domain.user.service.UserService;
import com.elice.ustory.global.exception.model.InvalidTokenException;
import com.elice.ustory.global.exception.model.RefreshTokenExpiredException;
import com.elice.ustory.global.redis.google.GoogleToken;
import com.elice.ustory.global.redis.google.GoogleTokenService;
import com.elice.ustory.global.redis.kakao.KakaoToken;
import com.elice.ustory.global.redis.kakao.KakaoTokenService;
import com.elice.ustory.global.redis.naver.NaverToken;
Expand All @@ -29,9 +31,11 @@ public class JwtUtil {
private final RefreshTokenService refreshTokenService;
private final KakaoTokenService kakaoTokenService;
private final NaverTokenService naverTokenService;
private final GoogleTokenService googleTokenService;

private static final String KAKAO_LOGIN_TYPE = "KAKAO";
private static final String NAVER_LOGIN_TYPE = "NAVER";
private static final String GOOGLE_LOGIN_TYPE = "GOOGLE";
private static final String INVALID_TOKEN_MESSAGE = "토큰이 없거나 형식에 맞지 않습니다.";
private static final String REFRESH_TOKEN_EXPIRED_MESSAGE = "RefreshToken이 만료되었습니다, 재로그인해주세요.";

Expand Down Expand Up @@ -61,6 +65,11 @@ public String refreshAuthentication(HttpServletRequest request) {
.orElseThrow(() -> new InvalidTokenException(INVALID_TOKEN_MESSAGE));

naverTokenService.saveNaverTokenInfo(loginUser.getId(), naverToken.getNaverToken(), newAccessToken);
} else if (loginUser.getLoginType().toString().equals(GOOGLE_LOGIN_TYPE)) {
GoogleToken googleToken = googleTokenService.getByAccessToken(accessToken)
.orElseThrow(() -> new InvalidTokenException(INVALID_TOKEN_MESSAGE));

googleTokenService.saveGoogleTokenInfo(loginUser.getId(), googleToken.getGoogleToken(), newAccessToken);
}
log.info("[refreshToken] AccessToken이 재발급 되었습니다: {}", newAccessToken);
log.info("[refreshToken] RefreshToken이 재발급 되었습니다: {}", newRefreshToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.elice.ustory.global.oauth.google;

import com.elice.ustory.domain.user.dto.LoginResponse;
import com.elice.ustory.domain.user.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;

@Controller
@Slf4j
@RequiredArgsConstructor
public class GoogleController {
private final GoogleOauth googleOauth;
private final GoogleService googleService;
private final UserService userService;

@RequestMapping(value = "/login/oauth2/code/google", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<LoginResponse> googleLogin(@RequestParam(name = "code") String code,
HttpServletResponse response) {
String accessToken = googleOauth.requestGoogleAccessToken(code);
HashMap<String, String> accountProfile = googleOauth.requestGoogleAccountProfile(accessToken);

String email = accountProfile.get("email");
String name = accountProfile.get("name");

if(!userService.checkExistByEmail(email)) {
googleService.googleSignUp(accountProfile);
}
//TODO: 이미 구글 이메일로 기본 회원가입을 했는데, 소셜로그인을 시도할 경우? -> "이미 가입된 이메일입니다. 다른 로그인 방식을 시도해보세요."

LoginResponse loginResponse = googleService.googleLogin(email, response, accessToken);

log.info("[googleLogin] 구글 닉네임: {}", name);
return ResponseEntity.ok().body(loginResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.elice.ustory.global.oauth.google;

import com.elice.ustory.global.exception.model.NotFoundException;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Optional;

@Component
public class GoogleOauth {
final String AUTHORIZATION_CODE = "authorization_code";
final String ACCESS_TOKEN = "access_token";
final String BEARER_TOKEN_PREFIX = "Bearer ";
final String NOT_FOUND_ACCESS_TOKEN = "구글 액세스 토큰을 찾을 수 없습니다.";

@Value("${google.clientId}")
private String clientId;

@Value("${google.clientSecret}")
private String clientSecret;

@Value("${google.redirectUri}")
private String redirectUri;

private String grantType = AUTHORIZATION_CODE;

@Value("${google.accessTokenUri}")
private String accessTokenUri;

@Value("${google.accountProfileUri}")
private String accountProfileUri;

public String requestGoogleAccessToken(final String code) {
RestTemplate restTemplate = new RestTemplate(); //TODO: 스프링 빈으로 관리
final String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8);
final HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("code", decodedCode);
params.add("grant_type", grantType);
params.add("redirect_uri", redirectUri);

final HttpEntity<MultiValueMap<String, String>> tokenRequestEntity = new HttpEntity<>(params, headers);
final String responseBody = restTemplate.exchange(accessTokenUri, HttpMethod.POST, tokenRequestEntity, String.class).getBody();
JsonObject responseJson = JsonParser.parseString(responseBody).getAsJsonObject();

return Optional.ofNullable(responseJson.get(ACCESS_TOKEN))
.map(JsonElement::getAsString)
.orElseThrow(() -> new NotFoundException(NOT_FOUND_ACCESS_TOKEN));
}

public HashMap<String, String> requestGoogleAccountProfile(String accessToken) {
HashMap<String, String> accountProfile = new HashMap<>();

RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();

headers.add(HttpHeaders.AUTHORIZATION, BEARER_TOKEN_PREFIX + accessToken);
final HttpEntity<MultiValueMap<String, String>> requestHeaderEntity = new HttpEntity<>(headers);
final String responseBody = restTemplate.exchange(accountProfileUri, HttpMethod.GET, requestHeaderEntity, String.class).getBody();
JsonObject responseJson = JsonParser.parseString(responseBody).getAsJsonObject();

String name = responseJson.get("name").getAsString();
String email = responseJson.get("email").getAsString();

accountProfile.put("name", name);
accountProfile.put("email", email);

return accountProfile;
}
}
105 changes: 105 additions & 0 deletions src/main/java/com/elice/ustory/global/oauth/google/GoogleService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.elice.ustory.global.oauth.google;

import com.elice.ustory.domain.diary.entity.Color;
import com.elice.ustory.domain.diary.entity.Diary;
import com.elice.ustory.domain.diary.entity.DiaryCategory;
import com.elice.ustory.domain.diary.repository.DiaryRepository;
import com.elice.ustory.domain.diaryUser.entity.DiaryUser;
import com.elice.ustory.domain.diaryUser.entity.DiaryUserId;
import com.elice.ustory.domain.diaryUser.repository.DiaryUserRepository;
import com.elice.ustory.domain.user.dto.LoginResponse;
import com.elice.ustory.domain.user.entity.Users;
import com.elice.ustory.domain.user.repository.UserRepository;
import com.elice.ustory.global.exception.model.NotFoundException;
import com.elice.ustory.global.jwt.JwtTokenProvider;
import com.elice.ustory.global.redis.google.GoogleTokenService;
import com.elice.ustory.global.redis.refresh.RefreshTokenService;
import com.elice.ustory.global.util.NicknameGenerator;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.UUID;

@Service
@Slf4j
@RequiredArgsConstructor
public class GoogleService {
private final String EMAIL_LITERAL = "email";
private final String NAME_LITERAL = "name";
private final String AUTHORIZATION_LITERAL = "Authorization";
private final String NOT_FOUND_USER_MESSAGE = "해당 유저를 찾을 수 없습니다.";

private final int REFRESH_TOKEN_TTL = 60 * 60 * 24 * 7;

private final UserRepository userRepository;
private final DiaryRepository diaryRepository;
private final DiaryUserRepository diaryUserRepository;

private final GoogleTokenService googleTokenService;
private final RefreshTokenService refreshTokenService;

private final PasswordEncoder passwordEncoder;
private final NicknameGenerator nicknameGenerator;
private final JwtTokenProvider jwtTokenProvider;

public void googleSignUp(HashMap<String, String> accountProfile) {
String email = accountProfile.get(EMAIL_LITERAL);
String name = accountProfile.get(NAME_LITERAL);

String randomPassword = String.valueOf(UUID.randomUUID()).substring(0, 8);
String encodedPassword = passwordEncoder.encode(randomPassword);
String formattedName = nicknameGenerator.formatNickname(name);

Users builtUser = Users.addUserBuilder()
.email(email)
.loginType(Users.LoginType.GOOGLE)
.name(formattedName)
.nickname(formattedName)
.password(encodedPassword)
.profileImgUrl("")
.profileDescription("자기소개")
.build();

userRepository.save(builtUser);

Diary userDiary = new Diary(
String.format("%s의 다이어리", builtUser.getNickname()),
"기본 DiaryImgUrl",
DiaryCategory.INDIVIDUAL,
String.format("%s의 개인 다이어리", builtUser.getNickname()),
Color.RED
);

diaryRepository.save(userDiary);
diaryUserRepository.save(new DiaryUser(new DiaryUserId(userDiary, builtUser)));
}

public LoginResponse googleLogin(String googleEmail, HttpServletResponse response, String googleToken) {

Users loginUser = userRepository.findByEmail(googleEmail)
.orElseThrow(() -> new NotFoundException(NOT_FOUND_USER_MESSAGE));

String accessToken = jwtTokenProvider.createAccessTokenSocial(loginUser.getId(), googleToken, loginUser.getLoginType());
String refreshToken = jwtTokenProvider.createRefreshToken();

LoginResponse loginResponse = LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();

log.info("[getLoginResult] LogInResponse 객체에 값 주입");
response.addHeader(AUTHORIZATION_LITERAL, accessToken);

refreshTokenService.saveTokenInfo(loginUser.getId(), refreshToken, accessToken, REFRESH_TOKEN_TTL);
googleTokenService.saveGoogleTokenInfo(loginUser.getId(), googleToken, accessToken);

log.info("[logIn] 정상적으로 로그인되었습니다. id : {}, token : {}", loginUser.getId(), loginResponse.getAccessToken());
return loginResponse;
}

public void googleLogout(String accessToken) { googleTokenService.removeGoogleTokenInfo(accessToken); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.elice.ustory.global.redis.google;

import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@RedisHash(value = "googleToken", timeToLive = 60 * 60 * 24 * 7)
@AllArgsConstructor
@NoArgsConstructor
public class GoogleToken {
@Id
private String id;

@Getter
private String googleToken;

@Indexed
private String accessToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.elice.ustory.global.redis.google;

import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface GoogleTokenRepository extends CrudRepository<GoogleToken, String> {
Optional<GoogleToken> findByAccessToken(String accessToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.elice.ustory.global.redis.google;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class GoogleTokenService {
private final GoogleTokenRepository googleTokenRepository;

public void saveGoogleTokenInfo(Long userId, String googleToken, String accessToken) {
googleTokenRepository.save(new GoogleToken(String.valueOf(userId), googleToken, accessToken));
}

public void removeGoogleTokenInfo(String accessToken) {
googleTokenRepository.findByAccessToken(accessToken)
.ifPresent(googleTokenRepository::delete);
}

public Optional<GoogleToken> getByAccessToken(String accessToken) {
return googleTokenRepository.findByAccessToken(accessToken);
}
}

0 comments on commit 6fb4fa8

Please sign in to comment.