Table of Contents
Spring Security – Filter 를 알면 모든게 보인다.
Spring Security 에 대한 디버깅을 위해 작동방식을 찾아 보았습니다.
소스코드는 깃허브 에 있습니다.
흐름도
AuthorizationFilter
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/assets/**", "/login", "/error").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
.loginPage("/login"));
SecurityFilterChain chain = http.build();
List<Filter> filters = chain.getFilters();
for (Filter filter : filters) {
System.out.println(filter.getClass());
}
return chain;
}
}
SecurityFilterChain 에 들어가 있는 모든 필터를 출력해 보았습니다.
http request
가 들어오면 SecurityFilterChain 이 가지고 있는 필터 중 AnonymousAuthenticationFilter 가 호출되고 login 페이지로 이동합니다.
.......session.DisableEncodeUrlFilter
.......context.request.async.WebAsyncManagerIntegrationFilter
.......context.SecurityContextHolderFilter
.......header.HeaderWriterFilter
.......authentication.logout.LogoutFilter
.......authentication.UsernamePasswordAuthenticationFilter
.......savedrequest.RequestCacheAwareFilter
.......servletapi.SecurityContextHolderAwareRequestFilter
.......authentication.AnonymousAuthenticationFilter
.......access.ExceptionTranslationFilter
.......access.intercept.AuthorizationFilter
login 페이지에서 아이디/비밀번호를 입력하면 UsernamePasswordAuthenticationFilter 와 DaoAuthenticationProvider 가 호출됩니다.
2024-09-01T11:45:01.880+09:00 DEBUG 22420 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy : Securing POST /login
2024-09-01T11:45:02.080+09:00 DEBUG 22420 --- [nio-8080-exec-3] o.s.s.a.dao.DaoAuthenticationProvider : Authenticated user
2024-09-01T11:45:02.135+09:00 DEBUG 22420 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@614d264d]
2024-09-01T11:45:02.135+09:00 DEBUG 22420 --- [nio-8080-exec-3] w.a.UsernamePasswordAuthenticationFilter : Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[]]
로그인 이후에는 SecurityContextHolderFilter 에서 SecurityContext 속 Authentication 을 확인하고 접속을 허용합니다.
2024-09-01T12:18:14.003+09:00 DEBUG 2232 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy : Securing GET /
2024-09-01T12:18:14.004+09:00 DEBUG 2232 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[]]]
2024-09-01T12:18:14.004+09:00 DEBUG 2232 --- [nio-8080-exec-3] o.s.security.web.FilterChainProxy : Secured GET /
Custom UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter 를 커스터마이징해서 filter 에서 provider 까지 넘어가는 과정을 따라가 볼 수 있습니다.
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
http.addFilterBefore(
new CustomUsernamePasswordAuthenticationFilter(authenticationManager),
UsernamePasswordAuthenticationFilter.class
);
// ......
}
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public CustomUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
Authentication authentication = super.attemptAuthentication(request, response);
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
if (user.getUsername().startsWith("test")) {
return new UsernamePasswordAuthenticationToken(
user,
null,
Stream.of("ROLE_ADMIN", "ROLE_USER")
.map(authority -> (GrantedAuthority) () -> authority)
.collect(Collectors.toList())
);
}
return authentication;
}
}
Custom DaoAuthenticationProvider
코드를 단순화 하기 위해 Repository 등은 생략했습니다.
DaoAuthenticationProvider 를 커스터마이징 해서 특수한 요구사항을 만족시킬 수 있습니다.
(연령체크, 국적체크 등)
@Bean
public AuthenticationProvider daoAuthenticationProvider(CustomUserDetailsService userDetailsService) {
CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
// authenticationProvider.setPreAuthenticationChecks(new CustomPreAuthenticationChecks());
// authenticationProvider.setPostAuthenticationChecks(new CustomPostAuthenticationChecks());
return authenticationProvider;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add((GrantedAuthority) () -> "ROLE_USER");
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return CustomUserDetails
.builder()
.username(username)
.password(passwordEncoder.encode("password"))
.authorities(authorities)
.build();
}
}
@Getter
@Setter
@Builder
public class CustomUserDetails implements UserDetails, Serializable {
private String username;
private String password;
private Collection<GrantedAuthority> authorities;
}