JAVA/JAVA | Spring 학습기록

[Spring] FeignClient를 이용한 Apple OAuth 구현 일지 (1)

kth990303 2023. 4. 11. 22:21
반응형

해당 글에서는 spring boot에서의 FeignClient 선택 이유 및 연동 프로덕션 및 테스트 코드, Apple OAuth 이론에 대해 다룹니다.


사이드 프로젝트 `모카콩`의 Wiki에 작성한 글에 해당된다.


 

해당 프로젝트 github: https://github.com/mocacong/Mocacong-Backend

 

GitHub - mocacong/Mocacong-Backend: 모카콩 백엔드

모카콩 백엔드. Contribute to mocacong/Mocacong-Backend development by creating an account on GitHub.

github.com


들어가며

모카콩에는 iOS 앱 배포를 위해 Apple OAuth가 존재합니다. 사용자들이 Apple 계정만으로 편리하게 모카콩에 접속하여 사용할 수 있도록 하기 위해서입니다.

출처: apple developer 공식 홈페이지

 

Apple OAuth의 작동 원리를 요약하자면 다음과 같습니다.

  1. 사용자가 Apple 계정으로 로그인
  2. 사용자가 입력한 정보를 바탕으로 Apple ID servers 에게 Identity Token 발급 요청
  3. Identity Token 값을 바탕으로 사용자 식별
  4. 필요 시, authorization code와 identity token을 바탕으로 access token과 refresh token 발급 요청 가능

모카콩에서는 1~3번 과정만을 진행했습니다. 4번 과정을 진행하지 않은 이유는, 사용자 식별 후 모카콩 서버 자체의 jwt를 발급해주기 위해서입니다. 1번은 사용자, 2번은 프론트에서 맡게 되며, 백엔드에서는 identity token을 받아 3번 과정만 처리합니다.

 

더 자세한 Apple OAuth의 작동 원리는 아래 링크에서 확인할 수 있습니다.

https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple

 

Authenticating users with Sign in with Apple | Apple Developer Documentation

Securely authenticate users and create accounts for them in your app.

developer.apple.com


Identity Token 값을 바탕으로 사용자 식별 과정

identity token은 JWT 형식이며, 2023년 4월 기준으로 해당 토큰에는 아래 정보들이 포함됩니다.

 

  • iss : 토큰 발급자를 의미하며, 값은 `https://appleid.apple.com`
  • sub : 유저 관련 정보
  • aud : 토큰 수신자 (모카콩 client_id)
  • iat : 토큰 발급 시각
  • exp : 토큰 만료 시각
  • nonce: 인증 요청 시 생성되는 무작위 문자열 (CSRF 공격 방지)
  • email : 사용자 이메일 (사용자 동의 시에만 제공)
  • email_verified : 사용자 이메일 검증 여부

 

이 외에도 nonce_supported, is_private_email, real_user_status, transfer_sub 정보가 포함돼있습니다.

 

모카콩 서버에서는 해당 정보들이 포함된 identity token을 받습니다. 그런데 해당 정보들을 바로 얻을 수가 없습니다. 먼저 헤더에서 암호화 알고리즘에 해당되는 alg와 공개키 식별자에 해당되는 kid 값을 추출해야 합니다. 또한, 애플 서버와 통신하여 공개키 구성요소 정보들 또한 얻어야 합니다. alg, kid와 구성요소 정보들이 있엉 공개키를 얻을 수 있기 때문입니다.

 

통신은 https://appleid.apple.com/auth/keys 로 GET api를 요청하면 됩니다.

요구되는 권한이 별도로 없기 때문에 해당 링크를 클릭하는 것만으로도 아래 사진처럼 바로 json 값을 열람할 수 있습니다. 일정 시간을 주기로 json 값은 바뀐다는 점 참고해주세요.

공개키를 얻고 나서야 최종적으로 identity token 헤더뿐 아니라 자체를 파싱하여 Claims 정보를 얻어낼 수 있습니다. 마지막으로 애플에서 강조하는 Claims 검증 작업을 거친 후에, 해당 Claims에서 유저 정보를 추출할 수 있습니다.

Claims 검증 작업에는 아래 5가지가 필수적으로 요구됩니다.

 

To verify the identity token, your app server must:

  • 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

 

관련 내용은 아래 링크에서 확인할 수 있습니다.

https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user

 

Verifying a user | Apple Developer Documentation

Check the validity and integrity of a user’s identity token.

developer.apple.com

Apple OAuth가 확실히 다른 OAuth보다 비교적 구현하기 빡센 느낌이 없지 않아 있는 듯합니다 😂


서버와의 HTTP 통신을 위해 선택한 라이브러리

공개키를 얻기 위해서는 애플 서버와 통신해야 되기 때문에 HTTP 통신 라이브러리 선택 작업도 필요합니다.

처음에는 해당 라이브러리로 RestTemplate vs WebClient 중에 고민했습니다. 고민 끝에 모카콩에서 결정한 라이브러리는 무엇일까요?

 

정답은 둘 다 아닙니다!

아니, 그럼 무엇을 사용하는 것이냐? 라고 물으신다면 모카콩에서는 최근에 부상하고 있(다고 생각하)는 FeignClient를 도입해보았습니다.

 

FeignClient는 Netflix에서 HttpClient를 간단하게 사용하기 위해 개발한 기술입니다. WebClient보다 가볍고 간단하다는 장점이 있습니다. 인터페이스를 만들고 @FeignClient 어노테이션을 선언만 하면 HttpClient 구현체를 장착해줍니다. @GetMapping 어노테이션 등으로 요청 url을 적기만 하면 서버와의 HTTP 통신이 가능하여 코드가 굉장히 간단합니다. 아래에 서술할 애플리케이션 코드를 보면 FeignClient의 간편함을 느끼실 수 있을겁니다.

 

다만, 단점도 존재합니다. FeignClient는 WebClient와 다르게 비동기 논블라킹 방식으로 사용할 수 없습니다. 동기 방식만 지원이 되는데요. Apple OAuth의 identity token을 파싱하기 위해서는 반드시 Apple ID servers 통신을 통한 public key가 필요하기 때문에 비동기 방식으로 이용할 수 없습니다. 그렇기 때문에 Apple OAuth 코드 구현에는 애초에 비동기 논블라킹의 장점을 챙길 수 없습니다. 따라서 FeignClient를 안 쓸 이유가 없다고 판단했습니다.

 

(+23.09.06 추가)

openfeign 도 async, Non-Blocking 방식으로 통신이 가능하다.

 

출처: https://kwonnam.pe.kr/wiki/springframework/feign

 

springframework:feign [권남]

 

kwonnam.pe.kr

 

 

FeignClient에 대해 더 자세히 알고 싶으신 분들을 위해 FeignClient를 2019년(!!) 부터 도입한 우아한형제들 기술블로그 2개를 첨부하겠습니다.

https://techblog.woowahan.com/2630/

https://techblog.woowahan.com/2657/

 

참고로 RestTemplate이 이후에 deprecated 될 예정이라고 잘못 알고 있는 분들이 꽤 있습니다.

저 또한 그렇게 알고 있었는데요, 최근에 지인이 보여준 토비님의 영상을 보고 난 뒤에 생각이 바뀌었습니다.

https://www.youtube.com/watch?v=S4W3cJOuLrU&t=530s

해당 영상은 토비님께서 2023년 3월 경에 촬영하신 영상입니다.

Spring Docs에서 RestTemplate 사용을 지양하고 WebClient를 지향할 예정이라고 적힌 적이 있기는 하지만, 현재 해당 내용은 사라지고 Spring 6 버전에서도 여전히 RestTemplate을 사용하고 있다는 내용입니다. 또, deprecated 관련 내용도 지금은 스프링 공식 문서에서 슬쩍 빠졌다고 합니다. 그렇기 때문에 RestTemplate도 실제로 좋은 선택지가 될 수 있으리라 생각합니다.

WebClient가 꼭 좋다고도 할 수 없는 것이, spring-webflux 의존성은 지나치게 무겁다는 단점이 있어 비동기 논블라킹 방식이 꼭 필요하지 않다면 오히려 독이 될 수 있습니다.


FeignClient 연동 프로덕션 코드

애플리케이션 프로덕션 코드를 보기 전에 FeignClient와 연동하는 코드를 먼저 살펴봅시다.

 

identity token을 파싱하기 위해서는 공개키가 존재해야 합니다. 그리고 공개키를 만드려면 https://appleid.apple.com/auth/keys 로 GET api 요청을 보내 공개키 구성요소 정보들을 받아야 합니다. 위에서 언급했듯이, 모카콩에서는 타 서버와의 HTTP 통신 라이브러리로 FeignClient를 선택했습니다.

 

FeignClient를 사용하기 위한 의존성을 추가해줍시다.

Build.gradle

implementation platform("org.springframework.cloud:spring-cloud-dependencies:2021.0.5")
implementation "org.springframework.cloud:spring-cloud-starter-openfeign"

feignClient를 사용하기 위해 필요한 의존성으로는 spring-cloud-starter-openfeign입니다.

spring-cloud 의존성을 추가할 때에는 springCloudVersion과 Spring Boot 버전 충돌이 나지 않도록 주의해야 합니다. 아래 사진을 보고 자신에 맞는 환경의 spring-cloud-dependencies를 함께 추가해줍시다.

출처:  https://spring.io/projects/spring-cloud  에서 release train 참고

 

모카콩의 스프링 부트 버전은 2.7.x 에 해당됩니다. 따라서 springCloudVersion은 2021.0.x 버전을 추가해주면 됩니다.

2021.0.x 의 가장 최신 버전은 2021.0.5이기 때문에 implementation platform("org.springframework.cloud:spring-cloud-dependencies:2021.0.5") 을 추가해주었습니다.

 

FeignClient을 스프링 빈으로 등록하여 사용하기 위해 @Configuration 으로 싱글톤으로 등록해줍시다. 주의할 점은 basePackageClasses를 지정해주지 않으면 feignClient를 사용할 수 없습니다.

FeignClientConfig.class

1
2
3
4
5
@Configuration
@EnableFeignClients(basePackageClasses = ServerApplication.class)
public class FeignClientConfig {
 
cs

 

이제 FeignClient 을 이용할 준비 자체는 끝났습니다!

yml에는 별도 설정을 할 필요가 없습니다만, 필요에 따라서 timeout 설정, 에러 상황 대처 지정 등을 추가로 yml로 설정할 수 있습니다. 이 포스팅에서는 해당 내용은 다루지 않겠습니다.

 

준비는 끝났으니 https://appleid.apple.com/auth/keys 와 통신하는 client의 코드를 한번 살펴보겠습니다.

AppleClient.class

1
2
3
4
5
6
7
@FeignClient(name = "apple-public-key-client", url = "https://appleid.apple.com/auth")
public interface AppleClient {
 
    @GetMapping("/keys")
    ApplePublicKeys getApplePublicKeys();
}
 
cs

네, 이게 끝입니다!

 

RestTemplate의 어마어마했던 코드량, 그리고 그나마 체이닝 기법으로 간편하게 코드를 작성할 수 있었던 WebClient보다도 훨씬 짧고 간단한 코드를 보고 계십니다. FeignClient은 간단한 Http 통신용 Client인데다가 구현체가 자동으로 장착된다는 점 덕분에 매우 간단하게 코드를 작성할 수 있습니다.

 

참고로 ApplePublicKeys 코드는 아래와 같습니다.

ApplePublicKeys.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class ApplePublicKeys {
 
    private List<ApplePublicKey> keys;
 
    public ApplePublicKey getMatchesKey(String alg, String kid) {
        return this.keys
                .stream()
                .filter(k -> k.getAlg().equals(alg) && k.getKid().equals(kid))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Apple JWT 값의 alg, kid 정보가 올바르지 않습니다."));
    }
}
cs

일종의 DTO 클래스이므로 필드명을 keys가 아닌 다른 값으로 하면 응답을 받아오지 못하고 null을 받아온다는 점 주의해주세요!

위 사진처럼 kty, kid 등의 공개키 구성요소 정보들을 객체로 가지며, 이를 keys 배열에 포함돼야 합니다.

ApplePublicKey.class

1
2
3
4
5
6
7
8
9
10
11
12
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class ApplePublicKey {
 
    private String kty;
    private String kid;
    private String use;
    private String alg;
    private String n;
    private String e;
}
cs

이렇게 해서 Apple 공개키를 구성할 정보들도 받아왔습니다.


FeignClient를 이용하여 Apple ID servers와 통신하는 AppleClient의 테스트 코드는 아래와 같습니다.

 

AppleClientTest.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootTest
class AppleClientTest {
 
    @Autowired
    private AppleClient appleClient;
 
    @Test
    @DisplayName("apple 서버와 통신하여 Apple public keys 응답을 받는다")
    void getPublicKeys() {
        ApplePublicKeys applePublicKeys = appleClient.getApplePublicKeys();
        List<ApplePublicKey> keys = applePublicKeys.getKeys();
 
        boolean isRequestedKeysNonNull = keys.stream()
                .allMatch(this::isAllNotNull);
        assertThat(isRequestedKeysNonNull).isTrue();
    }
 
    private boolean isAllNotNull(ApplePublicKey applePublicKey) {
        return Objects.nonNull(applePublicKey.getKty()) && Objects.nonNull(applePublicKey.getKid()) &&
                Objects.nonNull(applePublicKey.getUse()) && Objects.nonNull(applePublicKey.getAlg()) &&
                Objects.nonNull(applePublicKey.getN()) && Objects.nonNull(applePublicKey.getE());
    }
}
 
cs

외부 서버와 통신하는 과정이기 때문에 mock 객체를 이용할까 고민했습니다. 하지만 실제로 모카콩 서버나 apple 서버에 POST 요청을 보내 별도로 update가 돼서 무언가가 바뀌는 작업이 아닌, 단순 조회 요청일 뿐이기 때문에 실제 AppleClient를 주입받아 테스트하는 방식을 선택했습니다. 테스트가 조금은 느릴 수 있지만, 실제로 통신이 올바르게 되는지 검증해주는 역할이 중요하다고 생각했기 때문입니다.

configuration 관련 빈이나 AppleClient 빈을 주입받아야 되므로 @SpringBootTest 를 사용해주었습니다.


마치며

원래는 AppleOAuth 애플리케이션 전체 프로덕션 코드를 다루려고 했지만, 생각보다 양이 어마어마하더군요. 따라서 1, 2편으로 나누어서 포스팅하려 합니다. 1편도 짧지는 않다고 생각하는데 2편이 1편보다 더 길 듯합니다. (Apple OAuth의 힙스터 감성으로 인한 까다로운 코드 구현량…😭)

 

2편은 여기서 볼 수 있습니다.

https://kth990303.tistory.com/437

 

[Spring] FeignClient를 이용한 Apple OAuth 구현 일지 (2)

해당 글에서는 Apple OAuth 연동 프로덕션 코드와 테스트 코드에 대해 다룹니다. 모카콩에서는 iOS 앱 배포를 위해 Apple OAuth를 구현했습니다. Apple OAuth 이론, FeignClient 설정과 선정 이유, 관련 프로덕

kth990303.tistory.com

 

반응형