이 게시글은 당시에 고민했던 내용으로, securityFilterChain 을 파헤치기 아주 좋은 공부였습니다. 현재는 인증 모듈을 구현 완료하여, https://github.com/InhaBas/Inhabas.com-api/wiki/Auth-module-document 에 자세히 작성하여 놓았습니다.
아래의 사진과 같이 OAuth2 인증을 통해 로그인/회원가입이 가능하도록 되어있다.
이를 spring 에서 구현해야하기 때문에, spring security 및 oauth2 인증 관련하여 공부했다.
[spring security 구조]
아래 깃헙 이슈에서 자세히 기술해놓았다.
security 에 대해 대략적으로 공부한 뒤에, 아래와 같은 질문이 생겼다.
<주요한 기술적 문제>
- Member 엔티티로 OAuth2 인증 기반 SecurityContext 를 사용할 수 있나??
- login 한 유저를 controller로 받을 수 있나??
- 비지니스 로직에서 권한 검사 로직을 분리할 수 있나?
일단 1번 질문부터 해결해야 다음 질문을 해결할 가치가 있어보인다.
2, 3번은 권한로직 구현 이후에 비지니스 로직으로 넘어갈 때 고민해도 될 것 같다.
1번 질문을 해결하기 위해, 아래의 두가지 필요성을 느꼈다.
(1) request 와 response 의 흐름을 파악
(2) spring security 에서 제공하는 Oauth2 관련 필터들을 파악
(아래 나오는 내용들은 깃헙 이슈에서 보는 것이 더 가독성이 좋습니다.)
1. request & response 의 흐름 파악하기
- spring 이 시작되면, ServletContext 와 RootApplicationContext 가 load 되고, bean들을 관리한다..
- spring security 에 의해 설정된 SecurityFilterChain 이 DelegateFilterProxy와 FilterChainProxy 에 의해 관리된다.
- request 가 들어오면 각 필터의 doFilter 메서드 호출에 따라 지정된 작업을 수행한다. 각 필터들은 request를 변경할 수 있다.
- 반대로 response 가 나갈때도 마찬가지.
2. SecurityFilterChain 분석하기
- 이 때 인증에 필요한 부분은 SecurityFilterChain 을 거치면서 수행된다.
- 핵심 필터는 SecurityContextPersistenceFilter, AbstractAuthenticationProcessingFilter 두 개.
- 첫번째로 SecurityContextPersistenceFilter는 인증된 결과를 저장할 SecurityContext 를 만든다.
- 두번째로 AbstractAuthenticationProcessingFilter는 인증된 결과인 Authenticate 인스턴스를 SecurityContext 에 저장한다. (소셜로그인 정보가 담겨있다.)
(1) SecurityContextPersistenceFilter
- SecurityContext 는 인증결과를 단 하나 갖는다.
- SecurityContextHolder 가 SecurityContextStrategy를 결정하고(ThreadLocal, InheritenceThreadLocal, Global 셋 중 하나), SecurityContextStrategy 구현체가 SecurityContext를 갖는다.
- SecurityContextStrategy는 기본적으로 ThreadLocal 로 지정되어 있다.
- 스프링의 모든 인증과정은 SecurityContext가 이미 생성되었다고 가정하기 때문에, 인증 시작 전에 이 필터를 꼭 실행해야한다.
- SecurityContextRepository 는 request에 저장되어 있는 HttpSession 으로부터 SecurityContext 을 로드하거나, 비어있는SecurityContext 을 생성한다. 또 request가 종료될 때 saveContext() 를 호출하여, HttpSession 에 SecurityContext를 저장한다.
- HttpSession 을 생성할 일이 없으면 allowSessionCreation 을 false 로 지정해야한다. 서버 메모리를 아껴야하는 상황이거나, web 요청 간 모든 security class 가 context 에 persist 되지 않을 때 사용한다.
jwt 토큰 기반 인증 인가를 할 예정이어서, 세션을 아예 사용안 할 예정인데, SecurityContextRepository 의 HttpSession 저장 기능을 사용해야 하는지 궁금. 인증완료된 authentication 정보를 세션에 저장해서 사용하는 모양인데, 그러면 requests 간 인증을 최소화 할 수 있다는 장점이 있다. 하지만 그러면 jwt 토큰을 사용하는 이유가 없지.. 모바일과의 확장성을 고려해서 jwt 를 사용하고자 하는건데, 모바일 개발을 해보지 않아서 이 부분은 자세히 모르겠다. 인증 최소화는 향후 jwt 필터를 구현하면서 적용할 수 있을 듯 하다.
(2) AbstractAuthenticationProcessingFilter
- 이 필터에서 직접 구현해야하는 요소가 많다.
- 크게 보면 아래 3단계로 이루어져 있다.
- AbstractAuthenticationProcessingFilter 에서 attemptAuthentication() 을 호출하여 인증을 시도한다.
(실제 인증은 OAuth2LoginAuthenticationFilter 에게 위임한다.) - 인증 결과에 따른 핸들러를 호출한다.
성공 시에는 핸들러를 호출하기 전에, 새로운 SecurityContext를 생성하고, 인증결과를 컨텍스트에 저장한다. (Authentication 의 구현체인 OAuth2AuthenticationToken) - 소셜 로그인이 성공한 뒤에는, 우리 서비스의 기존 회원인지 확인한다. 신규회원이면 회원가입 시킨후에 로그인 진행하고, jwt 토큰을 발행한다.
- AbstractAuthenticationProcessingFilter 에서 attemptAuthentication() 을 호출하여 인증을 시도한다.
- 가장 복잡한 부분은 OAuth2LoginAuthenticationFilter 의 로직이다.
1. AbstractAuthenticationProcessingFilter 에서 전해 받은 request에서 accessToken 요청에 필요한 정보를 추출한다. 추출한 정보로 OAuth2LoginAuthenticationToken 을 만들고 ProviderManager 에게 전달한다.
2. ProviderManager 는 적절한 Provider 를 고르는데, 우리 서비스는 AuthorizationCode 방식으로 accessToken 을 요청할 것이기 때문에 설정에 의해 OAuth2LoginAuthenticationProvider 가 선택된다. ProviderManager 는 선택한 provider 에게 OAuth2LoginAuthenticationToken을 전달하고 인증을 위임한다.
3. OAuth2LoginAuthenticationProvider 는 전달받은 OAuth2LoginAuthenticationToken를 실제 accessToken 을 요청하기 위한 OAuth2AuthorizationCodeAuthenticationToken 으로 변환한다.
4. 실제 accessToken 을 요청하는 작업을 OAuth2AuthorizationCodeAuthenticationProvider에게 위임한다.
5. OAuth2AuthorizationCodeAuthenticationProvider 가 accessToken을 반환한다. 여기까지 했으면 사실상 OAuth2 인증을 성공한 셈이다. OAuth2Provider(e.g. google, kakao, naver, etc.) 에게 accessToken 을 얻어왔기 때문이다.
6-7. OAuth2UserService 의 loadUser() 를 호출해서 OAuth2User 를 가져온다. loadUser() 함수 안에서 OAuth2User 의 권한을 설정한다. 따라서 OAuth2UserService 상속받아 커스텀한 서비스를 만들어 기존 유저테이블의 권한과 연결하는 로직이 필요하다.
8-10. accessToken 과 소셜로그인 정보들을 취합해 OAuth2LoginAuthenticationToken을 만들어 반환한다.
11. SecurityContext에 저장할 OAuth2AuthenticationToken을 만든다. 민감한 정보인 accessToken 과 같은 데이터는 빠지고, OAuth2User 와 Provider 정보만 남는다.
12. accessToken과 같은 민감한 정보는 따로 사용할 수 있도록(재인증 및 provider api 요청 등등) AuthorizedClient의 인스턴스로 가공하고, AuthorizedClientRepository 가 저장한다. (여기서 좀 문제가 있다.)
13. OAuth2LoginAuthenticationFilter는 민감한 정보는 제외한 최종 인증된 결과물인 OAuth2AuthenticationToken을 반환한다.
위의 12번이 문제가 될 수 있는 상황
- AuthenticatedPrincipalOAuth2AuthorizedClientRepository 는 accessToken 과 클라이언트를 매핑한 AuthorizedClient를 저장,삭제,불러오기 등을 한다. 이 일을 OAuth2AuthorizedClientService 에게 위임한다.
세 가지의 인터페이스 메소드가 존재한다. loadAuthorizedClient(), saveAuthorizedClient(), removeAuthorizedClient() - OAuth2AuthorizedClientService의 구현체는 두 종류 InMemory, Jdbc 가 있는데, 기본은 InMemory 방식이다. InMemory 방식은 항상 주의해야하기 때문에, 언제 removeAuthorizedClient() 를 호출하나 찾아보았는데, 찾을 수 없었다. 만약 삭제하지 않고 계속 사용하게 된다면, 이용자 수만큼 메모리에 인스턴스가 남아있게 된다. 서비스 운영 시간이 길어질수록 서버 메모리가 점점 증가할 수 밖에 없는 구조이다.
- 혹시 몰라 또 찾아봤는데, 맞았다리.. (Use HttpSessionOAuth2AuthorizedClientRepository instead of AuthenticatedPrincipalOAuth2AuthorizedClientRepository as bean. spring-projects/spring-boot#24237)
- 우리 서비스는 외부 서비스를 직접적으로 이용하는 일이 없다. 즉 accessToken 을 실제 발급받기는 하지만, 사용할 일이 없고, 자체적인 jwt 토큰으로 인증을 할 예정이다. 기존회원들과 소셜 로그인 계정정보만 연동해 사용하는 구조.
- 이를 사용하는 경우는 재인증, 외부 api 이용 등. 대충 코드를 보니 재인증을 진행하기 위해서는 AuthenticationCodeOAuth2AuthenticationProvider 를 통해 authenticate() 를 진행해야한다. 그래야OAuth2AuthorizationRequestRedirectFilter 통해 재인증을 진행하고 loadAuthorizedClient() 등등을 하는 느낌.. 하지만 우리 서비스에는 굳이 필요한 기능은 아님.
따라서 OAuth2AuthorizedClientRepository 를 상속받아, save 시에 아무일도 일어나지 않게 처리해야겠다!
-> HttpSessionOAuth2AuthorizedClientRepository 를 이용하기로 했음!
[OAuth2 인증 관련 해야할 일]
- 성공 핸들러를 직접 구현해서, 신규회원 검사 로직과 jwt 토큰 발행 로직을 추가해야한다.
- 소셜로그인에 성공한 소셜로그인 계정에 대한 db table 생성.
- OAuth2UserService 를 직접 구현(
소셜로그인에 성공한 OAuth2User 인스턴스와 기존 회원을 비교해 적절한 권한을 매핑, 소셜 계정 저장 및 비교, 로그인 로그 등등) 소셜로그인은 jwt 토큰 발행의 start point 일 뿐. 모든 권한 검사는 jwt 에서 하게될 것. - HttpSessionOAuth2AuthorizedClientRepository 빈으로 등록
[최종 구현 모듈]
[SpringBoot] 7. SpringSecurity 인증 모듈 개발 (OAuth2, jwt, 소셜로그인)
'웹 프로젝트 (IBAS) > SpringBoot api 개편' 카테고리의 다른 글
[SpringBoot] 6. ManyToMany 를 "일대다/다대일"로 풀어서 사용하기 (+ 영속성 전이 문제) (0) | 2022.03.20 |
---|---|
[Spring boot] 5. 멀티모듈? MSA? 좋은 아키텍쳐가 뭐야?! (1) | 2022.03.09 |
[Spring Boot] 4. 로컬 개발을 위한 CORS 설정 - (1) w3c recommendation (0) | 2022.02.28 |
[Spring Boot] 2. 서버 개발 환경 분리 (Spring Cloud Config 적용) 및 배포 자동화 (0) | 2022.02.28 |
[Spring Boot] 1. 게시판 테이블 재설계 (0) | 2022.02.28 |