Spring Security – Filter 를 알면 모든게 보인다.

By | 2024년 8월 25일
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;

}

답글 남기기