Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ dependencies {

// Gson
implementation 'com.google.code.gson:gson'

// Goolge OAuth2
implementation 'com.google.api-client:google-api-client:2.8.0'

// Apache Commons Lang
// implementation 'org.apache.commons:commons-lang3:3.12.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ public class AuthController {
/**
* 소셜 로그인
* @param dto
* @param socialToken
* @param socialToken access-token: 카카오, 네이버 | id-token: 구글, 애플
* @return
*/
@PostMapping("/login")
public ResponseEntity<AuthRes> socialLogin(@RequestBody AuthReq dto,
@RequestHeader("social-access-token") String socialToken) {
@RequestHeader("social-token") String socialToken) {

String provider = dto.getProvider().toLowerCase();
OAuthService oAuthService = socialLoginServices.get(provider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import static com.ikdaman.global.exception.ErrorCode.GOOGLE_SERVER_ERROR;
import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_ACCESS_TOKEN;

@Service("google")
Expand All @@ -34,10 +35,53 @@ public class GoogleAuthService implements OAuthService {

@Override
@Transactional
public AuthRes login(AuthReq dto, String socialAccessToken) {
public AuthRes login(AuthReq dto, String socialToken) {

// 1. 소셜 idToken을 통해 Google userId 가져오기
final String checkProviderId;
try {
checkProviderId = clientGoogle.getUserDataByIdToken(socialToken);
} catch (Exception e) {
throw new BaseException(GOOGLE_SERVER_ERROR);
}

// 2. 요청으로 들어온 providerId와 비교해서 검증
if (!dto.getProviderId().equals(checkProviderId)) throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN);

// 3. 기존 회원 조회
Member member = memberRepository.findBySocialTypeAndProviderId(Member.SocialType.GOOGLE, checkProviderId)
.orElseGet(() -> {
String nickname;
do {
nickname = randomNickname.generate();
} while (!memberService.isAvailableNickname(nickname)); // 닉네임 중복되면 다시 생성

// 유저 정보 저장
Member newMember = Member.builder()
.socialType(Member.SocialType.GOOGLE)
.providerId(checkProviderId)
.nickname(nickname)
.build();
return memberRepository.save(newMember);
});

// 3. 신규 토큰 생성 및 저장
AuthToken accessToken = authTokenProvider.createUserAppToken(String.valueOf(member.getMemberId()));
AuthToken refreshToken = authTokenProvider.createRefreshToken(String.valueOf(member.getMemberId()));
redisService.setValuesWithTimeout(String.valueOf(member.getMemberId()), refreshToken.getToken(), refreshExpiry);

return AuthRes.builder()
.accessToekn(accessToken.getToken())
.refreshToken(refreshToken.getToken())
.nickname(member.getNickname())
.build();
}

@Transactional
public AuthRes loginByAccessToken(AuthReq dto, String socialToken) {

// 1. 소셜 accessToken을 통해 Google userId 가져오기
String checkProviderId = clientGoogle.getUserData(socialAccessToken);
String checkProviderId = clientGoogle.getUserDataByAccessToken(socialToken);

// 2. 요청으로 들어온 providerId와 비교해서 검증
if (!dto.getProviderId().equals(checkProviderId)) throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
import com.ikdaman.domain.auth.model.AuthRes;

public interface OAuthService {
AuthRes login(AuthReq dto, String socialAccessToken);
AuthRes login(AuthReq dto, String socialToken);
}
63 changes: 59 additions & 4 deletions src/main/java/com/ikdaman/global/auth/client/ClientGoogle.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,91 @@
package com.ikdaman.global.auth.client;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.ikdaman.global.auth.payload.OAuthUserRes;
import com.ikdaman.global.exception.BaseException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Arrays;

import static com.ikdaman.global.exception.ErrorCode.GOOGLE_SERVER_ERROR;
import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_ACCESS_TOKEN;

@Component
@RequiredArgsConstructor
public class ClientGoogle {

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

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

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

private final WebClient webClient;

// TODO: HttpHeaders.AUTHORIZATION 사용하도록 변경
public String getUserData(String accessToken) {
/**
* accessToken을 사용하여 Google의 userinfo API에서 사용자 정보 조회
*
* @param accessToken Google accessToken
* @return 사용자 Google 계정의 providerId
*/
public String getUserDataByAccessToken(String accessToken) {

// WebClient를 사용해 Google API 호출
OAuthUserRes OAuthUserRes = webClient.get()
.uri("https://www.googleapis.com/oauth2/v2/userinfo") // Google의 유저 정보 받아오는 url
.uri("https://www.googleapis.com/oauth2/v2/userinfo") // Google 사용자 정보 요청 URL
.header("Authorization", "Bearer " + accessToken)
// .headers(h -> h.setBearerAuth("U-" + accessToken)) // 임시로 발급받은 사용자 토큰으로 접근
.retrieve()
// onStatus <- error handling
// 4xx 에러 처리
.onStatus(status -> status.is4xxClientError(), response
-> Mono.error(new BaseException(INVALID_SOCIAL_ACCESS_TOKEN)))
// 5xx 에러 처리
.onStatus(status -> status.is5xxServerError(), response
-> Mono.error(new BaseException(GOOGLE_SERVER_ERROR)))
.bodyToMono(OAuthUserRes.class) // 유저 정보를 넣을 DTO 클래스
.block();

return String.valueOf(OAuthUserRes.getId());
}

/**
* idToken을 검증 및 파싱하여 Google의 사용자 정보 추출
*
* @param idToken Google idToken
* @return 사용자 Google 계정의 providerId
*/
public String getUserDataByIdToken(String idToken) throws Exception {

// GoogleIdTokenVerifier를 생성: Google의 공개키로 idToken의 서명을 검증
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
GsonFactory.getDefaultInstance()
)
// idToken의 Audience 설정: 웹, Android, iOS client ID를 모두 허용
.setAudience(Arrays.asList(
webClientId,
androidClientId,
iosClientId
))
.build();

GoogleIdToken googleIdToken = verifier.verify(idToken);
if (googleIdToken == null) {
throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN);
}

// Payload에서 Google의 "sub" (providerId) 추출
GoogleIdToken.Payload payload = googleIdToken.getPayload();
return payload.getSubject();
}
}
19 changes: 12 additions & 7 deletions src/main/java/com/ikdaman/global/auth/token/AuthToken.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ikdaman.global.auth.token;

import com.ikdaman.global.auth.enumerate.RoleType;
import com.ikdaman.global.exception.BaseException;
import io.jsonwebtoken.*;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -9,6 +10,12 @@
import java.security.Key;
import java.util.Date;

import static com.ikdaman.global.exception.ErrorCode.EXPIRED_ACCESS_TOKEN;
import static com.ikdaman.global.exception.ErrorCode.INVALID_ACCESS_TOKEN;
import static com.ikdaman.global.exception.ErrorCode.INVALID_ACCESS_TOKEN_FORMAT;
import static com.ikdaman.global.exception.ErrorCode.INVALID_ACCESS_TOKEN_SIGNATURE;
import static com.ikdaman.global.exception.ErrorCode.UNSUPPORTED_ACCESS_TOKEN;

@Slf4j
@RequiredArgsConstructor
public class AuthToken {
Expand Down Expand Up @@ -48,20 +55,18 @@ public Claims getTokenClaims() {
.parseClaimsJws(token)
.getBody(); // token의 Body가 다음의 exception들로 인해 유효하지 않으면 각각의 로그를 콘솔에 출력

// TODO: Error 코드 세분화 필요
} catch (SecurityException e) {
log.info("Invalid JWT signature.");
throw new BaseException(INVALID_ACCESS_TOKEN_SIGNATURE);
} catch (MalformedJwtException e) {
// 처음 로그인(/auth/kakao) 할 때, AccessToken(여기선 appToken) 없이 접근해도 token validate 체크
// -> exception 터트리지 않고 catch로 잡아줌
log.info("Invalid JWT token.");
throw new BaseException(INVALID_ACCESS_TOKEN_FORMAT);
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
throw new BaseException(EXPIRED_ACCESS_TOKEN);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
throw new BaseException(UNSUPPORTED_ACCESS_TOKEN);
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
throw new BaseException(INVALID_ACCESS_TOKEN);
}
return null;
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/ikdaman/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ public enum ErrorCode {
INVALID_ACCESS_TOKEN(HttpStatus.NOT_FOUND.value(), 4040102,"Access Token이 유효하지 않습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.NOT_FOUND.value(), 4040103,"Refresh Token이 유효하지 않습니다."),
INVALID_SOCIAL_PROVIDER(HttpStatus.NOT_FOUND.value(), 4040104,"유효하지 않은 소셜 로그인 제공업체입니다."),
NOT_MATCH_TOKEN_PROVIDER(HttpStatus.NOT_FOUND.value(), 4040101,"Social Access Token과 Provider Id가 매칭되지 않습니다."),
NOT_MATCH_TOKEN_PROVIDER(HttpStatus.NOT_FOUND.value(), 4040105,"Social Access Token과 Provider Id가 매칭되지 않습니다."),
INVALID_ACCESS_TOKEN_SIGNATURE(HttpStatus.UNAUTHORIZED.value(), 4010106, "Access Token의 서명이 유효하지 않습니다."),
INVALID_ACCESS_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED.value(), 4010107, "Access Token의 형식이 유효하지 않습니다."),
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED.value(), 4010108, "Access Token이 만료되었습니다."),
UNSUPPORTED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED.value(), 4010109, "Access Token이 지원되지 않습니다."),

// Member(02)
NOT_FOUND_USER(HttpStatus.NOT_FOUND.value(), 4040201, "존재하지 않는 유저입니다."),
Expand Down