걸음마부터 달리기
[24/11/12] 시큐리티 Authentication 본문
결국 스프링에서 지원하는 인증 방법은 이정도인데 가장 많이 쓰는 Username and Password 와 OAuth 2.0 Login에 대해 살펴볼려 한다.
그 전에 기본 Authentication 아키텍처가 어떻게 되어있는지 확인해보자.
SecurityContextHolder
이 SecurityContextHolder는 스프링 시큐리티가 누가 현재 인증되었는지에 대한 디테일을 저장하고 있다.
여기에 저장되어 있는 값은 곧 현재 인증된 사용자로 사용된다.
즉 SecurityContextHolder는 SecurityContext가 있냐 없냐에 따라 단순하게 인증 유무로 판단하고 값이 있다면 인증된 사용자로 간주한다. 즉 우리가 할건 그냥 SecurityContextHolder에 SecurityContext를 넣기만 하면 된다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
이 예시로 많을걸 알 수 있다.
Authentication 객체를 만들어서 Context에 넣을건데, 각각의 사용자에 대해 SecurityContext를 새로 만들어서 새로 만든 Authentication을 넣어주는게 좋다. (Race Condition 고려)
Authentication은 많은 구현체들이 있지만 가장 간단한 TestingAuthenticationToken을 사용했다.
Access Currently Authenticated User
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
스프링은 결국 요청이 올때마다 스레드 풀에서 스레드 가져와서 각각을 수행시킨다는 것을 잊지 말자. 따라서 스레드 당 한명의 사용자가 요청할 것이니 해당 스레드에서는 SecurityContext가 Holder에 하나 존재한다.
SecurityContextHolder는 스레드 로컬을 사용하기에 하나의 스레드에서는 동일한 SecurityContext를 가짐을 보장한다.
따라서 스레드 충돌 없이 인증정보 관리 가능하다.
Authentication
Authentication은 Security 내에서 두개의 주요 목적을 가지고 있다.
- 인증을 위해 사용자가 제공한 자격 증명을 제공하는 AuthenticationManager에 대한 입력입니다. 이 시나리오에서 isAuthenticated()는 false를 반환합니다.
- 현재 인증된 사용자를 나타냅니다. 현재 Authentication은 SecurityContext에서 얻을 수 있습니다.
AuthenticationManager와 Authentication에 대해 좀 더 알아보자.
만약 사용자가 로그인을 한다면 필터가 그 정보를 기반으로 Authentication의 필드값을 채워서 객체가 만들어지고 이 객체는 AuthenticationManager을 통해 전달되어 실제 인증이 수행된다. 즉 Provider를 통해 정보가 검증된다.
인증 성공하면 이 Provider가 인증된 사용자 정보를 포함하는 새로운 Authentication 객체를 반환하고 그것을 SecurityContext에 저장한다.
Authentication 객체는 다음을 포함합니다:
- principal: 사용자를 식별합니다. 사용자 이름/비밀번호로 인증할 때, 이는 종종 UserDetails의 인스턴스입니다.
- credentials: 종종 비밀번호입니다. 많은 경우, 사용자가 인증된 후에는 비밀번호가 유출되지 않도록 지워집니다.
- authorities: GrantedAuthority 인스턴스는 사용자가 부여받은 고수준의 권한을 나타냅니다. 두 가지 예로는 역할(roles)과 범위(scopes)가 있습니다.
AuthenticationFilter( ==AbstractAuthenticationProcessingFilter)
인증 프로세스의 시작으로 얘가 이제 AuthenticationManager를 호출함으로서 모든게 시작된다. 여기서 파싱해서 Authentication 객체를 만든다. 아래의 AbstractAuthenticationProcessingFilter와 동일
GrantedAuthority
GrantedAuthority 인스턴스는 Authentication.getAuthorities() 메서드를 통해 얻을 수 있다. 이 메서드는 GrantedAuthority 객체의 컬렉션을 제공한다. GrantedAuthority는 주체(principal)에게 부여된 권한을 나타낸다.이러한 권한은 일반적으로 ROLE_ADMINISTRATOR 또는 ROLE_HR_SUPERVISOR와 같은 "역할"이다. 특정 도메인에 대한 권한이 아닌 어플리케이션 전반의 권한을 나타낸다. 이러한 Authority는 사용자 이름/비밀번호 기반 인증에서 UserDetailsService에 의해 로드된다.
AuthenticationManager
스프링 시큐리티 필터가 어떻게 인증을 진행하는지에 대한 정의 API이다.
Authentication 객체는, 이 AuthenticationManager를 호출한 Spring Security 필터 인스턴스에 의해 SecurityContextHolder에 설정됩니다.
(즉 필터가 이 Manager를 호출해서 그 Manager가 작업을 하고 다시 필터쪽에서 Authentication을 SecurityContextHolder에 넣어준다.)
여러 실질 인증 방법을 정의한 AuthenticationManager가 있지만 가장 대표적인 구현체는 ProvideManager이다.
ProviderManager
ProviderManager는 AuthenticationManager의 가장 일반적으로 사용되는 구현체로 ProviderManager는 AuthenticationProvider 인스턴스의 리스트에 인증 처리를 위임한다.
결국 AuthenticationProvider가 특정 인증 로직을 들고있고 현재 들어온 Request에 관해 어떤 인증 방식을 쓸건지를 수많은 AuthenticationProvider 중에서 찾는 것이 ProviderManager가 할 일이다.
그런 의미에서 AuthenticationManger(ProviderManager) 은 인증 로직이 아닌 이 뒤의 AuthenticationProvider가 실제로 인증 로직 처리를 해주기에 정의 API라고 명시한 것이다.
최종적으로 어플리케이션은 AuthenticationManager을 통해 AuthenticationProvider를 결정하는데 단일 AuthenticationManager 빈을 통해 모든 인증을 처리할 수 있다는거다. 즉 어플리케이션은 각기 다른 유형의 인증을 수행하도록 하는 여러개의 AuthenticationProvider가 있어도 각 인증방법을 다르게 호출할 필요 없이 하나의 AuthenticationManager 빈을 통해 호출하면 된다. (==정의 API이다)
ProviderManager는 선택적으로 상위 AuthenticationManager를 구성할 수 있게 해줍니다. 이 상위 AuthenticationManager는 설정된 AuthenticationProvider들 중 어느 것도 인증을 수행할 수 없는 경우에 호출됩니다. 상위 AuthenticationManager는 어떤 유형이든 될 수 있지만, 보통은 또 다른 ProviderManager 인스턴스입니다.
사실 여러 ProviderManager 인스턴스가 같은 상위 AuthenticationManager를 공유하는 경우도 있습니다. 이는 여러 개의 SecurityFilterChain 인스턴스가 공통의 인증 처리(공유된 상위 AuthenticationManager)는 사용하지만, 각기 다른 인증 메커니즘(서로 다른 ProviderManager 인스턴스)을 사용하는 경우에 흔히 발생합니다.
AuthenticationProvider
이제 AuthenticationProvider를 ProviderManager에게 주입하여 서로 다른 로그인 인증을 동작시킬 수 있다.
Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint는 자격증명을 요청하는 HTTP응답을 보내기 위해 사용된다.
즉 어떤 자원에 대해 자격증명이 안된 사용자가 접근을 했을때 AuthenticationEntryPoint에서 자격증명 내놓으라는 Http 응답을 보낸다는 것이다.
AuthenticationEntryPoint는 구현체는 로그인 페이지로 리다이렉트하거나, 여러 응답(Json 포함) 을 보내는 작업을 수행할 수 있다.
AbstractAuthenticationProcessingFilter
인증요청을 처리한다. 즉 AuthenticationManager는 실제 인증 로직(AuthenticationProvider)를 찾기 전에 우선 인증 요청을 AuthenticationProvider에서 처리할 수 있게끔 전처리 해줘야하는데 그게 AbstractAuthenticationProcessingFilter이다.
따라서 먼저 AbstractAuthenticationProcessingFilter를 통해 전처리한걸 AuthenticationManager가 받아서 AuthenticationProvider를 통해 인증 로직을 수행한다. 여기서 파싱해서 Authentication 객체를 만든다.
AbstractAuthenticationProcessingFilter VS AuthenticationProvider
인증 전처리와 실제 인증 로직의 차이이다.
1. 사용자 자격 증명 제출
사용자가 자격 증명을 제출하면, AbstractAuthenticationProcessingFilter는 HttpServletRequest에서 인증할 Authentication 객체를 생성합니다. 생성된 Authentication 객체의 유형은 AbstractAuthenticationProcessingFilter의 하위 클래스에 따라 다릅니다. 예를 들어, UsernamePasswordAuthenticationFilter는 HttpServletRequest에 제출된 사용자 이름과 비밀번호를 기반으로 UsernamePasswordAuthenticationToken 객체를 생성합니다.
(==들어온 Request 기반으로 파싱해서 Authentication 객체를 만드는게 AbstractAuthenticationProcessingFilter)
2. 인증 시도
그 후, 생성된 Authentication 객체는 인증을 위해 AuthenticationManager에 전달됩니다.
3. 인증 실패 시 처리
- 인증에 실패하면, 실패 처리 과정이 시작됩니다.
- SecurityContextHolder가 비워집니다.
- RememberMeServices.loginFail이 호출됩니다. 만약 "Remember Me" 기능이 설정되지 않았다면, 이는 아무 작업도 하지 않습니다. (rememberme 패키지 참조)
- AuthenticationFailureHandler가 호출됩니다. (AuthenticationFailureHandler 인터페이스 참조)
4. 인증 성공 시 처리
- 인증이 성공하면, 성공 처리 과정이 시작됩니다.
- SessionAuthenticationStrategy가 새로운 로그인에 대해 알림을 받습니다. (SessionAuthenticationStrategy 인터페이스 참조)
- 인증된 Authentication 객체가 SecurityContextHolder에 설정됩니다. 이후, SecurityContext를 저장하여 향후 요청 시 자동으로 설정되도록 하려면, SecurityContextRepository#saveContext 메서드를 명시적으로 호출해야 합니다. (예: SecurityContextHolderFilter 클래스 참조)
- RememberMeServices.loginSuccess가 호출됩니다. "Remember Me" 기능이 설정되지 않았다면, 이는 아무 작업도 하지 않습니다. (rememberme 패키지 참조)
- ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent 이벤트를 발행합니다.
- AuthenticationSuccessHandler가 호출됩니다. (AuthenticationSuccessHandler 인터페이스 참조) . 기본설정으로는 이 핸들러에서 기존 요청의 url로 리다이렉트 시킨다.
AuthenticationFailureHandler VS AuthenticationEntryPoint
전자는 인증 요청을 받아서 하는 와중에 문제가 생기면 동작하는 핸들러이고
후자는 인증 요청이 아닌 인증 자체가 안되었는데 특정 자원에 접근하려 할때 인증 받고 자원을 요청해야하므로 인증 요청을 위한 자원으로 돌려보낼때 동작한다.
Authenticaion 의 흐름
- 사용자 인증 요청:
- 사용자가 로그인 정보(예: 사용자명과 비밀번호)와 함께 인증 요청을 보냅니다.
- AuthenticationFilter:
- UsernamePasswordAuthenticationFilter가 요청을 가로챕니다.
- 요청에서 인증 정보를 추출하여 인증되지 않은 UsernamePasswordAuthenticationToken 객체를 생성합니다.
- AuthenticationManager:
- Filter는 생성된 Authentication 객체를 AuthenticationManager(주로 ProviderManager)에게 전달합니다.
- AuthenticationProvider:
- AuthenticationManager는 등록된 AuthenticationProvider들 중 적합한 Provider를 선택하여 인증을 요청합니다.
- UserDetailsService:
- AuthenticationProvider는 UserDetailsService를 통해 사용자 정보를 조회합니다.
- UserDetailsService는 DB 등에서 사용자 정보를 가져와 UserDetails 객체를 생성합니다.
- 비밀번호 검증:
- AuthenticationProvider는 UserDetails의 정보와 사용자가 제공한 인증 정보를 비교합니다.
- 주로 PasswordEncoder를 사용하여 비밀번호를 검증합니다.
- 인증 완료:
- 인증이 성공하면, 권한 정보 등이 포함된 완전히 인증된 Authentication 객체를 생성합니다.
- SecurityContext에 저장:
- 인증된 Authentication 객체는 SecurityContextHolder를 통해 SecurityContext에 저장됩니다.
- 인증 결과 반환:
- 인증 결과(성공 또는 실패)가 필터 체인을 거쳐 최종적으로 사용자에게 반환됩니다.
종합과정
기본 전제:
SecurityContextPersistenceFilter가 요청을 가로채서 SecurityContextRepository 일단 세션에 SecurityContext 있는지 확인
있으면 세션에서 SecurityContext가져와서 Holder에 주입
없으면 SecurityContext 새로 만들기
SecurityContextPersistenceFilter >> 인증 filter >> ExceptionTranslationFilter
1. 인증 안된 상태에서 특정 자원에 접근
>> AuthenticationException 터지면서 ExceptionTranslationFilter 까지 올라옴. 그러면 catch해서 AuthenticationEntryPoint에 위임함. AuthenticationEntryPoint는 자격증명을 요청하는 HTTP 응답을 보내기 위해 사용하며 이는 일반적으로 로그인 페이지로 리다이렉트 시킴.
이때 기존 url을 저장하기 위해서 서블릿 Request 객체의 정보를 SavedRequest 객체로 변환하여 저장함
기본적으로 ExceptionTranslationFilter가 HttpSessionRequestCache를 사용하여 HTTP 세션에 저장함.
-----------------------------------이제 로그인 인증 시작------------------------------------------------------
AbstractAuthenticationProcessingFilter의 AuthenticationFilter에서 인증요청을 전처리해서 Authentication 객체로 일단 만듦.
만든 후 이걸 AuthenticationManager(ProviderManager)에 던지면 얘가 자신의 AuthenticationProvider 리스트 돌려보면서 되는 찾고 위임 시켜줌. 위임된 AuthenticationProvider에서 서비스단으로 자격 증명 해보고 실패하면 AuthenticationFailureHandler 가 호출. 인증이 성공하면 인증된
ROLE이 들어가있는 Authentication을 받고
AuthenticationProvider > ProviderManager > AuthenticationFilter 까지 Authentication이 반환되어 이 AuthenticationFilter 에서 SecurityContext에 등록한다.
그 이후 SecurityContextPersistenceFilter가 SecurityContext를 Session에 등록한다.
이러면 이제 다음 요청에도 SecurityContextPersistenceFilter가 Session에 SecurityContext 있는지 확인하고 반복.
2. 인증은 됐지만 인가문제
AccessDeniedException에서 인가 예외 터지면서 ExceptionTranslationFilter 의 AccessDeniedHHandler에 의해 처리됨. 기본적으로 403 응답을 보내거나 접근 거부 페이지로 리다이렉트함.