Authentication
스프링 시큐리티는 인증(Authentication)에 대한 포괄적인 지원을 제공한다. 전반적인 서블릿 인증 아키텍처부터 훑어볼 것이다. 예상할 수 있듯이 이 섹션은 구체적인 흐름에 대한 이야기 없이 추상적인 아키텍처에 대해서만 설명한다.
인증 메커니즘
- Username and Password - 사용자 이름/비밀번호로 인증하는 방법
- OAuth 2.0 Login - OAuth 2.0 OpenID Connect 로그인 및 비표준 OAuth 2.0 로그인 (i.e. GitHub)
- SAML 2.0 Login - SAML 2.0 로그인
- Central Authentication Server (CAS) - 중앙 인증 서버 (CAS; Central Authentication Server) 지원
- Remember Me - 세션 만료 후 사용자를 기억하는 방법
- JAAS Authentication - JAAS로 인증
- OpenID - OpenID 인증 (OpenID Connect와 혼동하지 말 것)
- Pre-Authentication Scenarios - SiteMinder 또는 Java EE 보안과 같은 외부 메커니즘으로 인증하지만 여전히 일반적인 악용에 대한 인증 및 보호를 위해 Spring Security를 사용한다.
- X509 Authentication - X509 인증
1. SecurityContextHolder
Spring Security 인증의 핵심은 SecurityContextHolder이다. 이는 SecurityContext를 포함하고 있다.
SecurityContextHolder는 Spring Security가 인증된 사람의 세부 정보(details)를 저장하는 곳이다. Spring Security는 SecurityContextHolder가 어떻게 채워지는지 신경쓰지 않는다. 만약 값을 들고있다면 현재 인증된 사용자로 사용된다.
사용자가 인증되었음을 확인하는 가장 간단한 방법은 SecurityContextHolder를 직접 설정하는 것이다.
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 1
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); //2
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); // 3
- 처음에 SecurityContext는 비어있다. 여러 스레드의 경쟁조건을 피하기 위해 SecurityContextHolder.getContext().setAuthentication(authentication)를 사용하는 대신 새로운 SecurityContext 인스턴스를 만드는 것이 중요하다.
- 그 다음으로 새로운 Authentication 객체를 생성한다. Spring Security는 어떤 유형의 Authentication 구현이 사용되었는지 신경쓰지 않는다. 예제에서는 매우 간단한 TestingAuthenticationToken을 사용한다. 이보다 더 흔한 프로덕션 시나리오는 userDetails, password, authorities를 사용하는 UsernamePasswordAuthenticationToken이다.
- 마지막으로, SecurityContext를 SecurityContextHolder에 설정한다. 그럼 SpringSecurity는 권한 부여를 하기 위해 해당 정보를 사용할 것이다.
인증된 객체에 대한 정보를 얻고 싶다면 다음과 같이 SecurityContextHolder에 접근하면 된다. 기본적으로 SecurityContextHolder는 ThreadLocal을 이러한 세부 정보를 저장하는데 사용한다. ThreadLocal을 사용하면 SecurityContext가 해당 메서드에게 인수로 명시적으로 전달하지 않은 경우에도 동일한 스레드의 메서드에서 항상 사용할 수 있다. 현재 인증 객체의 요청이 처리된 후 스레드를 지우는 데 주의를 기울이면 ThreadLocal을 사용하는 방식은 안전하다. Spring Security의 FilterChainProxy가 스레드를 항상 지워준다.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
2. SecurityContext
SecurityContext는 SecurityContextHolder에 들어있다. 그리고 이는 인증된 객체(Authentication)를 지니고 있다.
3. Authentication
Authentication은 Spring Security내에서 두 가지 주 목적을 제공한다.
- AuntenticationManager에 입력하는 것은 사용자가 인증을 위해 제공한 자격 증명을 제공하기 위함이다. 이 시나리오를 사용하면 isAuthenticated()는 false를 반환한다.
- 현재 인증된 사용자를 나타낸다. 현재 인증된 객체 Authentication은 SecurityContext에 저장되어 있다.
Authentication은 다음을 포함하고 있다.
- principal: 사용자를 식별한다. 사용자의 username/password를 인증할 때 이는 종종 UserDetail의 인스턴스로 표현된다.
- credentials: 대부분 암호를 나타낸다. 거의 대부분 경우 이 정보는 유출되지 않도록 사용자가 인증된 후 지워진다.
- authorities: GrantedAuthority는 사용자에게 부여되는 고수준 권한이다. 예로 role이나 scope가 있다.
4. GrantedAuthority
GrantedAuthority는 사용자에게 부여되는 고수준 권한이다. 예로 role이나 scope가 있다.
GrantedAuthority는 Authentication.getAuthorities() 메서드로 포함될 수 있다. 이 메서드는 GrantedAuthority 객체의 Collection을 제공한다. GrantedAuthority는 당연히 현재 유저에게 부여된 권한이다. 권한은 ROLE_ADMIN 이나 ROLE_MANAGER과 같은 일반적인 “role”을 뜻한다. 이러한 역할은 나중에 웹 권한 부여, 메서드 권한 부여 및 도메인 개체 권한 부여를 위해 구성된다. username/password기반 인증을 사용할 때 GrantedAuthority들은 일반적으로 UserDetailsService에 의해 저장된다.
GrantedAuthority는 특정 엔티티에 한정되지 않고 주어진 앱 전체 권한을 나타낸다. 특정 엔티티 객체 당 권한을 부여하게 되면 유저가 수백 수천으로 늘어나면 메모리가 빠르게 부족해진다. 따라서 Employee 44번 객체에게 GrantedAuthority를 부여하고 이런 일을 하지 않아도 된다.
5. AuthenticationManager
AuthenticationManager는 Spring Security의 필터가 인증을 수행하는 방법을 정의하는 API이다. Spring Security의 필터와 같은 Controller가 AuthenticationManager를 호출하면 Authentication 객체를 반환하는데, 이는 SecurityContextHolder에 설정된다. AuthenticationManager의 구현할 수 있는 방법은 다양하다. 대표적으로는 ProviderManager라는 구현체가 있다.
6. ProviderManager
ProviderManager는 가장 일반적인 AuthenticationManager의 구현체로 인증 제공자(AuthenticationProvider) 목록들에게 위임한다. 각 인증 제공자는 인증이 성공 또는 실패 또는 결정을 내릴 수 없음을 표시하고 만약 결정을 내릴 수 없다면 다음 다운스트림 인증 제공자가 결정하도록 넘긴다. 만약 구성된 AuthenticationProvider가 없는 경우 인증할 수 없기 때문에 ProviderNotFoundException을 던지며 인증을 실패한다.
- ProviderNotFoundException는 ProviderManager가 전달된 인증 유형을 지원하도록 구성되지 않았음을 나타내는 AuthenticationException이다.
인증 제공자(AuthenticationProvider) 목록에 위임하기
실제로 각 AuthenticationProvider는 특정한 방법으로 인증을 수행하게 된다. 예로, 위에선 AuthenticationProvider는 사용자 username/password의 유효성 검사를 할 수 있고 다음에 있는 다운 스트림에서는 SAML assertion을 인증할 수 있다. 따라서, 단 하나의 AuthenticationManager Bean안에 들어있는 여러 AuthenticationProvider들은 서로 자신만의 인증 방식을 수행하게 된다. ProviderManager를 사용하면 AuthenticationProvider가 어떤 인증을 수행할 수 없는 경우에는 해당 인증이 가능한 부모 AuthenticationManager 중 하나를 구성할 수도 있다.
같은 AuthenticationManager 공유
실제로 여러 ProviderManager 인스턴스가 같은 AuthenticationManager 부모를 공유할 수 있다.
여러 SecurityFilterChain 인스턴스가 있는 시나리오에서 일반적으로 같은 부모 AuthenticationManager 객체를 가진 공통된 인증들과 그 외에 다른 인증 메커니즘(다른 ProviderManager 객체)들로 구성된다.
Authenticaion객체에 있는 민감한 사용자 인증 정보 지우기
기본 ProviderManager는 성공적인 인증 요청에 의해 반환된 Authenticaion 객체에 있는 민감한 사용자 인증 정보를 지우려고 시도한다. 왜냐하면 이렇게 하면 비밀번호와 같은 정보가 HttpSession에서 필요 이상으로 오래 유지되는 것을 방지할 수 있기 때문이다.
계속해서 민감한 정보를 들고있다면 stateless 애플리케이션의 성능을 향상시키기 위해 캐시를 사용할 때 문제를 일으킬 수 있다. 만약 Authentication 객체가 캐시에 저장된 UserDetails 객체를 참조하고 있는데 사용자 인증 정보가 제거된다면 더 이상 캐시에 저장된 UserDetails값에 대해 인증을 할 수 없게 된다. 그래서 캐시를 사용하는 경우 이를 반드시 고려해야하며 이에 대한 확실한 대비책은 캐시 구현체 또는 반환된 인증 객체를 만드는 AuthenticationProvider에서 방어적 복사본을 미리 만드는 것이다.
- 또는 ProviderManager에서 EraseCredentialsAfterAuthentication 속성을 사용 중지할 수 있다. (참고)
7. AuthenticationProvider
AuthenticationProvider는 여러 인증 제공자를 ProviderManager에 삽입할 수 있다. 그리고 각 인증 제공자는 특정 유형의 인증을 수행한다.
- 예를 들어, DaoAuthenticationProvider는 사용자 username/password 기반 인증을 지원하고
- JwtAuthenticationProvider는 JWT 토큰 인증을 지원한다.
8. Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint는 클라이언트로부터 자격 증명(credential)을 요청하는 HTTP 응답을 보내는 데 사용된다. 가끔 클라이언트는 리소스를 요청하기 위해 사용자 username/password와 같은 자격 증명을 사전에 포함하는 경우가 있는데 이때는 Spring Security가 이미 해당 데이터가 포함되어 있기 때문에 클라이언트에게 자격 증명을 요청하는 HTTP 응답을 보낼 필요가 없다.
다른 경우로는 클라이언트가 액세스 권한이 없는 리소스에 접근하면 unauthenticated 요청을 보낼 때가 있다. 이 경우에는 AuthenticationEntryPoint 구현체가 클라이언트의 사용자 인증 정보를 요청하게 된다. AuthenticationEntryPoint 구현체는 로그인 페이지로 리디렉션을 수행하고 WWW-Authenticate 헤더 등으로 응답할 수 있다.
9. AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter는 사용자의 자격 증명을 인증을 처리하는 기본 필터로 사용된다.
- 자격 증명이 인증되기 전에 Spring Security는 일반적으로 AuthenticationEntryPoint를 사용하여 자격 증명을 요청한다.
- 그리고 그 다음으로 AbstractAuthenticationProcessingFilter가 제출된 모든 인증 요청을 인증한다.
Authenticate 과정
- 1) 사용자가 자격 증명(credentials)을 제출하면 AbstractAuthenticationProcessingFilter가 인증될 HttpServletRequest에서 인증을 생성한다.
- 생성된 인증 유형은 AbstractAuthenticationProcessingFilter의 하위 클래스에 따라 다르다.
- 예를 들어, UsernamePasswordAuthenticationFilter는 HttpServletRequest에 제출된 사용자 이름과 암호에서 UsernamePasswordAuthenticationToken을 만든다.
- 2) 그리고 Authentication 객체는 인증을 하기 위해 AuthenticationManager로 전달된다.
- 만약 인증이 실패하면 3)Failure로 이동한다,
- 3-1) SecurityContextHolder가 지워진다.
- 3-2) RememberMeServices.loginFail이 호출된다. ‘remember me’가 구성되지 않은 경우에는 작동하지 않는다.
- 3-3) AuthenticationFailureHandler가 호출된다.
- 만약 인증이 성공하면 4)Success로 이동한다.
- 4-1) SessionAuthenticationStrategy는 새 로그인에 대한 알림을 받는다.
- 4-2) Authentication은 SecurityContextHolder에서 설정된다. 나중에 SecurityContextPersistenceFilter는 SecurityContext를 HttpSession에 저장한다.
- 4-3) RememberMeServices.loginSuccess가 호출된다. ‘remember me’가 구성되지 않은 경우에는 작동하지 않는다.
- 4-4) ApplicationEventPublisher는 InteractiveAuthenticationSuccessEvent를 게시한다.
- 4-5) AuthenticationSuccessHandler가 호출된다.
https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
'Dot Programming > Spring' 카테고리의 다른 글
[Spring] TestContainers로 멱등성있는 MySql 테스트 환경 구축하기 (0) | 2022.04.03 |
---|---|
생산성 향상을 위해 반드시 테스트 코드를 작성하자 (0) | 2022.03.28 |
[Spring Security] 공식 문서로 Spring Security 구조 파악하기 (0) | 2022.02.12 |
[Spring] 조회 API 성능 최적화하기 2 - 컬렉션 조회 최적화 : ToMany 매핑 (0) | 2021.11.24 |
[Spring] 조회 API 성능 최적화하기 1 - 지연로딩(Lazy Loading)과 페치 조인(Fetch Join) : ToOne 매핑 (0) | 2021.11.22 |