앞 단계에서 Spring Security, JWT 를 REST API로 로그인 기능을 구현하였다. 앞으로 Apple Login을 구현해보고자한다.
iOS와의 협업을 위해 Spring Boot 3.2.0으로 REST API로 프로젝트 진행중입니다.
공부 중이므로 틀린 내용, 의견, 질문 있으시면 댓글 달아주시면 감사하겠습니다.
소셜 로그인 요청:
애플 로그인 화면 표시:
사용자 권한 부여:
애플 서버로부터 인증 정보 수신:
클라이언트에서 서버로 인증 정보 전송:
서버에서 토큰 교환 요청:
서버에서 토큰 검증 및 사용자 확인:
사용자 등록 또는 확인:
서버에서 사용자에게 JWT 토큰 발급:
클라이언트에서 JWT 토큰을 이용한 자체 로그인 처리:

| 키 | 값 |
|---|---|
| exp | id_token 만료시간(10분) |
| iss | https://appleid.apple.com |
| aud | Services ID - Identifier 값 |
| nonce | 생성된 임의 값 |
| RSA | Apple에서 제공받은 Public Key |
JWT 로 생성 된다.
필요한 값 : kid,alg,iss,iat,exp,aud,sub
| 키 | 값 |
|---|---|
| kid | 애플에서 생성한 Private Key에 대한 Key IDES256 |
| alg | ES256 |
| iss | App ID 생성에 사용된 Team ID |
| iat | 생성 시간 |
| exp | 만료 시간 |
| aud | https://appleid.apple.com |
| sub | Services ID - Identifier 값 |
[1]에서 전달받은 code와 [3]에서 생성한 client_secret의 값 그리고 "client_id, grant_type, redirect_uri" 값으로 "[POST]https://appleid.apple.com/auth/token" 을 호출하여 권한 부여를 위한 토큰 검증을 진행하도록 합니다. ("code"는 5분간 유효한 값이므로 주의하도록 한다.)
| 키 | 값 |
|---|---|
| client_id | Services ID - Identifier 값 |
| client_secret | eyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJjb20ud2hpdGVwYWVrLnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6XC9cL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiODNNNlk1QllLVCIsImV4cCI6MTU5ODgwNTU2NSwiaWF0IjoxNTk4ODAxOTY1fQ.2HO_p7883orlgHS4GQ893haS8SLbRBGLhxNSCZl2i1bwc8uTZSEn4gCQcmvwCqs6lN7zRiUGE5iLQvqNlkJNPQ |
| code | c3944a20072b7446b97633646556204f8.0.rruy.Gjgud84EqqpCvP31MrudDw |
| grant_type | authorization_code |
| redirect_uri | Services ID - Return URLs 값 |
[5]에서 전달받은 "refresh_token"에 대한 유효성 검증을 하고 싶다면 "client_id, client_secret, grant_type, refresh_token"의 값으로 "[POST] https://appleid.apple.com/auth/token" 호출하여 검증을 진행합니다.
| 키 | 값 |
|---|---|
| client_id | Services ID - Identifier 값 |
| client_secret | eyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJjb20ud2hpdGVwYWVrLnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6XC9cL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiODNNNlk1QllLVCIsImV4cCI6MTU5ODgwNTU2NSwiaWF0IjoxNTk4ODAxOTY1fQ.2HO_p7883orlgHS4GQ893haS8SLbRBGLhxNSCZl2i1bwc8uTZSEn4gCQcmvwCqs6lN7zRiUGE5iLQvqNlkJNPQ |
| grant_type | refresh_token |
| refresh_token | r8e88bc9f62bc496398b71117610c5aeb.0.mruy.UuuL5tpwnWaof86XPErqJg |
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AppleOAuthLoginController {
private final LoginService loginService;
@Autowired
public AppleOAuthLoginController(LoginService loginService) {
this.loginService = loginService;
}
@PostMapping("/login/oauth/apple")
public ResponseEntity<Object> appleOAuthLogin(@RequestBody AccessTokenRequest request) {
String accessToken = request.getAccessToken();
LoginResult result = loginService.appleOAuthLogin(accessToken);
return ResponseEntity.ok()
.header("token", result.getJwt())
.body(new AppleOAuthResponse(result.getJwt(), result.getUser().getId(), result.getRememberMeToken()));
}
}
package com.ward.ward_server.api.user.appleoauthlogin;
import org.springframework.stereotype.Service;
@Service
public class LoginService {
public LoginResult appleOAuthLogin(String accessToken) {
OAuthInfo userInfo = resolveUserInfoFromApple(accessToken);
User user = getOrCreateUser(userInfo.getEmail(), "apple", userInfo.getOauthId());
return new LoginResult(user, generateRememberMeToken(), generateJWTToken());
}
private OAuthInfo resolveUserInfoFromApple(String accessToken) {
return AppleOAuthUtil.getAppleOAuthInfo(accessToken);
}
private User getOrCreateUser(String email, String oauthProvider, String oauthId) {
User user = userRepository.findByOauthProviderAndOauthId(oauthProvider, oauthId);
if (user == null) {
user = new User();
user.setEmail(email);
user.setOauthProvider(oauthProvider);
user.setOauthId(oauthId);
user.setRememberMeToken(generateUUID());
userRepository.save(user);
}
return user;
}
}
package com.ward.ward_server.api.user.appleoauthlogin;
import org.springframework.stereotype.Component;
@Component
public class AppleOAuthUtil {
public static OAuthInfo getAppleOAuthInfo(String accessToken) {
AppleAuthorizationTokenResponse info = AppleOAuthUtil.getAuthorizationToken(accessToken);
String idToken = info.getIdToken();
// idToken을 디코딩하고 필요한 정보를 추출하는 작업은 여기서 수행해야 합니다.
// 예시: String email = extractEmailFromIdToken(idToken);
return new OAuthInfo(email, sub);
}
private static AppleAuthorizationTokenResponse getAuthorizationToken(String accessToken) {
// HTTP 요청을 사용하여 Apple OAuth 정보를 가져오는 작업은 여기서 수행해야 합니다.
// 예시: ResponseEntity<AppleAuthorizationTokenResponse> response = restTemplate.postForEntity(appleOAuthUrl, accessToken, AppleAuthorizationTokenResponse.class);
}
}
public class AccessTokenRequest {
private String accessToken;
// getters, setters, constructors
}
public class AppleOAuthResponse {
private String jwt;
private String userId;
private String rememberMeToken;
// getters, setters, constructors
}
public class OAuthInfo {
private String email;
private String oauthId;
// getters, setters, constructors
}
public class LoginResult {
private User user;
private String rememberMeToken;
private String jwt;
// getters, setters, constructors
}