-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from 9oormthon-univ/feat/apple-login
[FEAT] Apple 로그인 구현
- Loading branch information
Showing
11 changed files
with
258 additions
and
13 deletions.
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
src/main/java/com/groom/swipo/domain/auth/dto/ApplePublicKey.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.groom.swipo.domain.auth.dto; | ||
|
||
public record ApplePublicKey( | ||
String kty, | ||
String kid, | ||
String alg, | ||
String n, | ||
String e) { | ||
} |
6 changes: 6 additions & 0 deletions
6
src/main/java/com/groom/swipo/domain/auth/dto/request/AppleLoginRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.groom.swipo.domain.auth.dto.request; | ||
|
||
public record AppleLoginRequest ( | ||
String token | ||
){ | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/com/groom/swipo/domain/auth/dto/response/ApplePublicKeyResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.groom.swipo.domain.auth.dto.response; | ||
|
||
import java.util.List; | ||
import javax.naming.AuthenticationException; | ||
|
||
import com.groom.swipo.domain.auth.dto.ApplePublicKey; | ||
|
||
public record ApplePublicKeyResponse(List<ApplePublicKey> keys) { | ||
public ApplePublicKey getMatchedKey(String kid, String alg) throws AuthenticationException { | ||
return keys.stream() | ||
.filter(key -> key.kid().equals(kid) && key.alg().equals(alg)) | ||
.findAny() | ||
.orElseThrow(AuthenticationException::new); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
src/main/java/com/groom/swipo/domain/auth/exception/AppleAuthException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.groom.swipo.domain.auth.exception; | ||
|
||
import com.groom.swipo.global.error.exception.AuthGroupException; | ||
|
||
public class AppleAuthException extends AuthGroupException { | ||
public AppleAuthException(String message) { | ||
super(message); | ||
} | ||
public AppleAuthException() { | ||
this("애플 서버와의 통신 과정에서 문제가 발생했습니다."); | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
src/main/java/com/groom/swipo/domain/auth/service/AppleLoginService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package com.groom.swipo.domain.auth.service; | ||
|
||
import java.net.URI; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.PublicKey; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.util.Map; | ||
import javax.naming.AuthenticationException; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
import org.springframework.web.client.RestClient; | ||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.groom.swipo.domain.auth.dto.response.ApplePublicKeyResponse; | ||
import com.groom.swipo.domain.auth.dto.response.SocialLoginResponse; | ||
import com.groom.swipo.domain.auth.exception.AppleAuthException; | ||
import com.groom.swipo.domain.auth.exception.InvalidTokenException; | ||
import com.groom.swipo.domain.auth.util.ApplePublicKeyGenerator; | ||
import com.groom.swipo.domain.auth.util.JwtValidator; | ||
import com.groom.swipo.domain.user.entity.User; | ||
import com.groom.swipo.domain.user.entity.enums.Provider; | ||
import com.groom.swipo.domain.user.repository.UserRepository; | ||
import com.groom.swipo.global.jwt.TokenProvider; | ||
|
||
import io.jsonwebtoken.ExpiredJwtException; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Service | ||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
public class AppleLoginService { | ||
|
||
private static final String APPLE_PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys"; | ||
|
||
private final ApplePublicKeyGenerator applePublicKeyGenerator; | ||
private final JwtValidator jwtValidator; | ||
private final TokenProvider tokenProvider; | ||
private final TokenRenewService tokenRenewService; | ||
private final RestClient restClient; | ||
private final UserRepository userRepository; | ||
private final ObjectMapper objectMapper; | ||
|
||
@Transactional | ||
public SocialLoginResponse appleLogin(String code) { | ||
try { | ||
String appleAccountId = getAppleAccountId(code); | ||
return userRepository.findByProviderAndProviderId(Provider.APPLE, appleAccountId) | ||
.map(this::handleExistingUserLogin) | ||
.orElseGet(() -> SocialLoginResponse.of(appleAccountId, "default 이미지입니다.")); | ||
} catch (JsonProcessingException e) { | ||
throw new InvalidTokenException("JSON 형식이 잘못되었습니다."); | ||
} catch (AuthenticationException e) { | ||
throw new AppleAuthException("인증 실패"); | ||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) { | ||
throw new InvalidTokenException("유효하지 않는 identity Token 입니다."); | ||
} | ||
catch (IllegalArgumentException e) { | ||
throw new InvalidTokenException("토큰이 만료되었거나 잘못된 인자가 포함되어있습니다."); | ||
} | ||
} | ||
|
||
private SocialLoginResponse handleExistingUserLogin(User user) { | ||
String accessToken = tokenProvider.createAccessToken(user); | ||
String refreshToken = tokenProvider.createRefreshToken(user); | ||
tokenRenewService.saveRefreshToken(refreshToken, user.getId()); | ||
return SocialLoginResponse.of(user.getId(), accessToken, refreshToken); | ||
} | ||
|
||
private String getAppleAccountId(String identityToken) | ||
throws JsonProcessingException, AuthenticationException, NoSuchAlgorithmException, InvalidKeySpecException { | ||
if (identityToken == null) { | ||
throw new IllegalArgumentException("토큰이 비어있습니다."); | ||
} | ||
|
||
Map<String, String> headers = jwtValidator.parseHeaders(identityToken); | ||
ApplePublicKeyResponse applePublicKeys = getAppleAuthPublicKey(); | ||
PublicKey publicKey = applePublicKeyGenerator.generatePublicKey(headers, applePublicKeys); | ||
|
||
try { | ||
return jwtValidator.getTokenClaims(identityToken, publicKey).getSubject(); | ||
} catch (ExpiredJwtException e) { | ||
throw new IllegalArgumentException("토큰이 만료되었습니다.", e); | ||
} | ||
} | ||
|
||
private ApplePublicKeyResponse getAppleAuthPublicKey() throws JsonProcessingException { | ||
String response = restClient.get() | ||
.uri(URI.create(APPLE_PUBLIC_KEY_URL)) | ||
.retrieve() | ||
.body(String.class); | ||
|
||
return objectMapper.readValue(response, ApplePublicKeyResponse.class); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/main/java/com/groom/swipo/domain/auth/util/ApplePublicKeyGenerator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.groom.swipo.domain.auth.util; | ||
|
||
import java.math.BigInteger; | ||
import java.security.KeyFactory; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.PublicKey; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.RSAPublicKeySpec; | ||
import java.util.Base64; | ||
import java.util.Map; | ||
|
||
import javax.naming.AuthenticationException; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import com.groom.swipo.domain.auth.dto.ApplePublicKey; | ||
import com.groom.swipo.domain.auth.dto.response.ApplePublicKeyResponse; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class ApplePublicKeyGenerator { | ||
public PublicKey generatePublicKey(Map<String, String> tokenHeaders, | ||
ApplePublicKeyResponse applePublicKeys) | ||
throws AuthenticationException, NoSuchAlgorithmException, InvalidKeySpecException { | ||
ApplePublicKey publicKey = applePublicKeys.getMatchedKey(tokenHeaders.get("kid"), | ||
tokenHeaders.get("alg")); | ||
return getPublicKey(publicKey); | ||
} | ||
private PublicKey getPublicKey(ApplePublicKey publicKey) | ||
throws NoSuchAlgorithmException, InvalidKeySpecException { | ||
byte[] nBytes = Base64.getUrlDecoder().decode(publicKey.n()); | ||
byte[] eBytes = Base64.getUrlDecoder().decode(publicKey.e()); | ||
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(new BigInteger(1, nBytes), | ||
new BigInteger(1, eBytes)); | ||
KeyFactory keyFactory = KeyFactory.getInstance(publicKey.kty()); | ||
return keyFactory.generatePublic(publicKeySpec); | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
src/main/java/com/groom/swipo/domain/auth/util/JwtValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package com.groom.swipo.domain.auth.util; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.security.PublicKey; | ||
import java.util.Base64; | ||
import java.util.Map; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jwts; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
// 해당 코드는 Jwt 전체에 대해 검증하는 것이 아닌 애플 로그인떄 받아오는 itentity token 검증용 | ||
@Component | ||
public class JwtValidator { | ||
|
||
public Map<String, String> parseHeaders(String token) throws JsonProcessingException { | ||
if (token == null) { | ||
throw new IllegalArgumentException("토큰이 비어있습니다."); | ||
} | ||
|
||
String header = token.split("\\.")[0]; | ||
return new ObjectMapper().readValue(decodeHeader(header), new TypeReference<Map<String, String>>() {}); | ||
} | ||
|
||
public String decodeHeader(String token) { | ||
return new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8); | ||
} | ||
|
||
public Claims getTokenClaims(String token, PublicKey publicKey) { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(publicKey) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters