[Spring Security] 스프링 시큐리티 구조, 시큐리티 필터

스프링 시큐리티

 

 많은 프로젝트들이 기본적인 회원 관리를 필요로 하고 있고, 그것을 위해서 보안에 관련된 다양한 처리가 필요하다. 스프링은 spring security라는 하위 프레임워크에서 인증(Authentication)과 인가(Authorization)에 관련된 다양한 기능을 제공하고 있다.

인증(Authentication): 접근하려는 유저에 대한 확인 절차 (login, join..)
인가(Authorization): 인증된 사용자에 대해서 권한을 관리하는 것
접근주체(Principal): 보호 대상에 접근하려는 유저
비밀번호(Credential): 대상에 접근하려는 유저의 비밀번호

 

 

 

스프링 시큐리티 동작 원리

 

스프링 시큐리티는 이러한 인증과 인가를 위하여 Principal(=username)과 Credential(=password)을 사용하여 동작한다. 이러한 스프링 시큐리티는 filter를 기반으로 동작하기 때문에 스프링의 dispatcherservlet 이전에 동작하기 때문에 spring mvc와 분리되어 관리하고 동작한다. 

 

https://spring.io/guides/topicals/spring-security-architecture

 Http Request가 들어오면, 스프링 시큐리티는 많은 보안 관련된 많은 작업들을 FilterChainProxy를 통해 Security Filter Chain에 위임하고, Spring Security Filter들을 통해서 처리한다. Spring Security Filter에는 CsrfFilter, OAuth2LoginAuthenticationFilter, UsernamePasswordAuthenticationFilter등의 여러 필터들이 존재하는데, 우리가 사용할 principal-credential 기반(id-pw) 인가에 대한 관리는 UsernamePasswordAuthenticationFilter를 통해서 이루어진다.

 

스프링 시큐리티 구조

 

1. 클라이언트로부터 Http Request가 들어오면, spring security는 Security Filter Chain을 통해서 인가 과정을 관리하는데, 여러 security filter중에서 UsernamePasswordAuthenticationFilter(이하 AuthenticationFilter)라는 필터에서 인증 과정을 맡아 처리한다.

2. 이러한 AuthenticaitionFilter는 HttpServlet 객체에서 username과 password를 추출하여 UsernamePasswordAuthenticationToken을 생성한다.

3,4,5. AuthenticationFilter가 이 토큰을 AuthenticationManager로 넘기면, AuthenticationManager는 AuthenticationProvider에 이 토큰을 넘기고, AuthenticationProvider는 UserDetailsService로 토큰 정보(username, password)를 넘긴다.

6,7. UserDetailsService는 전달받은 사용자 정보를 기반으로 DB에서 알맞은 사용자를 찾고 이를 기반으로 하여 UserDetails객체를 만들어 AuthenticationProviders에 반환한다.

8. AuthenticationProvider는 전달받은 UserDetails를 인증해 성공하면 ProviderManager에게 검증된 UserDetails객체를 반환한다.

9,10 AuthenticationFilter는 검증된 UserDetails객체를 SecurityContextHolder의 SecurityContext에 저장한다.

 

 

 

 

지금까지 회원가입 로직을 개발할때는 API컨트롤러를 만들고, 서비스에서 JPARepository의 메소드를 사용하거나 JPA 명명규칙을 이용한 메소드를 만들어서 직접 인증을 거치는 방법 등을 주로 사용했다. 이러한 프로그램에 스프링 시큐리티를 도입하면, 스프링 시큐리티는 모든 접근요청을 가로채서 인증을 요구하게 된다.

 

 

인증이 완료되면 시큐리티 세션에 user정보를 등록하고, 이후에 우리는 그 유저정보를 가져와서 사용하게 된다. 이를 위해서 우리는 User객체가 스프링 시큐리티의 UserDetails 인터페이스를 구현하게 하면 된다. 

스프링 시큐리티가 로그인 요청을 가로채서 로그인을 진행하고 완료가 되면 UserDetails를 구현한 오브젝트를 스프링 시큐리티의 고유한 세션저장소에 저장한다.

 

 

Configuration 세팅하기

 

 HttpSecurity 필터를 이용하여 스프링 시큐리티 앞단의 설정들을 완료하면 된다.

기존에는 스프링 시큐리티를 이용하여 configuration class를 작성하기 위해서는 WebSecurityConfigurerAdapter를 상속하여 클래스를 생성하고 @EnableWebSecurity 어노테이션을 추가해야 했다. 하지만 최근 시큐리티 버전부터 WebSecurityConfigurerAdapter가 deprecated되면서 상속받아 오버라이딩하는 방식 대신 Bean으로 등록하는 방식으로 변경되었다.

HttpSecurity 클래스에는 이와 같은 다양한 메소드가 존재한다.

 

 

 

@Configuration // 스프링 빈 등록
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    protected SecurityFilterChain configure(HttpSecurity http) throws Exception {

        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/loginProc")
                .defaultSuccessUrl("/");

        return http.build();
    }
}

 

인자로 전달되는 HttpSecurity객체는 HTTP 보안 관점을 설명하기 위해서 사용된다.

 

- http object의 첫 번째 레벨의 함수들은 configurer을 나타낸다.

.formLogin() --> FormLoginConfigurer
.httpBasic() --> HttpBasicConfigurer()
.sessionManagement() --> SessionManagementConfigurer

 

- 두 번째 레벨은 configurer의 특성들을 나타낸다.

단, and()의 경우 이전까지의 속성에서 나와서 http.을 반복하는 효과와 동일하다.

 

위 코드에서는  우선 별도의 토큰요청을 필요로 하는 csrf protection을 disable하고, authorizeRequests()를 통해서 시큐리티 처리에 HttpServletRequest를 이용한다는 것을 명시한 후, 빌더 패턴을 이용하여 반환한다.

 

 

.csrf()

별도의 csfr 토큰을 필요로 하는 csrf protection을 관리한다. 일반적으로 프로젝트 단계에서는 csrf().disable()을 통해서 사용하지 않도록 설정하면 된다.

 

 

.authorizeRequests().antMachers()

.antMatchers("/user/**").authenticated()

- .authorizeRequests 아래에서 작동하고, 특정 리소스에 대해서 권한을 설정하는 역할을 한다. 뒤에 authenticated가 붙은 것은 허용된 사용자의 접근만을 허용한다는 것이다. 

- 단, 인증, 인가 어떤 경우에도 시큐리티 필터 체인은 통과한다. 따라서 Configuration에서 인증로직이 동작하지 않더라도, 필터 체인을 통과하다가 exception이 던져질 경우 인증에 실패하게 된다.

 

 

permitAll() - 무조건 접근 허용,

hasAnyRole(String str) - str의 권한을 가진 사용자만 접근 허용,

access(String str) - 인자로 들어온 SpEL식의 결과가 true이면 접근 허용

 

위와 같은 SpEL식들이 존재한다.

 

.formLogin()

http.formLogin()
        .loginPage("/login-page")
        .loginProcessingUrl("/login-process")
        .defaultSuccessUrl("/main")
        .successHandler(new CustomAuthenticationSuccessHandler("/main"))
        .failureUrl("login-fail")
        .failureHandler(new CustomAuthenticationFailureHandler("/login-fail"))

로그인 폼 페이지와 로그인 처리 등을 사용하겠다는 의미이다. 이것을 호출하지 않으면 (커스텀 필터 제외) form을 이용한 로그인 페이지 및 로그인 처리가 불가능하다.

 

.loginPage(): 로그인 경로를 설정한다

.loginProcessingUrl(): 로그인 처리를 하는 URL을 설정한다.

.defaultSuccessUrl()/.failureUrl(): 로그인 인증이 정상적/비정상적으로 처리되었을 경우 이동하는 페이지를 설정한다.

.success/defaultHandler(): 인증 성공/실패시 처리할 핸들러

 

 

 

참고

https://jhhan009.tistory.com/31