Spring Security (1) - Architecture

spaghetti·2024년 6월 21일

Spring Security

목록 보기
1/7

여태껏 운좋게도 spring을 최선버전으로만 써왔다. 그러나 최근 프로젝트에서 전자정부 프레임워크를 사용할 일이 있어 분석하다가 커스텀하려고 하다보니 입맞에 맞게 바꾸려면 기본 기능부터 잘 알아야겠다는 생각이 들었다.
나름 전자정부에서도 security의 기능을 커스텀하여 사용하고 있다보니 분석하는데 많은 도움이 되어 정리해본다.


security architecture

우선 톰캣과 같은 서블릿 컨테이너에서 MVC구조를 사용할때 처리되는 흐름도이다. 보면 client가 요청을 보내면 filter가 동작하고 그다음으로 servlet이, 그리고 interceptor, controller가 실행되는 흐름을 가지고 있다. 여기서 spring security를 사용하게 되면 위의 filter의 순서에서 실행되게 된다.

이는 security의 아키텍처로 FilterChain의 영역에서 여러 filter들을 사용하게 된다. 이때 위의 그림을 보면 DelegatinFilterProxy가 있는데 이는 원래 filter는 제일 위의 그림처럼 web context에 있는 영역이기 때문에 spring이 사용하는 기술을 사용할 수 없다. 이를 가능하게 하기위하여 해당 클래스를 이용한다.
이 클래스에서 springSecurityFilterChain 이름으로 생성된 빈을 찾아 요청을 위임하고, 실제로 보안 처리를 한다. security가 초기화될때 기본적인 filter들이 있는데 이를 관리하고 제어하는 역할을 한다. 보안 처리를 진행한 후 최종적으로 spring MVC의 DispatcherServlet에 전달하여 Servlet 처리를 하게 된다.

🔎 DispatcherServlet : http 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 controller에 위임해준다. 이전에는 web.xml을 통해서 처리했지만 이제는 해당 서블릿에서 핸들링하고 공통작업들을 처리해준다.

SecurityFilterChain

이러한 필터들은 인증(Authentication), 인가(Authorization) 혹은 protection 등과 같은 다양한 목적들로 사용할 수 있다.

인증과 인가 두 영어가 비슷하기도하고 '인증'과 '인가'라는 한국어 뜻도 좀 크게 와닿지 않아서 많이 헷갈렸던 개념이다. 근데 어떤 글에서 인증은 login으로, 인가는 permission으로 바꿔서 사용하자는 글을 보았는데 나에게는 더 확실하게 기억되는 개념이어서 이렇게 생각해봐도 좋을 것 같다.

인증은 인가를 확인하는 필터보다 먼저 선행되어야 한다. 하지만 일반적으로는 filter의 순서를 상세히 알 필요는 없다고 한다. (그것이 framework니까..끄덕)
spring boot 3버전부터는 security를 사용하는 방법도 확 달라졌기 때문에 이전 버전(WebSecurityConfigurerAdapter 상속하는 코드)은 생략한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults()) //(1)
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            ) //(3)
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults()); //(2)
        return http.build();
    }

}

(1) 첫번째로 csrfFilter를 호출한다.
(2) 두번째로 인증을 위한 BasicAuthenticationFilter 와 UsernamePasswordAuthenticationFilter 호출한다.
(3) 세번째로 인가를 위한 AuthorizationFilter를 호출한다.

이외에도 내가 커스텀한 filter를 호출할 수 있다. 이러한 커스텀 필터를 적용하고자 할때는

public class TenantFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("X-Tenant-Id"); 
        boolean hasAccess = isUserAllowed(tenantId); 
        if (hasAccess) {
            filterChain.doFilter(request, response); 
            return;
        }
        throw new AccessDeniedException("Access denied"); 
    }

}

...

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); 
    return http.build();
}

처럼 작성하면 된다.

🔎OncePerRequestFilter : 가끔 해당 클래스를 확장하여 이용하는 것을 볼 수 있는데, 이는 security는 api를 요청할때마다 계속 인증을 위한 필터가 동작하기 때문에 한번만 작동하고자 할때 이용하는 클래스이다.

Security Exception

이러한 필터들에서 에러가 발생하면 어떻게 될까? 이를 처리해주기 위한 filter가 바로 ExceptionTranslationFilter 이다. 인증에러(AuthenticationException)와 인가에러(AccessDeniedException)를 예외 처리용 역할을 하며, java와 http응답 사이의 브릿지 역할을 한다.
가장 마지막에 위치한 FilterSecurityInterceptor 필터에서 인증 예외가 발생하였을때나 인증을 하고 인가 처리 중 예외가 발생했을때 ExceptionTranslationFilter가 가로채서 예외 처리를 하게 된다.

이를 그림으로 보면 아래와 같다

위에서 인증에러와 인가에러는 간단하게 설명하자면,

  • AuthenticationException : 인증예외로 401오류 코드를 전달하고 security context를 초기화
  • AccessDeniedException : 인가예외로 AccessDeniedHandler에서 처리

여기서 만약 인증을 받지 않은상태로 /user api를 요청한다고 생각해보자. 이때 로그인을 하지 않았으므로 인증예외를 발생시킬 것 같은데 실제로 동작하는것을 보면 AccessDeniedException이 먼저 발생한다. 왜냐하면 익명사용자(anonymous)로 security가 객체를 생성하기 때문이다.
하지만 그렇다고해서 AccessDeniedHandler가 실행되진 않고 익명사용자인지 확인하여 다시 위의 그림인 Start Authentication 과정으로 이동하게 된다.

AuthenticationEntryPoint와 AccessDeniedHandler 인터페이스는 직접 구현할 수 있고, 이도 위와 같이 http.accessDeniedHandler(new AccessDeniedHandler()), http.AuthenticationEntryPoint(new AuthenticationEntryPoint())로 작성할 수 있다.

RequestCache

사용자가 인증이 없을때 인증이 필요한 리소스를 요청하게 되면 이후 인증을 성공했을 때 이 리소스를 다시 가져올 수 있는데 이때 RequestCacheAwareFilter가 이 객체를 사용하게 된다. 예를들어 처음 접근하고자 했던 주소 정보와 같은 리소스를 추출할 수 있다.

@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName("continue");
	http
		// ...
		.requestCache((cache) -> cache
			.requestCache(requestCache)
		);
	return http.build();
}

아래와 같이 사용할 수 있으며 만약 이러한 cache기능을 원하지 않는다면(사용자가 요청한 페이지가 아닌 모두 메인페이지로 통일하고자 할때) 이러한 저장 기능을 막는 NullRequestCache()가 있다.

[출처]

profile
개발 그렇게 하는거 아닌데의 그렇게를 맡고있습니다

0개의 댓글