Spring Security 란? (Photocard 적용전 스터디)
본 글은 Spring.io에 Spring Security내용을 한국어로 재해석하여 작성한 글로 다소 표현이 틀렸을 수도 있음을 감안해주셨으면 좋겠습니다.
Spring Security는 강력하고 그리고 강력하게 framework 접근 제어 그리고 권한은 사용자화 할 수 있다.
Spring을 기초로한 어플리케이션 보안을 위한 표준입니다.
Spring Security는 Java 어플리케이션들의 권한 그리고 인가를 제공하는 것에 중심을 둔 프레임워크이다.
모든 스프링프로젝트는 사용자가 요구하는 것을 충족시키는 것, 확장하는 것이 spring security를 통하여 매우 쉽게 적용되는 것을 볼 수 있을 것이다.
특징
- 포괄적이고 확장적인 인증 그리고 인가를 지원한다.
- 세션 고정, 클릭재킹, 크로사이트 요청위조에 의한 공격을 방어한다.
- Servlet API 통합한다.
- Spring Web MVC 패턴과 선택적으로 통합할 수 있다.
Spring Security 작동원리 및 아키텍처
Fileters의 검사
Spring Security's Servlet은 Servlet Filters를 기초로 지원한다. 그래서 이것은 일반적으로 첫번째 필터의 역할로서 도와준다.
아래 이미지는 단일 HTTP request를 위한 전형적인 Handler 레이어 이미지를 보여준다.
클라이언트는 application에 요청을 보내면, container는 요청URL Path를 기반으로 Filter 인스턴스 그리고 HttpServletRequest를 처리해야하는 Servlet이 FilterChain에 포함되어있다. Spring MVC 애플리케이션에서, Servelet은 DispatcherServlet의 인스턴스이다. 대게 하나의 Servlet에서 단일 HttpServletRequest 그리고 HttpServletResponse를 다룬다. 그러나 Filter는 하나이상을 사용할 수 있으며 아래와 같은 것들을 다룰 수 있다.
1. Filter 인스턴스 그리고 servlet이 downStream으로 호출되는 것을 막는다.
2. downstream으로 사용되는 filter 인스턴스 그리고 Servlet이 HttpServletRequest 또는 HttpServletResponse로 수정됩니다.
Filter의 강력함은 Filter가 전달된 FilterChain으로부터 온다.
추가 정보)
Servlet Filter란?
Client로부터 Server로 요청이 들어오기 전에 서블릿을 거쳐서 필터링하는 것을 서블릿 필터라고 한다.
FilterChain UsageExample
public vopid doFilter(ServletRequest request, ServletResponse response) {
// do something before the rest of the application
chain.doFilter(request, response);
// do. something after the rest of the application
}
DelegatingFilterProxy
스프링은 스프링의 ApplicationContext 그리고 Servlet container 라이프사이클 사이를 연결하는 DelegatingFilterProxy라 붙여진 Filter를 제공합니다.
그 Servlet컨테이너는 자체 표준을 사용하여 Filter 인스턴스를 등록을 허락할 수 있다. 그러나 이것은 Spring-defined Beans에는 정의 되지 않는다. 표준 Servlet 컨테이너 메커니즘을 통해서 DelegatingFilterProxy를 등록할 수 있지만 Filter를 구현하는 모든것을 SpringBean에 위임할 수 있다.
DelegatingFilterProxy는 ApplicationContext로 부터 BeanFilter0 를 조회한 후 , BeanFilter0을 호출한다.
public void doFilter(ServletRequest request, ServletResponse, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
DelegatingFilterProxy의 다른 이점은 Filter bean 인스턴스 조회를 지연할 수 있다.
이것은 중요하다. 왜냐하면 container가 시작되기전에 컨테이너가 Filer 인스턴스를 등록해야하기 때문에 매우 중요하다.
Spring에서는 일반적으로 Spring Bean들을 가져올 때, ContextLoaderListener을 사용합니다.이것은 filter 인스턴스들이 등록되기전까지는 사용되지 않는다.
FilterChainProxy
Spring Security's Servlet 지원은 FilterChainProxy 내에 포함되어있다.
FilterChainProxy는 SecurityFilterChain을 통해서 많은 Filter들이 위임되는 것을 허락하는 특별한 Filter로 제공한다.
FilterChainProxy는 Bean 이므로 DelegatingFilterProxy에 일반적으로 감싸져 있다.
SecurityFilterChain
SecurityFilterChain은 SpringSecurity Filter 인스턴스는 현재 요청에대해 호출하고 FilterChainProxy에서 결정되어 사용된다.
SpringSecurityFilterChain에 Security Filter는 일반적인 Bean이다. 그러나 DelegatingFilterProxy대신에 FilterChainProxy가 등록된다.
FilterChainProxy는 ServletContainer 또는 DelegatingFilterProxy내에서 직접적으로 등록함으로써 수많은 이점을 제공한다.
첫 번째, SecurityFilter는 모든 Spring Security's 시작 위치에서 지원 제공한다.
이러한 이유로, 만약 Spring Security's Servlet support에 문제 생겼다면 FilterChaingProxy 위치에 디버그를 추가해라.
두번째, FilterChainProxy는 Spring Security에 사용자 중심이다. 이것은 선택사항으로 간주되지 않는 것들에 대한 작업을 수행할 수 있다. 예를 들어, 이것은 SecurityContext의 메모리 누수를 방지하기 위해 SecurityContext를 지워야한다. 또한 특정 유형의 공격으로부터 애플리케이션을 보호하기 위해 Spring Security의 HttpFirewall을 적용한다.
게다가, 이것은 SpringFilterChain을 호출해야할 때, 조금 더 유연하게 결정되는 것을 제공한다.
Sevlet Container에서 Filter인스턴스는 오직 URL기반으로만 호출된다. 그러나 FilterChainProxy는 RequestMatcher Interface를 사용에의해 HttpServletRequest의 모든 항목을 기반으로 결정할 수 있다.
다수의 SecurityFilterChain은 아래의 이미지와 같다.
다수의 SecurityFilterChain 에서는 FilterChaingProxy를 어떤 것을 사용할 지 결정해야 한다.
일치되는 첫번째 SecurityFilterChain만 호출된다. 만약 /api/message/의 URL 요청한다면 이것은 SecurityFilterChain0 패턴에서 /api/**가 일치 되어 호출된다.
/messages/의 URL이 요청되면 /api/**의 SecurityFilterChain0패턴과 일치하지 않으므로 FilterChainProxy는 계속해서 각 SecurityFilterChain을 시도하다. 결국 일치하는 SecurityFilterChain인스턴스가 없다고하면 /** FilterChain을 실행한다.
SecurityFilterChain0에서는 세개의 보안 filter인스턴스를 구성한다. 그러나, SecurityFilterChainN 은 4개의 보안 Filter인스턴스로 구성되어 있다. 이것은 각각의 SecurityFilterChain에 고유하며 독립적으로 설정할 수 있다는 것이다.
실제로 애플리케이션에서 Spring Security가 특정 요청을 무시하도록 원하는 경우 SecurityFilterChain에는 보안 필터 인스턴스가 0개있을 수 있다.
Security Filters
Security 필터는 SecurityFilterChain API를 사용하여 FilterChainProxy를 삽입할 수 있다.
이러한 필터는 인증, 권한 보여, 악용방지 그리고 더욱 다양한 이유로 사용될 수 있다.
필터들은 적시에 호출되도록 특정 순서로 실행된다. 예를들어, 인증을 수행하는 필터는 권한을 수행하는 필터보다 전에 수행해야한다.
일반적으로는 Spring Security's Filter를 알 필요는 없지만 순서를 알고 있을 때, 도움이 될수도 있다.
이러한 부분에 알고 싶다면 아래 링크로 들어가서 확인해보자.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain(HttpSecurity http) throws Exception {
http
.crsf(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
첫째, CRSF 공격으로부터 보호하기 위해 CrsfFilter가 호출된다.
둘째, 요청을 인증하기 위해 인증 필터가 호출된다.
셋째, 요청을 승인하기 위해 AuthorizationFilter가 호출됨.
필터체인에 사용자정의 추가
대부분의 경우, aplication 보안을 제공하기 위하여 기본적인 security filter를 사용하는 것만으로도 충분하다.
예를 들어,tenant Id를 가져오는 필터를 추가하고, 현재유저가 tenant에 접근하는것을 체크하고 싶다고 해보자.
이전 설명은 이미 필터를 추가할 위치에 대한 단서를 제공한다. 현재 사용자를 알아야하므로 인증 필터 뒤에 추가해야한다.
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) servletResposne;
String tenantId = request.getHeader("X-Tenant-Id");
boolean hashAccess = isUserAllowed(tenantId);
if(hasAccess) {
filterChain.doFilter(request, response);
return;
}
throw new AccessDeniedException("Access denied");
}
}
1. request Header로부터 tenant Id를 얻기
2. 현재 사용자가 tenantID에 액세스할 수 있는지 확인하기
3. 사용자에게 액세스 권한이 있으면 체인의 나머지 필터를 호출한다. 사용자에게 액세스 권한이 없으면 AccessDeniedException이 발생한다.
필터를 구현하는 것 대신에 OncePerRequestFilter로 확장할 수 있으며 HttpServletRequest 그리고 HttpServletResponse매개변수와 함께 doFilterInternal메서드를 제공한다.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), Authorization.Filter.class);
return http.build();
}
1. AuthorizationFilter앞에 TenantFilter를 추가하려면 HttpSecurity#addFilterBefore를 사용해라.
필터 앞에 추가하면 인증 필터 뒤에 AuthorizationFilter 필터가 호출되도록 할 수 있다.
TenantFilter를 사용하여 HttpSecurity#addFilterAfter 특정 필터 뒤에 필터를 추가하거나 HttpSecurity#addFilterAt 필터체인의 특정 필터 위치에 필터를 추가할 수 있다.
이제 TenantFilter 필터 체인에서 호출되어 현재 사용자가 Tenant ID에 액세스할 수 있는지 확인해야한다.
@Component 필터에 주석을 달거나 구성에서 bean으로 선언하여 필터를 Spring bean으로 선언할 때 주의해라
SpringBoot가 이를 내장된 컨테이너에 자동으로 등록하기 때문이다.
이러한 이유때문에 필터가 컨테이너에 한번, Spring Security에 의해 한번, 다른 순서로 두번 호출 될 수 있다.
예를 들어 종속성 주입을 활용하고 중복 호출을 피하기 위해 필터를 Spring bean으로 선언하려는 경우 빈을 선언하고 속성을 다음과 같이 설정하여 SpringBoot에 필터를 컨테이너에 등록하지 않도록 지시할 FilterRegistrationBean의 빈을 enabeld그리고 false로 설정한다.
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistration<>(filter);
registration.setEnabled(false);
return registration;
}
보안 예외 처리
Http 응답을 ExceptionTranslationFilter 변환할 수 있다.
.AccessDeniedException AuthenticationException
ExceptionTranslationFilter 보안필터중 하나로 FilterChainProxy에 삽입된다.
1. 먼저, 애플리케이션의 나머지 부분을 호출한다.
ExceptionTranslationFilter.FilterChain.doFilter(request, response)
2. 사용자가 인증후 AuthenticationException 또는 인증 되지 않았다면
- SecurityContextHolder는 지워진다.
- HttpServletRequest는 인증이 성공하면 재사용될 수 있도록 저장된다.
- AuthenticationEntryPoint는 클라이언트로부터 자격 증명을 요청을하는 데 사용된다. 예를 들어 로그인 페이지로 리디렉션되거나 WWW-Authenticate 헤더를 보낼 수 있다.
3. AccessDeniedException라면 접근이 거부된다. AccessDeniedHandler는 거부된 액세스를 처리하기 위해 호출된다.
애플리케이션이 AccessDeniedException 또는 AuthenticationException을 발생시키지 않는 경우 ExceptionTranslationFilters는 아무 작업도 수행하지 않는다.
try{
filterChain.doFilter(request, response);
} catch(AccessDeniedException | AuthenticationException ex) {
if(!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
}
FilterChain.doFilter(request, response)를 호출하는 것은 애플리케이션의 나머지 부분을 호출하는 것과 동일하다.
이는 애플리케이션의 다른 부분 (FilterSecurityInterceptor 또는 method security)이 AuthenticationException 또는 AccessDeniedException을 발생시키는 경우 여기에서 예외를 잡아 다루는 것을 의미한다.
만약 사용자가 인증되지 않았거나 AuthenticationException인 경우 인증이 거부됩니다.
인증 사이에 요청을 저장
보안 예외처리에서 설명한 것 처럼 인증 요청이 없을 때, 그리고 인증이 필요한 리소스에 대한 요청인 경우, 인증 성공 후 다시 요청하려면 인증된 리소스에 대한 요청을 저장해야 한다.
Spring Security에서는 RequestCache 구현을 사용하여 HttpServletRequest를 저장함으로써 이를 수행한다.
RequestCache
HttpServletRequest는 RequestCache를 저장한다. 유저가 성공적으로 인증을 하였을 때, RequestCache는 일반적인 request를 사용한다. RequestCacheAwareFilter는 RequestCache를 사용하여 HttpServletReqeuest를 저장하는 것이다.
기본적으로 HttpSessionRequestCache가 사용된다. 아래 코드는 Continue라는 매개변수가 있는 경우 저장된 요청에 대해 HttpSession을 확인하는데 사용되는 RequestCache구현을 사용자 정의하는 방법을 보여준다.
@Bean
DefaultSecurityFilterChain springSecurit(HttpSecurity http) throws Exception{
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
RequestCacheAwareFilter는 RequestServletRequest 를 저장하는 것에 RequestCache를 사용한다.
Logging
Spring Security는 DEBUG 및 Trace 수준에서 모든 보안 관련 이벤트에 대한 포괄적인 로깅을 제공한다.
이는 보안 조치를 위해 Spring Security가 요청이 거부된 이유에 대한 세부정보를 응답 부문에 추가하지 않기 때문에 애플리케이션 디버깅할 때, 매우 유용할 수 있다. 401 또는 403 오류가 발생하는 경우 무슨 일이 일어나고 있는지 이해하는 데 도움이 되는 로그 메시지를 찾을 가능성이 높다.
사용자가 CRSF 토근 없이 CRSF보호가 활성화된 리소스에 POST요청을 시도하는 경우을 가정해보자.
로그가 없으면 요청이 거부된 이유에 대한 설명없이 사용자에게 403오류가 표시된다. 그러나 Spring Security에 대한 로깅을 활성화하면 다음과 같은 로그 메시지가 표시된다.
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
CRSF 토큰이 누락되어 요청이 거부되는 것이 위에 로그를 보면 분명해진다.
위에와 같은 기능을 사용하려면 아래 코드를 추가하면 된다.
logging.level.org.springframework.security=TRACE // application.properties
// logback.xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- ... -->
</appender>
<!-- ... -->
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
</configuration>