OAuth 2.0 Resource Server에서 JWT 토큰을 검증하고 권한을 처리하는 방법에 대해 알아보겠습니다. Spring Security를 사용하여 Resource Server를 구성하고, JWT 토큰의 scope와 roles 클레임을 권한으로 변환하는 과정을 살펴볼 것입니다. 또한, 자주 발생하는 SSL 관련 오류와 그 해결 방법에 대해서도 다룰 예정입니다.
Authorization_code vs Client_credentials
OAuth 2.0의 Authorization Code 방식과 Client Credentials 방식은 서로 다른 사용 사례와 흐름을 가지고 있습니다. 두 방식의 주요 차이점은 다음과 같습니다:
Authorization Code 방식:
사용자 개입이 필요한 인증 흐름
웹 애플리케이션이나 모바일 앱에서 주로 사용
리소스 소유자(사용자)의 동의를 얻어 액세스 토큰을 발급
보안성이 높고 refresh token을 사용할 수 있음
Client Credentials 방식:
클라이언트 애플리케이션이 직접 자신의 자격 증명으로 인증
서버 간 통신이나 백그라운드 작업에 주로 사용
사용자 컨텍스트 없이 클라이언트 자체의 권한으로 액세스 토큰을 얻음
간단하지만 사용자 특정 데이터에 접근할 수 없음
OAuth2 Authorization Server Setup
Spring Boot와 Spring Security를 사용하여 OAuth 2.0 Authorization Server를 구현하기 위해서는 다음과 같은 핵심 설정이 필요합니다:
=> 이부분은 사실 원격요청을 하지 않고 제공되는 공개키, pem을 통해서 로컬에서 검증이 가능하지만 현재 java로는 직접 구현해야 하는 부분이 많고, 자주는 아니지만 공개키는 변경될 수 있기 때문에 키값을 확인하기 위해서는 결국 원격요청이 필요해서 쉽게 가기로 했다.
-- "cognito:groups" 정보를 이용해서 권한을 세팅한다.
B. 기존 토큰이라면
- secretKey를 이용해서 검증하고
- "roles" 정보를 이용해서 권한을 세팅한다.
<정리>
이렇게 만들면 기존의 JWT Token(HS256) 와 신규 Cognito Token (RSA256) 를 모두 소화할 수 있다.
그러나 만들어 놓고보니 소스가 지저분하다. ResourceServer에서 권한을 확인하여 API접근제어를 해야하는데 좋은 구조는 아니다.
- 로직이 복잡하고(구조 특성상 해당모듈을 그대로 copy해가는 방식이 될텐데 유지보수성이...영), secretKey 공유의 문제도 있다.
- 꼭 두 가지 토큰을 소화할 필요가 있을까?
- 기존 방식은 외부 사용자가 고려되지 않은 방식이고, OAuth 2.0은 출발자체가 3rd Party연계를 간편하게 하기 위한 방법이다.
다양한 확장을 위해서 인증은 각각의 방식을 유지하고 서비스를 사용자에 따라서 분리하는 것이 해결책이라는 판단을 내렸다.
- 내부 사용자가 이용하며 Client, Resource Server가 같은 Boundary에 있는 레거시의 경우 기존 토큰을 사용하고
- 외부 사용자가 이용해야 하는 API 서비스는 별도로 분리 구성하며, 이 서비스에서는 OAuth 2.0만을 활용하여 인가/인증을 하도록 한다.
(이부분은 AWS API Gateway내에서 자격증명으로 Cognito를 연계하는 방식과 시너지를 낼 수 있을 것 같다.)
- 이를 위해서 보다 쉽게 Resource Server를 세팅하는 법을 Spring Security에서 제공하고 있는데 다음글에서 좀 더 자세히 적용해보고 서비스를 어떻게 분리하는 것이 보다 효과적일지 살펴본다.
- Spring Security 의 경우 Filter Chain 의 형태로 기능을 제공하고 있으며 필요에 따라서 추가/수정/삭제가 가능하다.
<내용>
1. Spring Security - Filter Chain
- Spring 에서 모든 호출은 DispatcherServlet을 통과하게 되고 이후에 각 요청을 담당하는 Controller 로 분배된다.
- 이 때 각 Request 에 대해서 공통적으로 처리해야할 필요가 있을 때 DispatcherServlet 이전에 단계가 필요하며 이것이 Filter이다.
- Spring Security는 FilterChainProxy를 통해서 상세로직을 구현하고 있다.
- 현재 Application에서 사용중인 Filter Chain은 Debug를 통해서 쉽게 확인할 수 있다.
- FilterChain이름이 의미하듯 Filter는 순서가 매우 중요하다.
2. 우리가 가장 많이 사용하는 UserNamePassword 구조에 대해서 좀 더 살펴보겠다.
(적당한 그림을 찾지 못해서 손으로 그려보았다.)
- Request가 들어왔을때 Filter를 거치게 되고, 적절한 AuthenticationToken이 존재하지 않는다면 AuthenticationProviders를 거쳐서 생성하게 되며 UserDeatilsService를 입맛에 맞게 구현하여 사용자 정보를 가져오는 부분을 구현할 수 있다.
- Spring Securty에서는 사람들이 가장 많이 사용하는 DB인증을 다음과 같이 미리 구현해 두었다. (오른쪽 파란색 박스)
3. 인증방식 변경 ( JWT Token)
최근에는 MSA구조의 효율성을 높이기 위해서 JWT Token 방식을 많이 사용하고 있다. Request에 대한 인증을 별도 서버를 거치지 않고 검증가능하고 기본로직에 필요한 내용을 담을 수 있어서 편리하다.
현재 서비스에 적용중인 대략적인 구조이다.
- JWT AuthTokenFilter에서 해당 처리를 마친 후 나머지 FilterChain을 수행하는 구조이다.
- Token을 통해서 인증 및 인가를 위한 정보를 생성하여 SecurityContextHolder를 통해서 세팅한다.
- JWT Filter의 내용을 간단히 살펴보면 아래와 같다. (보안상 TokenProvider 로직은 개별구현하는 것을 권장한다.)