Spring Security 의 Authentication 공식문서
https://docs.spring.io/spring-security/reference/6.1-SNAPSHOT/servlet/authentication/architecture.html#servlet-authentication-securitycontext
누가 인증(Authenticated) 되었는지 정보를 담고있는 저장소의 개념이다.
아래는 아키텍쳐 사진이다.
SecurityContextHolder
안에 SecurityContext
가, 또 그 안에 Principal
, Credentials
, Authorities
정보가 존재한다.
다음은 해당 아키텍쳐 사진을 예시 코드로 분석한 내용이다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
코드 순서를 보면, 바로 짐작이 가능하다. 가장 먼저 SecurityContextHolder
의 빈 SecurityContext
를 만들고, Authentication
정보를 담은 인스턴스를 생성후, SecurityContext
에 Authentication
정보를 삽입한다.
마지막에는 Authentication
이 주입된 SecurityContext
를 SecurityContextHolder
에 주입한다.
반대로, authentication
을 가져오는 코드 예시를 살펴보자.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
SecurityContextHolder
에서부터 얻게되는 '인증' 정보이다.
AuthenticationManager
에 입력값으로 주어지는 요소로, User가 인증하기 위해 credential을 제공하기 위한 용도로 사용된다.
현재 인증받은 User정보를 나타낸다. 이는 SecurityContext
로부터 얻을 수 있다.
Authentication에는 세가지 구성요소가 있다.
principal
유저를 확인하기 위해 사용된다. UserDetails의 인스턴스라는데..?
credentials
비밀번호 같은 것이다. 대부분의 경우, 인증된 후에 clear 된다. (유출되면 안되므로)
authorities
허가받은 정보를 담은 인스턴스이다.
부여받은 권한 정보를 말한다. (roles, scopes 등등..)
Authentication.getAuthorities()
해당 코드로 확인이 가능하다.
스프링 시큐리티의 Filter가 Authentication을 어떤식으로 수행하는지에 대해 정의된 API를 말한다.
AuthenticationManger
의 구현체이다.
다음은 실제 AuthenticationManager
를 구현한 코드 내용이다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
아래 그림을 보면 알 수 있는데, 여러 AuthenticationProvider
들을 거친다.
AuthenticationProvider
들은 Authentication이 성공인지, 실패인지, 알 수 없는지 판단하는 역할을 한다. 만약 어떠한 Provider도 인증여부를 확인할 수 없다면, ProviderNotFoundException
예외를 반환한다.
ProviderManager
가 특정 Authentication의 타입을 제공하는데 사용된 Provider이다.
여러 AuthenticationProvider
들을 ProviderManager
에 주입하여 사용한다.
처음에, 왜 인증정보를 각기 다른 Provider가 처리해야하는 지가 궁금했다.
이에 대해 다음과 같이 소개한다.
practice each AuthenticationProvider knows how to perform a specific type of authentication. For example, one AuthenticationProvider might be able to validate a username/password, while another might be able to authenticate a SAML assertion. This lets each AuthenticationProvider do a very specific type of authentication while supporting multiple types of authentication and expose only a single AuthenticationManager bean.
여러 타입의 인증정보가 존재하기 때문이었다. (아직 감은 안온다)
예시를 보고 이해할 수 있었다.
Each AuthenticationProvider performs a specific type of authentication. For example, DaoAuthenticationProvider supports username/password-based authentication, while JwtAuthenticationProvider supports authenticating a JWT token.
짠! 깨우쳤다.
Client로부터 요청받은 credential정보를 처리하는데 사용된다. (login page로 redirect하거나, 인증에 대한 응답을 보내주는 역할)
public class SecurityConfig {
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
// PasswordEncoder는 BCryptPasswordEncoder를 사용
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// token을 사용하는 방식이기 때문에 csrf를 disable합니다.
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling((exceptionHandling) -> //컨트롤러의 예외처리를 담당하는 exception handler와는 다름.
exceptionHandling
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
// enable h2-console
.headers((headers)->
headers.contentTypeOptions(contentTypeOptionsConfig ->
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)))
// disable session
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests((authorizeRequests)->
authorizeRequests
//users 포함한 end point 보안 적용 X
.requestMatchers("/users/**").permitAll() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다.
.requestMatchers("/error/**").permitAll()
.requestMatchers("/swagger-ui/**","/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
/* swagger v3 */
"/v3/api-docs/**",
"/swagger-ui/**").permitAll()
// .requestMatchers(PathRequest.toH2Console()).permitAll()// h2-console, favicon.ico 요청 인증 무시
.requestMatchers("/favicon.ico").permitAll()
.anyRequest().authenticated() // 그 외 인증 없이 접근X
)
.exceptionHandling((exceptionHandling)->exceptionHandling
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.apply(new JwtSecurityConfig(tokenProvider)); // JwtFilter를 addFilterBefore로 등록했던 JwtSecurityConfig class 적용
return httpSecurity.build();
}
}
맨 아랫줄에 보면, exceptionHandling으로 해당 요소를 추가한 것을 볼 수 있다.
사용자 정보의 인증을 위한 가장 기본적인 필터이다.
대표적으로 UsernamePasswordAuthenticationFilter가 이친구를 상속받아 사용한다.
사용자가 요청한 credential이 인증을 받기 위해 SpringSecurity는 AuthenticationEntryPoint
로 요청을 보낸다.
사용자가 credential 정보를 보내면, AbstractAuthenticationProcessingFilter
가 HttpServletRequest
에 담긴 credential을 토대로 Authentication을 생성한다. 이 때, Authentication의 종류에 따라 다른 AbstractAuthenticationProcessingFilter
가 선택이 된다. 예를들어 UsernamePasswordAuthenticationFilter
는 UsernamePasswordAuthenticationToken
를 처리하는 역할을 담당한다.
생성된 Authentication을 AuthenticationManager에 보낸다.
인증이 실패하면 실패절차를 거친다.
이때, SecurityContextHolder는 비워지고,
RememberMeServices.loginFail, AuthenticationFailureHandler
가 동작한다.
인증이 성공하면 성공절차를 거친다.
SessionAuthenticationStrategy
가 로그인 여부를 알게된다.
인증 정보가 SecurityContextHolder에 들어간다.
RememberMeServices.loginSuccess
, AuthenticationSuccessHandler
가 발생한다.
ApplicationEventPublisher
는InteractiveAuthenticationSuccessEvent
를 발생 시킨다.