Spring boot

[소셜로그인] 2탄 Spring Security, JWT, OAuth 적용하기

서론

 

지난 글에서 Spring Security, JWT, OAuth 개념을 정리했다.

 

https://cme10575.tistory.com/165

 

[소셜로그인] 1탄 Spring Security, JWT, OAuth 개념

출시 전인 프로젝트에 참여하게 되었다. 전에 백엔드 개발자로 계시던 분이 나가시게 되면서 내가 들어왔다. 소셜로그인 기능이 구현된 상태였는데, 본인 계정으로 깃헙과 구글의 Oauth 프로젝트

cme10575.tistory.com

 

이번 글에서는 Spring Security, JWT, OAuth를 적용시키는 방법을 정리할 것이다.

 

앞서 글에서 정리했듯이 Spring Security는 여러개의 Filter 를 통해 '인증'과 '권한'을 처리한다.

 

Spring Security는 총 33개의 필터를 제공하는데 그 중  Security Authentication Filter(UsernamePasswordAuthentication, OAuth2LoginAuthenticationFilter와 같은 인증 필터)는 AuthenticationManager를 통해 인증을 수행한다.

먼저 가장 쉽게 접할 수 있는 기본 인증 필터인 UsernamePasswordAuthenticationFilter의 인증 과정에 대해 알아보자

 

 


UsernamePasswordAuthenticationFilter의 인증 과정

 

 

AuthenticationManager는 Spring Security의 필터들이 인증을 수행하는 방법에 대한 명세를 정의해 놓은 인터페이스이다.

 

 AuthenticationManager는 일반적으로 ProviderManager로 구현되며,  ProviderManager는 여러 AuthenticationProvider에 인증을 위임한다.

 

여러 AuthenticationProvider중 하나라도 인증에 성공한다면 ProviderManager에게 인증된 Authentication객체를 반환하고 이는 event 기반으로 AuthenticationFilter에 전송된다.

 

ProviderManager에 설정된 AuthenticationProvider중 어느 것도 성공적으로 인증을 수행할 수 없다면, 인증은 실패할 것이고 알맞는 예외가 ProviderManager에게 건내질 것이다.

 

인증이 성공할 경우 AuthenticationFilter는 SecuritycontextHolder의 SecurityContext에 인증된 Authentication 객체를 저장할 수 있도록 한다.

 


궁금한 점

 

여기까지는 다른 분들이 정리해둔 자료가 많아 이해하기 쉬웠다.

그러나 인증 과정 뿐이 아니라 로그인을 한 사용자와 하지 않은 사용자는 어디서 어떻게 나누는지에 대해 알고싶었다.

좀 더 큰 흐름을 봐야 OAuth와 JWT토큰을 사용하여 로그인 과정을 커스텀할 수 있을 것 같았다.

(사실 내가 참여한 프로젝트는 이미 전에 계시던 분이 커스텀을 마치고 가셨으나, 내가 이해할 수 없었다.. 또한 JWT 토큰을 사용할 경우 db에 접근하여 인증하는 것이 바람직하지 않다고 이해했는데 우리 코드는 db에 접근을 하는 중이어서 이 흐름에 대해 알아보고 수정해야겠다고 생각했다.)

 

내가 궁금했던 부분은 다음과 같다.

1. 전통적인 세션 사용 방식에서 로그인 이전 유저와 로그인된 유저는 어떤 필터를 통해 어떤 과정으로 요청이 처리되는 것인지?

2. OAuth+JWT에서 로그인하지 않은 사용자와 로그인된 유저의 요청(헤더에 토큰이 없음)은 어떤 필터를 통해서 어떻게 처리되는것인지?

 

검색을 해 본 결과 나름의 답을 얻을 수 있었다.

 

 


1. 전통적인 세션 방식의 요청 흐름

 

위 그림은 JWT 토큰을 사용하지 않고, 전통적인 세션 방식을 사용할 때의 흐름도이다.

요청이 들어오면 먼저 SecurityContextPersistanceFilter에서 SecurityContext의 존재 여부를 확인한다.

헤더에 session id를 확인하고 그에 해당하는 SecurityContext가 있다면 가져오고 없으면 새로 만들어준다.


그 후 UsernamePasswordAuthenticationFilter에서 위의 UsernamePasswordAuthenticationFilter의 인증 과정 대로 인증을 마치고 SecurityContext에 인증 객체를 저장한다. 인증에 성공하면 SessionManagementFilter에 따라 후속처리를 하고 LoginSuccessHandler에 따라 동작하게된다.

 

따라서 로그인을 하지 않은 유저라면 SecurityContextPersistanceFilter에서 해당하는 SecurityContext가 없으므로 새로 만들어주고 UsernamePasswordAuthenticationFilter에서 인증을 하게 되는 것이다.

 

로그인을 한 유저라면 SecurityContextPersistanceFilter에서 SecurityContext을 찾을 수 있으므로 인증을 건너뛰게 되는 것 같다. 

 

 


2. OAuth+JWT 방식의 요청 흐름

 

JWT를 사용하는 경우 SecurityConfig 파일에서 아래 설정을 해준다.

 

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

 

 

정확히 표현하자면 SessionCreationPolicy.STATELESS 설정으로 인해  세션 존재 여부를 떠나서 세션을 통한 인증 메커니즘 방식을 취하지 않습니다. 그렇기 때문에 인증에 성공한 이후라도 클라이언트가 다시 어떤 자원에 접근을 시도할 경우 SecurityContextPersistenceFilter 는 세션 존재 여부를 무시하고 항상 새로운 SecurityContext 객체를 생성하기 때문에 인증성공 당시 SecurityContext 에 저장했던 Authentication 객체를 더 이상 참조 할 수 없게 되어 버립니다.
그렇기 때문에 매번 인증을 받아야 하고 인증을 필요로 하는 상태가 됩니다.
즉 세션방식의 인증 처리가 되지 않는 것입니다. - https://www.inflearn.com/questions/34886

 

로그인을 마쳐도 Context가 저장되지 않기 때문에 SecurityContextPersistanceFilter에서는 매번 새로운 객체를 생성하고 새로 인증하게 된다.

 

먼저 로그인 안 된 유저의 요청을 알아보자.

SecurityConfig파일에서 아래와 같이 설정하면

 

http.oauth2Login()

SecurityContextPersistanceFilter에서 새로 요청된 인증은 기존의 UsernamePasswordAuthenticationFilter 대신 같은 부모 AbstractAuthenticationProcessingFilter를 상속한 OAuth2LoginAuthenticationFilter에서 실행된다.

 

자세한 과정은 여기를 확인하면 된다. 

OAuth로 소셜로그인을 성공하면(우리 프로젝트는 CustomUserService를 만들어서 db에 있는 유저인지도 확인했다) 아래 SuccessHandler가 호출된다. 만약 커스텀하고 싶다면 이 핸들러를 상속한 새로운 SuccessHandler를 만들어 SpringConfig파일에서 등록해주면 된다.

 

SavedRequestAwareAuthenticationSuccessHandler 

원래 기본 핸들러에서는 redirect 만 해주지만 우리 프로젝트에서는 jwt 토큰을 발급하여 헤더에 추가하고 redirect시켜줬다.

이렇게 로그인 플로우를 알아보았다.

 

 

이제 헤더에 JWT 토큰이 있는 경우, 즉 로그인을 마친 유저의 요청 흐름을 알아보자

 

세션을 사용하지 않도록 설정해두었으니 위와 마찬가지로 SecurityContextPersistanceFilter에서 새로운 객체를 생성하고 인증해야한다.

다만 UsernamePasswordAuthenticationFilter 이전에 토큰을 인증을 처리할 새로운 필터를 생성해서 넣어준다.

OncePerRequestFilter를 상속한 JwtAuthenticationFilter를 생성하고 doFilterInternal을 override해서 토큰을 검증하고

인증이 완료되었다면 ContextHolder에 저장해준다. 인증이 완료되었기 때문에 뒤의 로그인 과정이 일어나지 않게 되는 것 같다.

 

 Authentication authentication = jwtUtil.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);

 

 


출처

 

https://sungminhong.github.io/spring/security/

https://dotheright.tistory.com/355

https://velog.io/@shinmj1207/Spring-Spring-Security-JWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8

https://velog.io/@tmdgh0221/Spring-Security-%EC%99%80-OAuth-2.0-%EC%99%80-JWT-%EC%9D%98-%EC%BD%9C%EB%9D%BC%EB%B3%B4#oauth2-%EC%9D%B8%EC%A6%9D-%EA%B3%BC%EC%A0%95

 

잘못된 부분이 있다면 알려주세요~!

'Spring boot' 카테고리의 다른 글

ec2 인스턴스 ssh로 접근하기  (0) 2022.04.09
DB call을 줄여보자  (0) 2022.03.10
[소셜로그인] 1탄 Spring Security, JWT, OAuth 개념  (0) 2022.02.14
에러노트 UnsatisfiedDependencyException  (0) 2022.02.12
[MVC1] 서블릿  (0) 2021.12.09