Apple Login]with Spring Security,JWT,REST API,Spring Boot 3

손지민·2023년 12월 19일

Spring Security

목록 보기
9/11

개요

앞 단계에서 Spring Security, JWT 를 REST API로 로그인 기능을 구현하였다. 앞으로 Apple Login을 구현해보고자한다.

iOS와의 협업을 위해 Spring Boot 3.2.0으로 REST API로 프로젝트 진행중입니다.
공부 중이므로 틀린 내용, 의견, 질문 있으시면 댓글 달아주시면 감사하겠습니다.

Apple Login

1.1. 흐름

  1. 소셜 로그인 요청:

    • 클라이언트 애플리케이션에서 애플 소셜 로그인을 요청합니다.
  2. 애플 로그인 화면 표시:

    • 사용자는 애플 아이디와 비밀번호로 로그인하거나, 애플리케이션에서 제공하는 다른 로그인 방식을 선택합니다.
  3. 사용자 권한 부여:

    • 사용자는 로그인 시에 해당 애플리케이션이 요청하는 권한에 대해 동의합니다.
  4. 애플 서버로부터 인증 정보 수신:

    • 사용자가 로그인에 성공하면, 클라이언트 애플리케이션은 애플 서버로부터 사용자의 인증 정보(예: 인증 코드)를 받습니다.
  5. 클라이언트에서 서버로 인증 정보 전송:

    • 클라이언트 애플리케이션은 서버로 사용자의 인증 정보(예: 애플 인증 코드)를 전송합니다.
  6. 서버에서 토큰 교환 요청:

    • 서버에서는 받은 인증 정보(애플 인증 코드 등)를 사용하여 애플 서버에 토큰 교환 요청을 보냅니다.
  7. 서버에서 토큰 검증 및 사용자 확인:

    • 서버에서는 받은 토큰을 애플 서버에서 검증하고, 해당 사용자가 서버에 이미 등록되어 있는지 확인합니다.
  8. 사용자 등록 또는 확인:

    • 등록되어 있지 않은 경우, 서버에서는 해당 사용자의 정보를 새로 등록합니다.
    • 이미 등록된 경우, 해당 사용자의 정보를 가져옵니다.
  9. 서버에서 사용자에게 JWT 토큰 발급:

    • 서버는 해당 사용자에 대한 정보를 기반으로 JWT 토큰을 생성합니다.
    • 이 JWT 토큰을 클라이언트에게 반환하고, 클라이언트는 이 토큰을 사용하여 이후 요청에 대한 인증을 수행합니다.
  10. 클라이언트에서 JWT 토큰을 이용한 자체 로그인 처리:

    • 받은 JWT 토큰을 안전한 저장소에 저장합니다.
    • 이 후의 모든 요청에서는 Authorization 헤더에 해당 토큰을 포함하여 서버에 요청을 보냅니다.
    • 서버에서는 이 토큰을 검증하고, 해당 사용자의 권한을 확인하여 요청을 처리합니다.

블로그1 정리-1(2020.08.30)

애플 개발자 계정에서 할 일

  1. Server to Server Notification Endpoint
    • 애플 계정 변동 발생 시 안내 - 입력한 Endpoint 로 유저 정보, 이벤트 등 데이터 전송된다.
  2. TeamID: 클라이언트 시크릿 생성 시 필요
  3. Domain : wardservice.shop/www.wardservice.shop
    Return URLs : SSL적용된 URL만 됩니다.
    • 등록한 Return URLs 로 애플 로그인 진행한 유저 정보가 전달된다.
      • JSON 형식 데이터 반환
    • 여러 개 입력 가능하다.
    • ex. wardservice.shop/redirect
  4. Service ID 등록이 완료 된 후 자신의 Services ID 의 "Identifier"를 기억하고 있어야합니다. Services ID의 실별자는 client_id, aud 의 값으로 사용됩니다.
  5. Key ID와 파일은 클라이언트 시크릿(client secret)을 생성할 때 필요한 데이터입니다.

정리

  • 클라이언트 시크릿client_secret을 생성할 때 사용될 Team ID를 기억해주세요.
  • endpoint URL에 등록한 URL로 PAYLOAD라는 키로 JWT 데이터를 전달받을 수 있습니다.
  • client_id와 aud로 사용될 Identifier를 기억해주세요.
    • (Return URLs에 등록된 URL로 유저 정보에 대한 JSON 데이터를 전달받을 수 있습니다.)
  • Key - 클라이언트 시크릿을 생성할 때 필요한 Private Key를 생성하기 위한 설정입니다.
    • Key ID와 .p8 파일은 기억해주세요.

블로그1 정리-2(2020.08.31)

적용

1. 유저 로그인 후 정보 받기

  • Mapping("/redirect")
  • 로그인하면 "https://appleid.apple.com/auth/authorize" 호출되고
  • ServicesID에 정의된 ReturnURLs 로 JSON 데이터가 반환됩니다.
  • 반환받은 JSON 데이터는 "state, code, id_token, user" 4개의 키로 이루어져 있습니다.
  • 여기서 알고 있어야 할 부분은 user 키는 유저가 서비스 최초 가입할 때만 받을 수 있습니다.
  • 또한, 유저는 자신의 email을 공유할 수도 있고, 하지 않을 수도 있습니다.
  • (JSON 데이터는 유저가 email을 공유하지 않은 데이터이며, "code" 키의 값은 5분 동안 유효합니다.)

2. id_token 5가지 유효성 검증

expid_token 만료시간(10분)
isshttps://appleid.apple.com
audServices ID - Identifier 값
nonce생성된 임의 값
RSAApple에서 제공받은 Public Key

3. client_secret 생성

JWT 로 생성 된다.
필요한 값 : kid,alg,iss,iat,exp,aud,sub

kid애플에서 생성한 Private Key에 대한 Key IDES256
algES256
issApp ID 생성에 사용된 Team ID
iat생성 시간
exp만료 시간
audhttps://appleid.apple.com
subServices ID - Identifier 값

4. 토큰 검증 및 발급

[1]에서 전달받은 code와 [3]에서 생성한 client_secret의 값 그리고 "client_id, grant_type, redirect_uri" 값으로 "[POST]https://appleid.apple.com/auth/token" 을 호출하여 권한 부여를 위한 토큰 검증을 진행하도록 합니다. ("code"는 5분간 유효한 값이므로 주의하도록 한다.)

client_idServices ID - Identifier 값
client_secreteyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJjb20ud2hpdGVwYWVrLnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6XC9cL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiODNNNlk1QllLVCIsImV4cCI6MTU5ODgwNTU2NSwiaWF0IjoxNTk4ODAxOTY1fQ.2HO_p7883orlgHS4GQ893haS8SLbRBGLhxNSCZl2i1bwc8uTZSEn4gCQcmvwCqs6lN7zRiUGE5iLQvqNlkJNPQ
codec3944a20072b7446b97633646556204f8.0.rruy.Gjgud84EqqpCvP31MrudDw
grant_typeauthorization_code
redirect_uriServices ID - Return URLs 값
  • "[POST] https://appleid.apple.com/auth/token" 호출이 정상적으로 완료되면 JSON 데이터를 반환받습니다.
  • 반환받은 JSON 데이터에서 "id_token"을 decode 하여 필요한 유저 정보를 얻을 수 있습니다.

5. refresh_token 검증 및 토근 재발급

[5]에서 전달받은 "refresh_token"에 대한 유효성 검증을 하고 싶다면 "client_id, client_secret, grant_type, refresh_token"의 값으로 "[POST] https://appleid.apple.com/auth/token" 호출하여 검증을 진행합니다.

client_idServices ID - Identifier 값
client_secreteyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJjb20ud2hpdGVwYWVrLnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6XC9cL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiODNNNlk1QllLVCIsImV4cCI6MTU5ODgwNTU2NSwiaWF0IjoxNTk4ODAxOTY1fQ.2HO_p7883orlgHS4GQ893haS8SLbRBGLhxNSCZl2i1bwc8uTZSEn4gCQcmvwCqs6lN7zRiUGE5iLQvqNlkJNPQ
grant_typerefresh_token
refresh_tokenr8e88bc9f62bc496398b71117610c5aeb.0.mruy.UuuL5tpwnWaof86XPErqJg
  • "refresh_token"에 대한 "[POST] https://appleid.apple.com/auth/token" 호출이 정상적으로 완료되면 JSON 데이터를 반환받습니다.
  • 반환받은 JSON 데이터에서 "id_token"을 decode 하여 필요한 유저 정보를 얻을 수 있습니다.

[[추가 내용(1) - 이메일 변경, 서비스 해지, 애플 계정 탈퇴 이벤트가 발생한 경우]]

  • 유저의 애플 계정에 대한 이벤트가 발생하면 body안에 payload 키로 jwt 형태의 데이터가 담겨서 "App ID에 등록된 Endpoint URL"로 전송됩니다.
  • payload의 값은 jwt이므로 decode 하면 HEADER와 PAYLOAD 데이터 영역으로 나뉩니다.

블로그2

AppleOAuthLoginController

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()));
    }
}

LoginService

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;
    }
}

AppleOAuthUtil

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);
    }
}

DTO 및 모델 클래스

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
}

참고

블로그1
블로그2

profile
Developer

0개의 댓글