오늘 포스팅은 ‘가는길 지금’ 서비스에서 구현한 애플 로그인 구현 과정에 대해서 알아보도록 하겠습니다.
보통 소셜로그인의 포스팅은 워낙 많아서, 따로 작성하지 않으려 했지만 이번 애플 로그인은 비교적? 정보가 적어서 작성을 하게 됐습니다.
보통 요즘 앱이든 웹이든 어떤 서비스를 만들게 되면, 소셜로그인 기능이 없는 서비스를 찾기 매우 매우? 힘듭니다
그만큼 사용자가 이용하기에 편리함을 제공하기 때문에, 필수적으로 개발을 합니다
저의 개인적인 경우도 그렇고 일반적으로 보통 소셜로그인을 사용하면 네이버, 카카오, 구글 이 세 가지 소셜로그인을 많이 이용하고 애플로그인은 거의 이용하지 않아 ‘가는길 지금’ 서비스에도 위에 세 가지 로그인만 추가하려 했지만, 저희 가는길 지금 서비스는 ios도 런칭 예정인데, ios 앱
의 경우에는 소셜 로그인을 사용하고 심사에 통과하려면 애플 로그인 구현을 필수적이라 구현하게 됐습니다….
애플 로그인 공식 문서는 여기서 확인 할 수 있습니다!
공식 문서를 확인해보면 , 매우 매우 정보가 적고 카카오,네이버,구글에 비해 공식문서가 매우 부실합니다.
어것또한 미니멀리즘인가?
불친절한 공식 문서 탓에 삽질을 통해서 완성을 했습니다,,,
먼저 구현 과정 들어가기 전에 앞서 , 먼저 애플 로그인 인증 흐름을 알아보도록 하겠습니다
애플 공식 문서에 보면 Sign in with Apple은 위와 같은 flow로 구현이 된다고 나와있습니다.(Insanely Simple …)
이걸 조금 더 이해하기 쉽게 아래 flow 다이어그램으로 살펴보겠습니다.
다이어그램을 통해 전체적인 흐름을 봤으니, 이제 본견적으로 애플 로그인을 구현하겠습니다
Apple Developer 설정 및 키 발급은 해당 포스팅에서 다루지 않습니다
자세한 내용은 애플 공식 문서 참고 부탁드리겠습니다 🙏🙏
사용할 파라미터만 보겠습니다
client_id (필수)
Apple Developer에 등록한 Services Ids에서 identifier
값
redirect_url (필수)
response_mode
비워두면
(empty fragment) GET
요청
form_post
는 redirect_url에 POST
요청
response_type : code 로 해야 인가 코드 반환
scope : 전달받을 정보 (name, email)
email name
로 설정
저는 아래와 같이 로그인 요청을 진행했습니다!
/**
* 애플 인가 코드 받아오기
*/
private URI getAppleAuthUri() {
return UriComponentsBuilder.fromHttpUrl("https://appleid.apple.com/auth/authorize")
.queryParam("client_id", appleProperties.getClientId())
.queryParam("redirect_uri", appleProperties.getRedirectUrl())
.queryParam("scope", "email name")
.queryParam("response_type", "code")
.queryParam("response_mode", "form_post")
.build().toUri();
}
그리고 따로 설정한 redirect_url에서 해당 요청을 post
로 받습니다
@Hidden
@PostMapping("/apple")
public ResponseEntity<Void> appleCallback(@RequestParam String code) {
AppleLoginParams appleLoginParams = new AppleLoginParams(code);
return oAuthLoginService.login(appleLoginParams);
}
로컬에서 테스트 불가… 도메인(https)이 등록이 돼있고 앱 환경에서만 테스트가 가능합니다.(꽤나 보수적인 애플,,)
response mode를 form_post
로 해야 개인 정보 공유한 유저에 한해, Email값이나 name값을 받아올 수 있습니다. 그래서 회원가입시 email이 필요한 서비스에서는 form_post
로 설정해야 합니다
저는 처음에 비워둬서 꽤나 삽집을 했답니다..
2번에서 말한 scope email, name은 최초 로그인 요청시에만 애플에서 제공합니다. 그래서 scope가 필요하단면, 첫 요청시 DB에 누락없이 저장을 해야합니다!
위에 과정을 통해 로그인 요청을 하고 인가 코드를 받아왔다면, 이제 저희가 Apple Developer에 설정한 cliend_id, team_id , pem 파일 등등 과 인가 코드를 통해 Client Secret을 생성해야 합니다
자세한 내용이 궁금하신 분들은 애플 공식 문서 클릭!
Client Secret은 저희가 애플 서버에 사용자 정보를 얻기 위해 " 나 진짜 이 앱 만든 팀 or 사람이야!"라고 증명하는 용도입니다
이 암호문을 만들 때는 JWT 형식을 사용합니다.
alg
: 서명 알고리즘으로 ES256
을 사용합니다.kid
: Apple Developer 계정의 10자리 키 식별자입니다.iss
: 개발자 계정의 10자리 Team IDiat
: 토큰 생성 시간exp
: 만료 시간aud
: https://appleid.apple.com
sub
: 앱의 Client ID (Services ID)/**
* client_secret 생성 Apple Document URL ‣
* https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
* @return client_secret(jwt)
*/
public String generateClientSecret() {
LocalDateTime expiration = LocalDateTime.now().plusMinutes(5);
return Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, appleProperties.getKeyId())
.setIssuer(appleProperties.getTeamId())
.setAudience(appleProperties.getAuth())
.setSubject(appleProperties.getClientId())
.setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
.setIssuedAt(new Date())
.signWith(readPrivateKey(), SignatureAlgorithm.ES256)
.compact();
}
/**
* 파일에서 private key 획득
* @return Private Key
*/
public PrivateKey readPrivateKey() {
try {
String privateKeyPEM = appleProperties.getPrivateKey()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", ""); // 공백 및 줄바꿈 제거
byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyPEM);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC"); // 필요한 알고리즘으로 변경 가능 (예: "RSA")
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
throw new RuntimeException("Error converting private key from String", e);
}
}
p8 파일
을 넣어주시면 됩니다이제 방금 전 단계에서 생성한 토큰을 이제 애플 서버로 검증 요청을 보내야 합니다!/
애플 서버가 토큰을 검증하는 방법
공식문서에 따르면, identity Token내 payload에 속한 값들이 변조되지 않았는지 검증하기 위해서는 애플 서버의 public key를 사용해 JWS E256 signature를 검증해야한다고 나와있습니다
자세한 내용은 공식문서 참고 부탁드리겠습니다
유효하다면 애플 서버에서는 아래와 같은 익숙치 않은 응답을 줍니다
{
"access_token": "cs12x...c65qq9",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rca6...sCSqs",
"id_token": "eyKse...23xdf"
}
정말 애플 번거럽죠?
보통 다른 OAuth는 이 단계에서 사용자의 정보를 주지만 , 애플 로그인은 여기서 id_token의
payload를 한 번 더 파싱 해서 사용자의 정보를 얻을 수 있습니다!!
아래 코드를 통해 id_token
을 파싱하면
드디어….! 다음과 같은 응답을 받을 수 있습니다!
private static <T> T decodePayload(String token, Class<T> targetClass) {
String[] tokenParts = token.split("\\.");
String payloadJWT = tokenParts[1];
Base64.Decoder decoder = Base64.getUrlDecoder();
String payload = new String(decoder.decode(payloadJWT));
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
log.info("Apple 로그인 응답: " + payload);
try {
return objectMapper.readValue(payload, targetClass);
} catch (Exception e) {
throw new RuntimeException("Error decoding token payload", e);
}
}
{
"iss": "https://appleid.apple.com",
"aud": "~~~",
"exp": 토큰 만료시간,
"iat": 토큰 생성시간,
"sub": "애플 사용자 식별값",
"at_hash": "~~",
"auth_time": ~~~,
"nonce_supported": true,
"email" : "사용자 이메일"
}
애플 로그인을 진행할 경우, 간혹가다가 사용자가 자기 정보 공유하는 걸 원치 않아 email값을 제공하는 걸 동의하지 않을 수도 있습니다! (email 가리기)
이렇게 되면, 애플 서버에서는 email을 빼고 응답하기 때문에, DB에 이메일을 저장해야 하는 서비스의 경우는 email 대신에 사용자를 식별할 수 있는 저장할 대체 값이 필요합니다!!
아래 코드처럼 저는 이러한 경우를 대비해서 , sub
값을 email를 대체해서 저장해 줬습니다!
@Override
public OAuthInfoResponse requestOauthInfo(String idToken) {
AppleInfoResponse appleInfoResponse = decodePayload(idToken, AppleInfoResponse.class);
// email과 nickname이 null 또는 빈 값일 경우, sub 값을 할당
if (appleInfoResponse.getEmail() == null || appleInfoResponse.getEmail().isEmpty()) {
appleInfoResponse.setEmail(appleInfoResponse.getSub());
}
return appleInfoResponse;
}
https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
https://gengminy.tistory.com/56#%F-%-F%--%-D%--UserService
유익한 내용입니다! 잘 읽고가요~