diff --git a/src/main/java/com/ikdaman/domain/auth/controller/AuthController.java b/src/main/java/com/ikdaman/domain/auth/controller/AuthController.java index 9dfa36c..1e44eb9 100644 --- a/src/main/java/com/ikdaman/domain/auth/controller/AuthController.java +++ b/src/main/java/com/ikdaman/domain/auth/controller/AuthController.java @@ -3,9 +3,8 @@ import com.ikdaman.domain.auth.model.AuthReq; import com.ikdaman.domain.auth.model.AuthRes; import com.ikdaman.domain.auth.service.AuthService; -import com.ikdaman.domain.auth.service.OAuthService; +import com.ikdaman.domain.auth.service.SocialAuthService; import com.ikdaman.global.auth.model.Tokens; -import com.ikdaman.global.exception.BaseException; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; @@ -13,17 +12,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.Map; import java.util.UUID; -import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_PROVIDER; - @RestController @RequestMapping("/auth") @RequiredArgsConstructor public class AuthController { - private final Map socialLoginServices; + private final SocialAuthService socialAuthService; private final AuthService authService; /** @@ -36,13 +32,7 @@ public class AuthController { public ResponseEntity socialLogin(@RequestBody AuthReq dto, @RequestHeader("social-token") String socialToken) { - String provider = dto.getProvider().toLowerCase(); - OAuthService oAuthService = socialLoginServices.get(provider); - if (oAuthService == null) { - throw new BaseException(INVALID_SOCIAL_PROVIDER); - } - - AuthRes res = oAuthService.login(dto, socialToken); + AuthRes res = socialAuthService.login(dto, socialToken); HttpHeaders headers = new HttpHeaders(); headers.add("Authorization", res.getAccessToekn()); diff --git a/src/main/java/com/ikdaman/domain/auth/service/GoogleAuthService.java b/src/main/java/com/ikdaman/domain/auth/service/GoogleAuthService.java deleted file mode 100644 index 7d6d3fd..0000000 --- a/src/main/java/com/ikdaman/domain/auth/service/GoogleAuthService.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.ikdaman.domain.auth.service; - -import com.ikdaman.domain.auth.model.AuthReq; -import com.ikdaman.domain.auth.model.AuthRes; -import com.ikdaman.domain.member.entity.Member; -import com.ikdaman.domain.member.repository.MemberRepository; -import com.ikdaman.domain.member.service.MemberService; -import com.ikdaman.global.auth.client.ClientGoogle; -import com.ikdaman.global.auth.token.AuthToken; -import com.ikdaman.global.auth.token.AuthTokenProvider; -import com.ikdaman.global.exception.BaseException; -import com.ikdaman.global.util.RandomNickname; -import com.ikdaman.global.util.RedisService; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -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") -@RequiredArgsConstructor -public class GoogleAuthService implements OAuthService { - - private final ClientGoogle clientGoogle; - private final AuthTokenProvider authTokenProvider; - private final MemberService memberService; - private final MemberRepository memberRepository; - private final RedisService redisService; - private final RandomNickname randomNickname; - - @Value("${auth.refresh-token-validity}") - private long refreshExpiry; // RefreshToken 만료일 - - @Override - @Transactional - 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.getUserDataByAccessToken(socialToken); - - // 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(); - } -} diff --git a/src/main/java/com/ikdaman/domain/auth/service/KakaoAuthService.java b/src/main/java/com/ikdaman/domain/auth/service/KakaoAuthService.java deleted file mode 100644 index 9a74785..0000000 --- a/src/main/java/com/ikdaman/domain/auth/service/KakaoAuthService.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.ikdaman.domain.auth.service; - -import com.ikdaman.domain.auth.model.AuthReq; -import com.ikdaman.domain.auth.model.AuthRes; -import com.ikdaman.domain.member.entity.Member; -import com.ikdaman.domain.member.repository.MemberRepository; -import com.ikdaman.domain.member.service.MemberService; -import com.ikdaman.global.auth.client.ClientKakao; -import com.ikdaman.global.auth.token.AuthToken; -import com.ikdaman.global.auth.token.AuthTokenProvider; -import com.ikdaman.global.exception.BaseException; -import com.ikdaman.global.util.RandomNickname; -import com.ikdaman.global.util.RedisService; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_ACCESS_TOKEN; - -@Service("kakao") -@RequiredArgsConstructor -public class KakaoAuthService implements OAuthService { - - private final ClientKakao clientKakao; - private final AuthTokenProvider authTokenProvider; - private final MemberService memberService; - private final MemberRepository memberRepository; - private final RedisService redisService; - private final RandomNickname randomNickname; - - @Value("${auth.refresh-token-validity}") - private long refreshExpiry; // RefreshToken 만료일 - - @Override - @Transactional - public AuthRes login(AuthReq dto, String socialAccessToken) { - - // 1. 소셜 accessToken을 통해 Kakao userId 가져오기 - String checkProviderId = clientKakao.getUserData(socialAccessToken); - - // 2. 요청으로 들어온 providerId와 비교해서 검증 - if (!dto.getProviderId().equals(checkProviderId)) throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); - - // 3. 기존 회원 조회 - Member member = memberRepository.findBySocialTypeAndProviderId(Member.SocialType.KAKAO, checkProviderId) - .orElseGet(() -> { - String nickname; - do { - nickname = randomNickname.generate(); - } while (!memberService.isAvailableNickname(nickname)); // 닉네임 중복되면 다시 생성 - - // 유저 정보 저장 - Member newMember = Member.builder() - .socialType(Member.SocialType.KAKAO) - .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(); - } -} diff --git a/src/main/java/com/ikdaman/domain/auth/service/NaverAuthService.java b/src/main/java/com/ikdaman/domain/auth/service/SocialAuthService.java similarity index 58% rename from src/main/java/com/ikdaman/domain/auth/service/NaverAuthService.java rename to src/main/java/com/ikdaman/domain/auth/service/SocialAuthService.java index 6ae3590..dec0649 100644 --- a/src/main/java/com/ikdaman/domain/auth/service/NaverAuthService.java +++ b/src/main/java/com/ikdaman/domain/auth/service/SocialAuthService.java @@ -3,66 +3,74 @@ import com.ikdaman.domain.auth.model.AuthReq; import com.ikdaman.domain.auth.model.AuthRes; import com.ikdaman.domain.member.entity.Member; +import com.ikdaman.global.auth.enumerate.Provider; import com.ikdaman.domain.member.repository.MemberRepository; import com.ikdaman.domain.member.service.MemberService; -import com.ikdaman.global.auth.client.ClientNaver; import com.ikdaman.global.auth.token.AuthToken; import com.ikdaman.global.auth.token.AuthTokenProvider; import com.ikdaman.global.exception.BaseException; import com.ikdaman.global.util.RandomNickname; import com.ikdaman.global.util.RedisService; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import static com.ikdaman.global.exception.ErrorCode.NOT_MATCH_TOKEN_PROVIDER; -@Service("naver") +/** + * 통합 소셜 로그인 서비스 + */ +@Service @RequiredArgsConstructor -public class NaverAuthService implements OAuthService { +@Transactional +public class SocialAuthService implements SocialService { - private final ClientNaver clientNaver; - private final AuthTokenProvider authTokenProvider; - private final MemberService memberService; + private final SocialTokenValidator tokenValidator; private final MemberRepository memberRepository; - private final RedisService redisService; + private final MemberService memberService; private final RandomNickname randomNickname; + private final AuthTokenProvider authTokenProvider; + private final RedisService redisService; @Value("${auth.refresh-token-validity}") private long refreshExpiry; // RefreshToken 만료일 @Override - @Transactional - public AuthRes login(AuthReq dto, String socialAccessToken) { + public AuthRes login(AuthReq req, String socialToken) { + + // 1. Provider 문자열 → Enum 변환 + Provider provider = Provider.from(req.getProvider()); - // 1. 소셜 accessToken을 통해 Naver userId 가져오기 - String checkProviderId = clientNaver.getUserData(socialAccessToken); + // 2. 토큰 검증 및 providerId(sub) 추출 + String providerIdFromToken = tokenValidator.validate(provider, socialToken); - // 2. 요청으로 들어온 providerId와 비교해서 검증 - if (!dto.getProviderId().equals(checkProviderId)) throw new BaseException(NOT_MATCH_TOKEN_PROVIDER); + // 3. 요청의 providerId와 토큰에서 추출한 providerId 일치 검증 + if (!req.getProviderId().equals(providerIdFromToken)) throw new BaseException(NOT_MATCH_TOKEN_PROVIDER); - // 3. 기존 회원 조회 - Member member = memberRepository.findBySocialTypeAndProviderId(Member.SocialType.NAVER, checkProviderId) + // 4. 회원 조회(없으면 생성) + Member member = memberRepository + .findBySocialTypeAndProviderId(Member.SocialType.valueOf(provider.name()), providerIdFromToken) .orElseGet(() -> { String nickname; do { nickname = randomNickname.generate(); } while (!memberService.isAvailableNickname(nickname)); // 닉네임 중복되면 다시 생성 - // 유저 정보 저장 + // 신규 회원 저장 Member newMember = Member.builder() - .socialType(Member.SocialType.NAVER) - .providerId(checkProviderId) + .socialType(Member.SocialType.valueOf(provider.name())) + .providerId(providerIdFromToken) .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); + // 5. 토큰 발급 및 리프레시 토큰 Redis 저장 + String key = String.valueOf(member.getMemberId()); + AuthToken accessToken = authTokenProvider.createUserAppToken(key); + AuthToken refreshToken = authTokenProvider.createRefreshToken(key); + redisService.setValuesWithTimeout(key, refreshToken.getToken(), refreshExpiry); return AuthRes.builder() .accessToekn(accessToken.getToken()) diff --git a/src/main/java/com/ikdaman/domain/auth/service/OAuthService.java b/src/main/java/com/ikdaman/domain/auth/service/SocialService.java similarity index 85% rename from src/main/java/com/ikdaman/domain/auth/service/OAuthService.java rename to src/main/java/com/ikdaman/domain/auth/service/SocialService.java index 9377c72..c7f3e69 100644 --- a/src/main/java/com/ikdaman/domain/auth/service/OAuthService.java +++ b/src/main/java/com/ikdaman/domain/auth/service/SocialService.java @@ -3,6 +3,6 @@ import com.ikdaman.domain.auth.model.AuthReq; import com.ikdaman.domain.auth.model.AuthRes; -public interface OAuthService { +public interface SocialService { AuthRes login(AuthReq dto, String socialToken); } \ No newline at end of file diff --git a/src/main/java/com/ikdaman/domain/auth/service/SocialTokenValidator.java b/src/main/java/com/ikdaman/domain/auth/service/SocialTokenValidator.java new file mode 100644 index 0000000..b04433a --- /dev/null +++ b/src/main/java/com/ikdaman/domain/auth/service/SocialTokenValidator.java @@ -0,0 +1,29 @@ +package com.ikdaman.domain.auth.service; + +import com.ikdaman.global.auth.enumerate.Provider; +import com.ikdaman.global.auth.client.SocialTokenClient; +import com.ikdaman.global.exception.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Map; + +import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_PROVIDER; + +/** + * Provider에 맞는 SocialTokenClient를 찾아 토큰을 검증하고 providerId를 추출 + */ +@Service +@RequiredArgsConstructor +public class SocialTokenValidator { + + private final Map clients; + + public String validate(Provider provider, String token) { + SocialTokenClient client = clients.values().stream() + .filter(c -> c.provider() == provider) + .findFirst().orElse(null); + if (client == null) throw new BaseException(INVALID_SOCIAL_PROVIDER); + return client.extractProviderId(token); + } +} diff --git a/src/main/java/com/ikdaman/global/auth/client/AppleClient.java b/src/main/java/com/ikdaman/global/auth/client/AppleClient.java new file mode 100644 index 0000000..994c076 --- /dev/null +++ b/src/main/java/com/ikdaman/global/auth/client/AppleClient.java @@ -0,0 +1,166 @@ +package com.ikdaman.global.auth.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ikdaman.global.auth.enumerate.Provider; +import com.ikdaman.global.exception.BaseException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +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.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static com.ikdaman.global.exception.ErrorCode.APPLE_SERVER_ERROR; +import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_ACCESS_TOKEN; + +@Component +@RequiredArgsConstructor +public class AppleClient implements SocialTokenClient { + + @Value("${auth.apple.client-id}") + private String clientId; + + private final WebClient webClient; + private final ObjectMapper objectMapper; + + /** + * Apple ID 토큰을 검증하고 사용자 정보를 추출 + * + * @param idToken Apple ID 토큰 + * @return 사용자 Apple 계정의 providerId + */ + public String getUserDataByIdToken(String idToken) { + try { + // Apple의 공개키 목록 조회 + AppleKeysResponse keysResponse = webClient.get() + .uri("https://appleid.apple.com/auth/keys") + .retrieve() + .onStatus(status -> status.is4xxClientError(), response + -> Mono.error(new BaseException(INVALID_SOCIAL_ACCESS_TOKEN))) + .onStatus(status -> status.is5xxServerError(), response + -> Mono.error(new BaseException(APPLE_SERVER_ERROR))) + .bodyToMono(AppleKeysResponse.class) + .block(); + + if (keysResponse == null || keysResponse.getKeys() == null) { + throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); + } + + // ID 토큰을 파싱하여 헤더 정보 추출 + String[] tokenParts = idToken.split("\\."); + if (tokenParts.length != 3) { + throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); + } + + // 헤더 디코딩 + String headerJson = new String(Base64.getUrlDecoder().decode(tokenParts[0])); + Map header; + try { + header = objectMapper.readValue(headerJson, Map.class); + } catch (Exception e) { + throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); + } + String kid = (String) header.get("kid"); + String alg = (String) header.get("alg"); + + // 해당 키 찾기 + AppleKey matchingKey = keysResponse.getKeys().stream() + .filter(key -> key.getKid().equals(kid)) + .findFirst() + .orElseThrow(() -> new BaseException(INVALID_SOCIAL_ACCESS_TOKEN)); + + // 공개키 생성 + PublicKey publicKey = createPublicKey(matchingKey); + + // 토큰 검증 및 페이로드 추출 + Claims claims = verifyAndDecodeToken(idToken, publicKey, alg); + + // aud (audience) 검증 + String aud = claims.getAudience(); + if (!clientId.equals(aud)) { + throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); + } + + // sub (subject) 반환 - Apple의 providerId + return claims.getSubject(); + + } catch (Exception e) { + if (e instanceof BaseException) { + throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); + } + throw new BaseException(INVALID_SOCIAL_ACCESS_TOKEN); + } + } + + /** + * Apple 공개키를 생성 + */ + private PublicKey createPublicKey(AppleKey key) throws Exception { + byte[] nBytes = Base64.getUrlDecoder().decode(key.getN()); + byte[] eBytes = Base64.getUrlDecoder().decode(key.getE()); + + BigInteger n = new BigInteger(1, nBytes); + BigInteger e = new BigInteger(1, eBytes); + + RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return factory.generatePublic(spec); + } + + /** + * 토큰을 검증하고 페이로드를 디코딩 + */ + private Claims verifyAndDecodeToken(String token, PublicKey publicKey, String alg) { + return Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(token) + .getBody(); + } + + @Override + public Provider provider() { + return Provider.APPLE; + } + + @Override + public String extractProviderId(String token) { + return this.getUserDataByIdToken(token); + } + + // Apple Keys Response DTO + public static class AppleKeysResponse { + private List keys; + + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys; + } + } + + // Apple Key DTO + @Setter + @Getter + public static class AppleKey { + private String kty; + private String kid; + private String use; + private String alg; + private String n; + private String e; + } +} diff --git a/src/main/java/com/ikdaman/global/auth/client/ClientGoogle.java b/src/main/java/com/ikdaman/global/auth/client/GoogleClient.java similarity index 61% rename from src/main/java/com/ikdaman/global/auth/client/ClientGoogle.java rename to src/main/java/com/ikdaman/global/auth/client/GoogleClient.java index 2844fe7..473c7fb 100644 --- a/src/main/java/com/ikdaman/global/auth/client/ClientGoogle.java +++ b/src/main/java/com/ikdaman/global/auth/client/GoogleClient.java @@ -4,6 +4,7 @@ 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.enumerate.Provider; import com.ikdaman.global.auth.payload.OAuthUserRes; import com.ikdaman.global.exception.BaseException; import lombok.RequiredArgsConstructor; @@ -12,6 +13,8 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.Arrays; import static com.ikdaman.global.exception.ErrorCode.GOOGLE_SERVER_ERROR; @@ -19,7 +22,7 @@ @Component @RequiredArgsConstructor -public class ClientGoogle { +public class GoogleClient implements SocialTokenClient { @Value("${auth.google.client-id.android}") private String androidClientId; @@ -64,28 +67,43 @@ public String getUserDataByAccessToken(String accessToken) { * @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); + public String getUserDataByIdToken(String idToken) { + try { + // 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(); + } catch (GeneralSecurityException e) { + throw new BaseException(GOOGLE_SERVER_ERROR); + } catch (IOException e) { + throw new BaseException(GOOGLE_SERVER_ERROR); } + } + + @Override + public Provider provider() { + return Provider.GOOGLE; + } - // Payload에서 Google의 "sub" (providerId) 추출 - GoogleIdToken.Payload payload = googleIdToken.getPayload(); - return payload.getSubject(); + @Override + public String extractProviderId(String token) { + return this.getUserDataByIdToken(token); } } diff --git a/src/main/java/com/ikdaman/global/auth/client/ClientKakao.java b/src/main/java/com/ikdaman/global/auth/client/KakaoClient.java similarity index 83% rename from src/main/java/com/ikdaman/global/auth/client/ClientKakao.java rename to src/main/java/com/ikdaman/global/auth/client/KakaoClient.java index cc6f642..e42462f 100644 --- a/src/main/java/com/ikdaman/global/auth/client/ClientKakao.java +++ b/src/main/java/com/ikdaman/global/auth/client/KakaoClient.java @@ -1,5 +1,6 @@ package com.ikdaman.global.auth.client; +import com.ikdaman.global.auth.enumerate.Provider; import com.ikdaman.global.auth.payload.OAuthUserRes; import com.ikdaman.global.exception.BaseException; import lombok.RequiredArgsConstructor; @@ -13,11 +14,10 @@ @Component @RequiredArgsConstructor -public class ClientKakao { +public class KakaoClient implements SocialTokenClient { private final WebClient webClient; - // TODO: HttpHeaders.AUTHORIZATION 사용하도록 변경 public String getUserData(String accessToken) { OAuthUserRes OAuthUserRes = webClient.get() .uri("https://kapi.kakao.com/v2/user/me") // Kakao의 유저 정보 받아오는 url @@ -34,4 +34,14 @@ public String getUserData(String accessToken) { return String.valueOf(OAuthUserRes.getId()); } + + @Override + public Provider provider() { + return Provider.KAKAO; + } + + @Override + public String extractProviderId(String token) { + return this.getUserData(token); + } } diff --git a/src/main/java/com/ikdaman/global/auth/client/ClientNaver.java b/src/main/java/com/ikdaman/global/auth/client/NaverClient.java similarity index 83% rename from src/main/java/com/ikdaman/global/auth/client/ClientNaver.java rename to src/main/java/com/ikdaman/global/auth/client/NaverClient.java index 7aca268..96a6525 100644 --- a/src/main/java/com/ikdaman/global/auth/client/ClientNaver.java +++ b/src/main/java/com/ikdaman/global/auth/client/NaverClient.java @@ -1,5 +1,6 @@ package com.ikdaman.global.auth.client; +import com.ikdaman.global.auth.enumerate.Provider; import com.ikdaman.global.auth.payload.NaverUserRes; import com.ikdaman.global.exception.BaseException; import lombok.RequiredArgsConstructor; @@ -12,11 +13,10 @@ @Component @RequiredArgsConstructor -public class ClientNaver { +public class NaverClient implements SocialTokenClient { private final WebClient webClient; - // TODO: HttpHeaders.AUTHORIZATION 사용하도록 변경 public String getUserData(String accessToken) { NaverUserRes userRes = webClient.get() .uri("https://openapi.naver.com/v1/nid/me") // Naver의 유저 정보 받아오는 url @@ -33,4 +33,14 @@ public String getUserData(String accessToken) { return userRes.getResponse().getId(); } + + @Override + public Provider provider() { + return Provider.NAVER; + } + + @Override + public String extractProviderId(String token) { + return this.getUserData(token); + } } diff --git a/src/main/java/com/ikdaman/global/auth/client/SocialTokenClient.java b/src/main/java/com/ikdaman/global/auth/client/SocialTokenClient.java new file mode 100644 index 0000000..0079948 --- /dev/null +++ b/src/main/java/com/ikdaman/global/auth/client/SocialTokenClient.java @@ -0,0 +1,11 @@ +package com.ikdaman.global.auth.client; + +import com.ikdaman.global.auth.enumerate.Provider; + +/** + * 소셜 토큰 검증/파싱 + */ +public interface SocialTokenClient { + Provider provider(); + String extractProviderId(String token); +} diff --git a/src/main/java/com/ikdaman/global/auth/enumerate/Provider.java b/src/main/java/com/ikdaman/global/auth/enumerate/Provider.java new file mode 100644 index 0000000..fe367cf --- /dev/null +++ b/src/main/java/com/ikdaman/global/auth/enumerate/Provider.java @@ -0,0 +1,22 @@ +package com.ikdaman.global.auth.enumerate; + +import com.ikdaman.global.exception.BaseException; + +import static com.ikdaman.global.exception.ErrorCode.INVALID_SOCIAL_PROVIDER; + +/** + * 소셜 로그인 제공자 + */ +public enum Provider { + KAKAO, NAVER, GOOGLE, APPLE; + + /** + * 문자열 to Enum + * @param name 제공자 문자열 + * @return 매칭되는 Provider 값 + */ + public static Provider from(String name) { + if (name == null) throw new BaseException(INVALID_SOCIAL_PROVIDER); + return Provider.valueOf(name.trim().toUpperCase()); + } +}