도입 목적
기존 유저 서비스 Express 서버에 존재하던 카카오 로그인/회원가입 기능을 Spring 서버로 마이그레이션하기 위함과 IOS APP 출시를 위한 애플 로그인/회원가입 기능이 필요했기 때문에 도입하게 되었습니다.
또한 현재 DB엔 계정 테이블이 권한에 따라 user, manager, admin 3개의 테이블로 나눠져있었으며 각 테이블엔 해당 계정에 필요한 개인 정보를 함께 포함하고 있었기 때문에
OAuth2.0 제공 서비스에 따라 수집할 수 있는 개인정보가 다른 점을 포괄하고 계정의 권한별로 테이블이 나눠져 있는 점을 개선하고자 기존 ERD에서의 변경 작업도 함께 진행하게 되었습니다.
이 때 핵심적인 목표는 각 루트로 회원가입한 유저를 한 계정에 통합시키는 것이었습니다.
선행 학습
카카오와 애플에서 제공하는 OAuth2.0 도입하기 전에 어떤 방식으로 동작하는지와 ERD를 설계하기 위해 이들이 제공하는 데이터가 어디까지고 지금 서비스에 어떤게 필요한지 파악하기 위해 먼저 조사해보았습니다.
먼저 PAYCO에 OAuth2.0에 대한 설명이 자세하게 되어있어서 가져왔습니다.
아래는 카카오에서 제공하는 OAuth2.0입니다.
클라이언트에게 보여지는 화면으로는 카카오 로그인을 선택한 후 동의 항목에 동의만 하면 로그인이 되는 과정입니다.
1.
유저가 카카오 로그인을 하면 FE에서 https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={RestAPI Key}&redirect_uri={redirectUrl} 에 GET 요청해 인증 코드를 발급 받고 Redirect URI를 통해 인증 코드를 받습니다.
2.
인증 코드를 통해 https://kauth.kakao.com/oauth/token?client_id={RestAPI Key}&redirect_uri={redirectUrl}&code={받아온 인증 코드}&grant_type=authorization_code 에 POST 요청해 accessToken과 refreshToken을 받아옵니다.
3.
이제 받아온 accessToken과 refreshToken을 BE에 보내면 https://kapi.kakao.com/v2/user/me 에 유저 정보를 GET 요청으로 가져올 수 있게 됩니다.
4.
가져온 유저 정보로 DB에 해당 유저가 등록한 계정이 있는지 확인하고 기존 유저인데 카카오 로그인을 한 적이 없다면 카카오ID를 저장하고 로그인, 카카오 통합 유저라면 바로 로그인, 기존 유저가 아니라면 회원가입 로직을 처리합니다.
5.
현재 저희 서비스에서는 닉네임을 추가로 수집하기 위해 회원가입 토큰을 발급한 후 회원가입 API를 호출하여 따로 처리하고 있습니다.
위 사진은 카카오 싱크를 통해 수집할 수 있는 정보 목록입니다.
카카오ID, 닉네임, 프로필 사진, 이메일 등은 필수로 받을 수 있고
그 외에 이름, 성별, 연령대, 생년월일, 전화번호 등은 동의를 통해 수집할 수 있는걸 확인할 수 있습니다.
로그아웃 시엔 받아온 토큰을 만료시키기 위해 서비스 로그아웃과 카카오계정 로그아웃을 각각 수행하고
회원탈퇴 시엔 카카오ID를 포함한 제공 받은 개인정보를 모두 파기하도록 권장하고 있습니다.
다음으로 애플에서 제공하는 OAuth2.0입니다.
1.
유저는 애플 인증을 거친 후 userIdentifier, authorizationCode, identityToken 등을 받아 APP에 로그인 요청을 합니다.
2.
APP은 유저에게 받은 정보들을 담아 BE의 애플 인증 API를 호출합니다.
3.
BE는 Apple ID Server에서 공개키를 받아와 받은 identityToken을 열어보고 안에 값이 맞는지 1차 검증을 합니다.
4.
identityToken의 값을 기반으로 clientSecretToken을 만들어 비밀키로 사인한 후 authorizationCode와 함께 Apple ID Server에 2차 검증을 받습니다.
5.
2차 검증까지 통과하면 refreshToken을 받을 수 있고 refreshToken만으로도 검증이 가능하며 이를 통해 accessToken과 identityToken을 다시 받아올 수 있습니다.
authorizationCode는 5분, identityToken은 10분 뒤 만료되기 때문에 APP측에서 session을 유지하기 위해선 authorizationCode를 통해 session 유지를 위한 token을 Apple ID Server로 부터 발급받아야 합니다.
또한 identityToken을 검증하기 위해선 BE에서 다음과 같은 처리가 필요합니다.
•
Verify the JWS E256 signature using the server’s public key
•
Verify the nonce for the authentication
•
Verify that the iss field contains https://appleid.apple.com
•
Verify that the aud field is the developer’s client_id
•
Verify that the time is earlier than the exp value of the token
유저가 애플 인증을 거치게 되면 받는 IdentityToken에는 위 사진에 있는 정보들이 담겨져 있습니다.
적용 과정
카카오 로그인을 먼저 구현해보겠습니다.
//AuthController.java
@Operation(summary = "카카오 인증", description = "| 설명 | 인증 필요여부 | 추가 설명 |\n| --- | --- | --- |\n| 유저 카카오 인증 시 사용되는 API | false | |")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "**`OK`**\n\n### 1. 이미 카카오 가입이 되어있는 경우\n- result : \"LOGIN_OK\"\n- token : 일반 로그인 했을 때와 같은 토큰이 발급된다.\n- 처리 : 로그인과 동일한 로직으로 로그인 시키면 됨\n\n### 2. 동일 이메일로 회원가입을 해뒀던 경우\n- result : \"GOCHO_USER\"\n- token : 일반 로그인 했을 때와 같은 토큰이 발급된다.\n- 처리 : \"이미 고초대졸닷컴에 가입된 이메일입니다. 해당 계정에 카카오 로그인이 연결되었습니다!\" 안내창을 띄워주고 온 토큰으로 로그인\n\n### 3. 새로 가입하는 유저\n- result : \"NEW_USER\"\n- token : 일반 로그인 했을 때와 같은 토큰이 발급된다.\n- 처리 : \"로그인과 동일한 로직으로 로그인 시키면 됨\"", content = @Content(schema = @Schema(implementation = ResponseDTO.class), examples = @ExampleObject(value = "{\n" + " \"status\": \"Number\",\n" + " \"statusName\": \"String\",\n" + " \"data\": {\n" + "\t\t\"result\": \"String\",\n" + " \"accessToken\": \"String\",\n" + " \"refreshToken\": \"String\"\n" + " }\n" + "}"))),
@ApiResponse(responseCode = "400", description = "**`BLANK_TOKEN`**\n\n- D : 토큰이 공백이거나 입력되지 않은 경우\n- M : 토큰이 공백이거나 입력되지 않았습니다.\n- P : F", content = @Content)
})
@PostMapping("/kakao")
public ResponseEntity<ResponseDTO> kakaoAuth(HttpServletRequest request, @RequestBody @Valid KakaoOAuth2DTO kakaoOAuth2DTO) {
return new ResponseEntity<>(authService.kakaoAuth(request, kakaoOAuth2DTO), HttpStatus.OK);
}
Java
복사
HTTP 요청을 매핑하기 위해 유저 서비스의 AuthController에 kakaoLogin과 kakaoRegister 메서드를 작성해줍니다.
API를 사용하는 클라이언트를 위한 Swagger 문서화도 어노테이션을 통해 Controller 레이어에 함께 작성해줍니다.
//KakaoOAuth2DTO.java
@Getter @Setter
@NoArgsConstructor
public class KakaoOAuth2DTO {
@NotBlank(message = "토큰이 존재하지 않습니다.")
@Schema(name = "access_token", description = "엑세스 토큰", example = "엑세스 토큰", required = true)
private String accessToken;
@NotBlank(message = "토큰이 존재하지 않습니다.")
@Schema(name = "refreseh_token", description = "리프레시 토큰", example = "리프레시 토큰", required = true)
private String refreshToken;
@Builder
public KakaoOAuth2DTO(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Java
복사
클라이언트에서 보내주는 accessToken과 refreshToken을 받기 위한 카카오 인증 DTO도 만들어줍니다.
Service 레이어에서 인증 로직을 작성하겠습니다.
//AuthService.java
public ResponseDTO kakaoAuth(HttpServletRequest request, KakaoOAuth2DTO kakaoOAuth2DTO) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> userInfo = kakaoOAuth2Provider.getUserInfo(kakaoOAuth2DTO.getAccessToken());
Long kakaoId = Long.valueOf((String) userInfo.get("id"));
String email = (String) userInfo.get("email");
User user = userRepository.findByKakaoIdOrEmail(kakaoId, email);
if (user != null && user.getKakaoId() != null && user.getKakaoId().equals(kakaoId)) {
result.put("result", "LOGIN_OK");
} else if (user != null && user.getKakaoId() == null && user.getEmail().equals(email)) {
user.updateKakaoId(kakaoId);
result.put("result", "GOCHO_USER");
} else {
User newUser = User.builder()
.email(email)
.password("kakao_only")
.kakaoId(kakaoId)
.appleId(null)
.nickname(nicknameGenerator())
.badge("default")
.image(List.of("default", "default_work", "jobi", "jobi_safety", "jobi_chat", "jobi_play", "jobi_teach").get((int) (Math.random() * 6)))
.share(0)
.build();
userRepository.save(newUser);
user = newUser;
result.put("result", "NEW_USER");
}
CustomUserDetails principal = new CustomUserDetails(user);
String accessToken = authenticationProvider.createAccessToken(principal);
String refreshToken = authenticationProvider.createRefreshToken(request, principal, 0);
result.put("access_token", accessToken);
result.put("refresh_token", refreshToken);
return ResponseDTO.builder()
.status(HttpStatus.OK.value())
.statusName(HttpStatus.OK.name())
.data(result)
.build();
}
Java
복사
DTO에 담겨온 accessToken으로 유저 정보(카카오ID, 이메일)를 가져온 후 Repository 레이어에서 해당 정보와 일치하는 유저를 조회합니다.
이 때 분기점은 3가지로 핵심적인 목표였던 유저 통합을 위해 기존 유저인지 신규 유저인지 구분합니다.
1.
조회된 유저의 카카오 ID와 전달 받은 카카오 ID가 일치한다면 로그인 처리
2.
조회된 유저의 이메일은 일치하지만 기존 카카오 ID가 null이라면 기존 유저로 통합 후 로그인 처리
3.
유저가 조회되지 않는다면 신규 유저로 회원가입을 시킨 후 로그인 처리
이제 받아왔던 토큰으로 유저 정보를 받아오는 역할을 하는 KaKaoOAuth2Provider 클래스를 작성하였습니다.
//KakaoOAuth2Provider.java
@Slf4j
@Service
public class KakaoOAuth2Provider {
public Map<String, Object> getUserInfo(String token) {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
ResponseEntity<String> response = rt.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.GET, request, String.class);
String body = response.getBody();
JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject();
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", jsonObject.get("id").getAsString());
userInfo.put("email", jsonObject.getAsJsonObject("kakao_account").get("email").getAsString());
userInfo.put("nickname", jsonObject.getAsJsonObject("properties").get("nickname").getAsString());
return userInfo;
}
}
Java
복사
카카오 유저의 정보를 받기 위해선 https://kapi.kakao.com/v2/user/me 여기로 헤더에 토큰을 담아 GET 요청을 보내주면 됩니다.
요청을 보낸 후 받아온 응답을 JSON Object로 만들어 카카오 ID, 이메일을 뽑아 userInfo 라는 맵을 만들어 반환합니다.
마지막으로 유저를 조회하기 위한 Repository 레이어의 코드를 작성하였습니다.
//UserRepository.java
public User findByEmail(String email) {
QUser user = QUser.user;
return jpaQueryFactory
.selectFrom(user)
.where(user.email.eq(email))
.fetchOne();
}
public User findByNick(String nick) {
QUser user = QUser.user;
return jpaQueryFactory
.selectFrom(user)
.where(user.nickname.eq(nick))
.fetchOne();
}
public User findByKakaoIdOrEmail(Long kakaoId, String email) {
QUser user = QUser.user;
return jpaQueryFactory
.selectFrom(user)
.where(user.kakaoId.eq(kakaoId).or(user.email.eq(email)))
.fetchOne();
}
public void save(User user) {
em.persist(user);
}
Java
복사
현재 저희 서비스는 MySQL + JPA + QueryDSL을 사용하고 있습니다.
단순한 코드라 자세한 설명은 생략하도록 하겠습니다.
아래부터는 애플 로그인 구현 과정입니다.
//AuthController.java
@Operation(summary = "애플 인증", description = "| 설명 | 인증 필요여부 | 추가 설명 |\n| --- | --- | --- |\n| 유저 애플 인증 시 사용되는 API | false | |")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "**`OK`**\n\n### 1. 이미 애플 가입이 되어있는 경우\n- result : \"LOGIN_OK\"\n- token : 일반 로그인 했을 때와 같은 토큰이 발급된다.\n- 처리 : 로그인과 동일한 로직으로 로그인 시키면 됨\n\n### 2. 동일 이메일로 회원가입을 해뒀던 경우\n- result : \"GOCHO_USER\"\n- token : 일반 로그인 했을 때와 같은 토큰이 발급된다.\n- 처리 : \"이미 고초대졸닷컴에 가입된 이메일입니다. 해당 계정에 카카오 로그인이 연결되었습니다!\" 안내창을 띄워주고 온 토큰으로 로그인\n\n### 3. 새로 가입하는 유저\n- result : \"NEW_USER\"\n- token : 일반 로그인 했을 때와 같은 토큰이 발급된다.\n- 처리 : \"로그인과 동일한 로직으로 로그인 시키면 됨\"", content = @Content(schema = @Schema(implementation = ResponseDTO.class), examples = @ExampleObject(value = "{\n" + " \"status\": \"Number\",\n" + " \"statusName\": \"String\",\n" + " \"data\": {\n" + "\t\t\"result\": \"String\",\n" + " \"accessToken\": \"String\",\n" + " \"refreshToken\": \"String\"\n" + " }\n" + "}"))),
@ApiResponse(responseCode = "400", description = "**`BLANK_CODE`**\n\n- D : userIdentifier 또는 identityToken 또는 authorizationCode가 공백이거나 입력되지 않은 경우\n- M : 코드가 공백이거나 입력되지 않았습니다.\n- P : F", content = @Content),
@ApiResponse(responseCode = "500", description = "**`FAIL_GET_PUBLICKEY`**\n\n- D : 공개키를 가져오는 과정에서 에러가 발생한 경우\n- M : 공개키를 가져오는 것에 실패하였습니다.\n- P : T\n\n" +
"**`FAIL_GET_PRIVATEKEY`**\n\n- D : 비밀키를 가져오는 과정에서 에러가 발생한 경우\n- M : 비밀키를 가져오는 것에 실패하였습니다.\n- P : T\n\n" +
"**`FAIL_GET_PAYLOAD`**\n\n- D : identityToken의 payload를 가져오는 과정에서 에러가 발생한 경우\n- M : PAYLOAD를 가져오는 것에 실패하였습니다.\n- P : T\n\n" +
"**`FAIL_VALIDATE_TOKEN`**\n\n- D : clientSecret 토큰을 검증하는 과정에서 에러가 발생한 경우\n- M : 토큰을 검증하는 것에 실패하였습니다.\n- P : T", content = @Content)
})
@PostMapping("/apple")
public ResponseEntity<ResponseDTO> appleAuth(HttpServletRequest request, @RequestBody @Valid AppleOAuth2DTO appleOAuth2DTO) {
return new ResponseEntity<>(authService.appleAuth(request, appleOAuth2DTO), HttpStatus.OK);
}
Java
복사
우선 애플 인증 HTTP 요청을 받기 위한 메서드를 Controller 레이어에 만들어줍니다.
//AppleOAuth2DTO.java
@Getter @Setter
@ToString
@NoArgsConstructor
public class AppleOAuth2DTO {
@NotBlank(message = "코드가 존재하지 않습니다.")
@Schema(name = "user_identifier", description = "유저 식별값", example = "유저 식별값", required = true)
private String userIdentifier;
@NotBlank(message = "코드가 존재하지 않습니다.")
@Schema(name = "identity_token", description = "식별 토큰", example = "식별 토큰", required = true)
private String identityToken;
@NotBlank(message = "코드가 존재하지 않습니다.")
@Schema(name = "authorization_code", description = "인증 코드", example = "인증 코드", required = true)
private String authorizationCode;
@Schema(description = "상태", example = "상태")
private String state;
@Schema(description = "이메일", example = "이메일")
private String email;
@Schema(name = "family_name", description = "성", example = "성")
private String familyName;
@Schema(name = "given_name", description = "이름", example = "이름")
private String givenName;
}
Java
복사
APP에서 유저에게 받은 정보들을 전달 받을 DTO도 만들어줍니다.
//AppleOAuth2Provider.java
@Slf4j
@Service
public class AppleOAuth2Provider {
@Value("${oauth2.apple.auth-url}")
private String authUrl;
@Value("${oauth2.apple.team-id}")
private String teamId;
@Value("${oauth2.apple.client-id}")
private String clientId;
@Value("${oauth2.apple.key-id}")
private String keyId;
@Value("${oauth2.apple.key-path}")
private String keyPath;
private ApplePublicKeyDTO getPublicKey() {
try {
return WebClient.builder().build()
.get()
.uri("https://appleid.apple.com/auth/keys")
.retrieve()
.bodyToFlux(ApplePublicKeyDTO.class).blockFirst();
} catch (Exception e) {
throw new CustomException(ErrorCode.FAIL_GET_PUBLICKEY);
}
}
private PrivateKey getPrivateKey() {
try(InputStream ips = new ClassPathResource(keyPath).getInputStream()) {
byte[] bytes = IOUtils.toByteArray(ips);
String privateKey = new String(bytes);
Reader pemReader = new StringReader(privateKey);
PEMParser pemParser = new PEMParser(pemReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo object = PrivateKeyInfo.getInstance(pemParser.readObject());
return converter.getPrivateKey(object);
} catch (Exception e) {
throw new CustomException(ErrorCode.FAIL_GET_PRIVATEKEY);
}
}
public Claims getClaimsBy(String identityToken) {
if (!StringUtils.hasText(identityToken)) {
throw new CustomException(ErrorCode.EMPTY_TOKEN);
}
try {
ApplePublicKeyDTO applePublicKeyDTO = getPublicKey();
String headerOfIdentityToken = identityToken.substring(0, identityToken.indexOf("."));
Map<String, String> header = new ObjectMapper().readValue(new String(Base64.getDecoder().decode(headerOfIdentityToken), StandardCharsets.UTF_8), Map.class);
ApplePublicKeyDTO.Key key = applePublicKeyDTO.getMatchedKeyBy(header.get("kid"), header.get("alg")).orElseThrow(() -> new CustomException(ErrorCode.EMPTY_CODE));
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 publicKeySpec = new RSAPublicKeySpec(n, e);
KeyFactory keyFactory = KeyFactory.getInstance(key.getKty());
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(identityToken).getBody();
} catch (Exception ex) {
throw new CustomException(ErrorCode.FAIL_GET_PAYLOAD);
}
}
public String createClientSecret() {
Date now = new Date();
return Jwts.builder()
.setHeaderParam("kid", keyId)
.setHeaderParam("alg", "ES256")
.setIssuer(teamId)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + 3600000))
.setAudience(authUrl)
.setSubject(clientId)
.signWith(SignatureAlgorithm.ES256, getPrivateKey())
.compact();
}
public AppleValidatedTokenDTO validateToken(String clientSecret, String authorizationCode) {
if (!StringUtils.hasText(authorizationCode) || !StringUtils.hasText(clientSecret)) {
throw new CustomException(ErrorCode.EMPTY_CODE);
}
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("client_id", clientId);
formData.add("client_secret", clientSecret);
formData.add("code", authorizationCode);
formData.add("grant_type", "authorization_code");
try {
return WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configure -> configure.defaultCodecs()
.jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper().setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy()))))
.build()).build()
.post()
.uri("https://appleid.apple.com/auth/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(formData)
.retrieve()
.bodyToFlux(AppleValidatedTokenDTO.class).blockFirst();
} catch (Exception e) {
throw new CustomException(ErrorCode.FAIL_VALIDATE_TOKEN);
}
}
}
Java
복사
이 클래스는 애플 인증을 처리할 메서드들을 모아놓은 클래스입니다.
getPublicKey()는 Apple ID Server에 GET 요청을 보내 공개키를 받아오는 메서드입니다.
getPrivateKey()는 Apple Developer에서 다운 받은 p8 파일을 통해 비밀키를 가져오는 메서드입니다.
getClaimsBy()는 전달 받은 identityToken을 공개키를 이용해 파싱한 후 payload를 가져오는 메서드입니다.
createClientSecret()은 Apple ID Server에 identityToken 검증을 위한 clientSecret을 만드는 메서드로 이 때 비밀키를 사용해 JWT를 서명합니다.
validateToken()은 clientSecret과 authorizationCode를 이용해 Apple ID Server로부터 accessToken과 refreshToken을 받아오는 메서드입니다.
//ApplePublicKeyDTO.java
@Getter @Setter
public class ApplePublicKeyDTO {
private List<Key> keys;
@Getter @Setter
public static class Key {
private String kty;
private String kid;
private String use;
private String alg;
private String n;
private String e;
}
public Optional<Key> getMatchedKeyBy(String kid, String alg) {
return this.keys.stream()
.filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg))
.findFirst();
}
}
Java
복사
AppleOAuth2Provider 클래스의 getPublicKey() 메서드를 통해 공개키를 받아오기 위한 DTO도 작성해줍니다.
Apple ID Server에서 보내주는 공개키는 리스트로 Key 안에 kty, kid, use, alg, n, e와 같은 값들이 존재합니다.
getMatchedKeyBy()메서드는 유저가 보낸 IdentityToken을 서명한 키를 공개키 리스트에서 찾기 위한 메서드입니다.
//AppleValidatedTokenDTO.java
@Getter @Setter
public class AppleValidatedTokenDTO {
private String tokenType;
private String idToken;
private String accessToken;
private String refreshToken;
private Integer expiresIn;
}
Java
복사
AppleOAuth2Provider 클래스의 validatToken() 메서드를 통해 Apple ID Server에서 보내주는 accessToken, refreshToken 등을 받기 위한 DTO도 작성해줍니다.
마지막으로 애플 인증 로직을 처리하기 위한 코드도 Service 레이어에 작성해줍니다.
//AuthService.java
public ResponseDTO appleAuth(HttpServletRequest request, AppleOAuth2DTO appleOAuth2DTO) {
Map<String, Object> result = new HashMap<>();
String clientSecret = appleOAuth2Provider.createClientSecret();
AppleValidatedTokenDTO validatedToken = appleOAuth2Provider.validateToken(clientSecret, appleOAuth2DTO.getAuthorizationCode());
Claims claims;
if (StringUtils.hasText(validatedToken.getIdToken())) {
claims = appleOAuth2Provider.getClaimsBy(validatedToken.getIdToken());
} else {
throw new CustomException(ErrorCode.UNAUTHORIZED);
}
String email = String.valueOf(claims.get("email"));
String sub = String.valueOf(claims.get("sub"));
User user = userRepository.findByAppleIdOrEmail(sub, email);
if (user != null && user.getAppleId() != null && user.getAppleId().equals(sub)) {
result.put("result", "LOGIN_OK");
} else if (user != null && user.getAppleId() == null && user.getEmail().equals(email)) {
user.updateAppleId(sub);
result.put("result", "GOCHO_USER");
} else {
User newUser = User.builder()
.email(email)
.password("apple_only")
.kakaoId(null)
.appleId(sub)
.nickname(nicknameGenerator())
.badge("default")
.image(List.of("default", "default_work", "jobi", "jobi_safety", "jobi_chat", "jobi_play", "jobi_teach").get((int) (Math.random() * 6)))
.share(0)
.build();
userRepository.save(newUser);
user = newUser;
result.put("result", "NEW_USER");
}
CustomUserDetails principal = new CustomUserDetails(user);
String accessToken = authenticationProvider.createAccessToken(principal);
String refreshToken = authenticationProvider.createRefreshToken(request, principal, 0);
result.put("access_token", accessToken);
result.put("refresh_token", refreshToken);
return ResponseDTO.builder()
.status(HttpStatus.OK.value())
.statusName(HttpStatus.OK.name())
.data(result)
.build();
}
Java
복사