1. SecurityContextPersistenceFilter
Spring Security는 default로 HttpSessionSecurityContextRepository
클래스를 사용하여 Security Context를 Session으로부터 가져오거나 저장한다. 만약 Session에 Security Context가 없다면 새로 만든다. SecurityContextPersistenceFilter
는 HttpSessionSecurityContextRepository
로부터 얻은 Security Context를 SecurityContextHolder
에 저장한다.
NOTE
SessionCreationPolicy를 STATELESS로 설정했을 경우 Filter가 모두 한 사이클이 돌고나서 SecurityContextHolder에 저장된 SecurityContext를 비운다.
IMPORTANT
공식 문서에서는 SecurityContextPersistenceFilter는 deprecate되고 SecurityContextHolderFilter으로 대체될 것이라고한다. 그 이유는 SecurityContextPersistenceFilter의 경우 request, response를 같이 사용하여 SecurityContext를 저장하는데 이러한 동작은 request가 완료되기 전에 저장될 수 있다는 문제가 될 수 있다고 한다. SecurityContextHolderFilter는 이런 문제를 해결하기위해 단순히 SecurityContextRepository에서 SecurityContext를 가져온 후 SecurityContextHolder에 저장하도록한다. 즉, 명시적으로 SecurityContext만 저장하도록 호출된다는 것이다. (문서에는 explicit save라고 함)
2. LogoutFilter
현재 request가 Logout 요청인지 확인하고 로그아웃을 수행한다.
3. UsernamePasswordAuthenticationFilter
다음과 같이 formLogin
을 추가했을 경우 동작한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
먼저 authentication(인증)이 필요한지 확인한 후 request로부터 username과 password를 획득하여 UsernamePasswordAuthenticationToken
를 생성한다. 이때 request의 Method는 POST
여야한다. 그리고 AuthenticationManager
라는 녀석이 authentication을 수행하는데 보통 실제로 내부에서는 DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
라는 AuthenticationProvider가 돌아간다. (생성된 Token을 받아 동작함) 예를 들어서 AuthenticationManager를 Customize하고 싶다면 다음과 같이 작성하면 된다.
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(authenticationProvider);
}
먼저 AbstractUserDetailsAuthenticationProvider
는 캐시(UserCache)에 username에 해당하는 UserDetails가 이미 존재하는지 확인하다. 없다면 DaoAuthenticationProvider
에서 구현된 UserDetailsService().loadUserByUsername
메소드를 통해 UserDetails
를 얻어 실제로 존재하는 유저인지 확인한 후 반환한다. 이후 해당 UserDetails의 expire, lock, disable 등을 확인하고 UserCache에 올린다. 마지막으로 UsernamePasswordAuthenticationToken
을 다시 생성하여 반환한다.
4. ConcurrentSessionFilter
다음과 같이 session을 1개로 제한했을 경우 ConcurrentSessionFilter
가 동작한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
);
return http.build();
}
요청마다 현재 유저의 session이 만료되었는지 확인하며 session이 만료되었다면 로그아웃을 수행한다.
5. RememberMeAuthenticationFilter
구현된 RememberService
로 Authentication을 얻어 SecurityContext에 저장한다. 예를 들어 TokenBasedRememberMeServices
을 사용한다면 다음과 같이 작성한다.
@Bean
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
return rememberMe;
}
먼저 RememberService의 autoLogin이라는 메소드를 수행한다. RememberMeServices의 기본 구현체 AbstractRememberMeServices
의 autoLogint 메소드를 확인해보면 쿠키(remember-me)로부터 값을 가져온 후 해당 값을 통해 UserDetails를 얻는다. (default로 "remember-me"라는 쿠키를 가져온다.) 이때 UserDetails를 얻는 processAutoLoginCookie
메소드는 AbstractRememberMeServices를 상속받은 TokenBasedRememberMeServices
또는 PersistentTokenBasedRememberMeServices
에 구현되어있다.
6. AnonymousAuthenticationFilter
SecurityContextHolder
에 Authentication 정보가 null인지 확인하여 anonymous(익명) 유저에대한 Authentication을 생성한다. 즉, Authentication이 없다면 익명 유저라고 판단하여 새로 Authentication을 생성하여 SecurityContext에 저장한다. 이때 anonymous 유저의 authority(권한)은 "ROLE_ANONYMOUS"로 등록된다.
7. SessionManagementFilter
앞서 설명한 내용처럼 STATELESS로 설정하거나 session의 개수를 제한하는 등 session과 관련된 작업을 처리한다. 이외에도 다음과 같이 session이 만료되었을때 redirect할 endpoint를 지정할 수 있다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionUrl("/invalidSession")
);
return http.build();
}
Session Fixation Attack
Session Fixation Attack은 어떤 사이트에 생성된 세션으로 다른 사용자가 로그인하도록 유도하는 것이다. 예를 들어 같은 세션의 세션 id를 포함하는 링크를 보내는 방법이 있다. 이런 공격을 막기위해 Spring Security는 자동으로 새로운 세션을 생성하거나 유저가 로그인할때마다 세션 id를 변경한다.
Spring Security의 Session Fixation Protection 전략은 다음 세 가지로 나누어진다.
- changeSessionId - session을 새로 생성하지 않고 Sevlet container에 의해 session fixation protection을 사용한다. 단, servlet 3.1에서 사용 가능하다.
- newSession - 새로운 clean한 session을 생성한다.
- migrateSession - 새로운 session을 생성하고 원래있던 모든 session의 속성값들을 새로운 session에 복사한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement((session) - session
.sessionFixation((sessionFixation) -> sessionFixation
.newSession()
)
);
return http.build();
}
8. ExceptionTranslationFilter
FilterChain 내에 발생되는 예외를 처리한다.
9. FilterSecurityInterceptor
어떤 Object의 attribute에 대한 Authorization(권한) 설정을 처리한다. 따라서 Authentication이 무조건 존재해야하며 권한 설정은 AccessDecisionManager
가 decide한다.
@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionManager accessDecisionManager;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
try {
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
return new AuthorizationDecision(true);
} catch (AccessDeniedException ex) {
return new AuthorizationDecision(false);
}
}
@Override
public void verify(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
}
}
실제로는 AccessDecisionManager
내부에서 AccessDecisionVoter
를 사용하여 판단한다. AccessDecisionVoter
는 다음과 같이 여러 구현체가 존재하며 AccessDecisionManager
는 여러 Voter들을 가질 수 있다.
그리고 AccessDecisionManager
는 Voter들의 결과를 통해 다음 세 가지 방법으로 최종 결과를 출력한다. 만약 true라면 void를 return하고 false라면 예외를 throw한다.
- AffirmativeBased: voter 중 하나라도 deny되었다면 false
- ConsensusBased: voter 중 deny가 적다면 true, grant된 것보다 많거나 같다면 false
- UnanimousBased: voter 중 grant된 것이 하나라도 있으면 true
마지막으로 RunAsManager
를 통해 권한이 부여된 Authentication(RunAsUserToken)으로 새로 생성하여 바꿔준다.
[reference]
https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html#authz-voter-adaptation
https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html#customize-global-authentication-manager
https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html
https://gngsn.tistory.com/160