- nginx
- spring boot
- 스프링부트
- java
- springboot
- DI
- PYTHON
- hibernate
- spring
- sql
- 스프링
- 데이터베이스
- 1차원 배열
- string
- join
- AWS
- @transactional
- spring security 6
- ORM
- 프로그래머스
- 문자열
- jpa
- 자바
- Docker
- spring mvc
- select
- mysql
- SSL
- Django
- static
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
개발하는 자몽
Spring Security 6 - Architecture 본문
Spring Security 6 - Architecture 공식 문서를 번역했습니다. 필요에 의해 설명을 추가한 부분도 있습니다.
Architecture :: Spring Security
The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec
docs.spring.io
🔖목차🔖
Spring Security Architecture
Spring Security는 인증, 검증, 권한 부여를 편리하게 할 수 있도록 도와주는 Spring Framework의 라이브러리이다.
Filter
Spring Security는 Spring MVC의

따라서

클라이언트가 애플리케이션으로 요청을 보내면 컨테이너는 요청 URI 경로를 따라
하나 이상의
- 다운 스트림 필터 인스턴스(Downstream Filter Instance) 또는 서블릿이 호출되지 않도록 한다. 이 경우 필터는 일반적으로
HttpServletResponse 를 작성한다. - 다운 스트림 필터 인스턴스 및 서블릿에서 사용하는
HttpServletRequest 또는HttpServletResponse 를 수정한다.
Note 자신보다 먼저 호출되는 필터를
여러 개의 필터가 연결되어 이루어진
아래 코드는
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
필터는 다운 스트림 필터 인스턴스와 서블릿에만 영향을 미치므로 각 필터가 호출되는 순서는 매우 중요하다.
DelegatingFilterProxy
Spring은 서블릿 컨테이너 생명 주기와 Spring의

아래 코드는
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
위 코드의 첫 줄은
필터 인스턴스는 컨테이너가 시작되기 전에 등록되어야 한다. 그러나 Spring은 일반적으로
FilterChainProxy
SpringSecurity가 제공하는

SecurityFilterChain

- Spring Security가 서블릿 기반으로 제공하는 모든 기능의 시작점이므로, 트러블 슈팅 시
FilterChainProxy 에 디버그 포인트를 추가하기 좋다. FilterChainProxy 는 Spring Security 사용의 핵심이다. 이는SecurityContext 를 초기화하여 메모리 누수를 피하거나,HttpFirewall 을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호하도록 한다.SecurityFilterChain 이 언제 호출되어야 하는지 보다 유연하게 결정할 수 있도록 한다. 서블릿 컨테이너 안에서 필터 인스턴스는 URL만을 기반으로 호출되지만,FilterChainProxy 는RequestMatcher 인터페이스를 사용하여HttpServletRequest 의 모든 것을 기반으로 호출을 결정할 수 있다.
다중 SecurityFilterChain
아래 그림을 보면,

아래 코드는 실제 Spring Security의
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
Iterator var3 = this.filterChains.iterator();
SecurityFilterChain chain;
do {
if (!var3.hasNext()) {
return null;
}
chain = (SecurityFilterChain)var3.next();
if (logger.isTraceEnabled()) {
++count;
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
}
} while(!chain.matches(request));
return chain.getFilters();
}
위 그림을 다시 보면,
Security Filters
예를 들어, 인증(Authentication)을 수행하는 필터는 권한 부여(Authorization)를 수행하는 필터보다 호출되어야 한다. 두 필터가 반대의 순서로 호출되면 정상적인 작동을 보장할 수 없다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults()) //CsrfFilter
.authorizeHttpRequests(authorize -> authorize //AuthorizationFilter
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults()) //BasicAuthenticationFilter
.formLogin(Customizer.withDefaults()); //UsernamePasswordAuthenticationFilter
return http.build();
}
}
위와 같이 설정할 경우, 아래와 같은 순서대로 필터가 실행될 것이다.
- CSRF 공격에 대항하여
CsrfFilter 가 호출된다. - 요청을 인증하기 위해 인증 필터(authentication filters)가 호출된다.
- 요청을 인가하기 위해
AuthorizationFilter 가 호출된다.
Security Filter 출력하기
추가한 필터가
필터 목록은
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
뿐만 아니라 각 요청에 대해 각 필터의 호출을 출력하도록 애플리케이션을 구성할 수도 있다. 이렇게 하면 추가한 필터가 특정 요청에 대해 호출되는지 확인하거나, 예외가 발생하는 위치를 찾는 데 유용하다. 이를 위해 보안 이벤트(security event)를 기록하도록 애플리케이션을 구성할 수 있다.
Filter Chain에 사용자 정의 필터 추가하기
대부분 기본 필터로도 충분하지만, 커스텀/사용자 정의 필터가 필요할 수도 있다. 예를 들어
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
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"); (1)
boolean hasAccess = isUserAllowed(tenantId); (2)
if (hasAccess) {
filterChain.doFilter(request, response); (3)
return;
}
throw new AccessDeniedException("Access denied"); (4)
}
}
request header 에서tenant id 를 가져온다.- 현재 사용자가 가져온
tenant id 에 대해 접근 권한이 있는지 확인한다. - 권한이 있으면,
chain 에 남은 필터들을 호출한다. - 권한이 없으면,
AccessDeniedException 을 던진다.
Note
이제 위 필터를
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
Spring Boot는 해당 커스텀 필터를 자동으로 임베디드 컨테이너에 등록한다. 이로 인해 필터가 다른 순서로 두 번 호출되게 할 수도 있다; 컨테이너에 의해 한 번, Spring Security에 의해 한 번.
하지만 해당 커스텀 필터가 의존관계 주입이 필요한 경우에는
- Spring Security Filter 초기화
Filter 나OncePerRequestFilter 를 구현한 커스텀 필터는 메인 애플리케이션 컨텍스트가 완전히 로드되고 모든bean 이 인스턴스화되기도 전에 애플리케이션 시작 프로세스에서 매우 일찍 초기화되고 등록된다. - Spring Bean 초기화
커스텀 필터가 필요로 하는 의존관계를 포함한Spring Bean 은security filter 가 등록된 후 시작 프로세스 후반에 초기화되고 인스턴스화된다. - 의존관계 주입
커스텀 필터가 의존관계 주입을 필요로 하면 Spring은 의존관계 주입 없이는 필터를 자동으로 인스턴스화하고 등록할 수 없다. 이는 Spring Security가 해당 필터를 초기화하고 등록할 때 아직 의존 관계는 사용할 수 없기 때문이다. - Spring Bean으로 등록하기
커스텀 필터를Spring Bean 으로 명시적으로 등록하면, Spring은 커스텀 필터bean 의 생명 주기를 관리하고 필터의 의존 관계를 올바르게 해결할 수 있게 된다. 메인 애플리케이션 컨텍스트가 완전히 로드될 때 Spring은 커스텀 필터bean 을 인스턴스화하고 필요한 의존 관계를 주입할 수 있다.
의존 관계 주입을 활용하고 중복 호출을 피해야 하는 경우,
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
OncePerRequestFilter 와 중복 호출에 관련된 개인적인 고찰🤔OncePerRequestFilter 를 구현한 커스텀 필터의 경우에는FilterRegistrationBean 빈을 선언하고enabled 속성을false 로 설정할 필요는 없는 것 같다.OncePerRequestFilter 는 한 요청 당 한 번만 호출되도록 Spring Security에서 내부적으로 처리하기 때문이다.
실제로 해당 커스텀 필터가 호출될 때 로그를 찍어보면 한 번만 출력되는 것을 확인할 수 있다.
Security Exception 처리하기

ExceptionTranslationFilter 가FilterChain.doFilter(request, response) 를 호출한다.- 인증되지 않은 사용자 거나
AuthenticationException 이 발생한 경우, 인증 프로세스를 시작한다.- SecurityContextHolder를 비운다.
- 인증이 성공하면 원래의 요청을 다시 실행할 수 있도록
HttpServletRequest 를 저장해둔다. AuthenticationEntryPoint 는 클라이언트로부터 자격 증명(request credentails)을 요청하는 데 사용된다. 예를 들어 로그인 페이지로 리디렉션 되거나WWW-Authenticate 헤더를 보낼 수도 있다.
- 2번이 아닌
AccessDeniedException 인 경우, 접근이 거부된다(Access Denied). 거부된 접근을 처리하기 위해AccessDeniedHandler 가 호출된다.
Note 애플리케이션이
아래는
try {
filterChain.doFilter(request, response); //(1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); //(2)
} else {
accessDenied(); //(3)
}
}
- Filter에서 설명한 것처럼
FilterChain.doFilter(request, response) 는 나머지 애플리케이션을 호출하는 것과 동일하다. 이는 애플리케이션의 다른 부분(FilterSecurityIntercepter 또는 method security)이AuthenticationException 또는AccessDeniedException 을 던지는 경우, 여기서catch 되어 처리될 수 있음을 의미한다. - 사용자가 인증되지 않았거나
AuthenticationException 이 발생한 경우, 인증 프로세스를 시작한다. - 2번이 아니라면, 접근이 거부된다(Access Denied).
인증 간 Request 저장하기
어떤 요청이 인증되지 않았는데 인증이 필요한 자원에 관한 것일 때 인증 성공 후 다리 요청하기 위해 인증된 자원에 대한 요청을 저장해두어야 한다. Spring Security에서는
🔹RequestCache
기본적으로 하나의
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
🔹요청이 저장되지 않도록 방지하기
세션에 인증되지 않은 요청을 저장하지 않고, 대신 브라우저나 DB로 옮겨 저장하고 싶을 수도 있다. 또는 로그인하지 않은 사용자가 방문하려는 페이지 대신 홈페이지로 리디렉션 하는 것을 원하면 이 기능을 사용하지 않을 수도 있다.
이 경우에는
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
🔹RequestCacheAwareFilter
Logging
Spring Security는 모든 보안 관련 이벤트에 대해
logging:
level:
org:
springframework:
security: TRACE
'Java & Kotlin > Spring' 카테고리의 다른 글
[Spring Security] 역할/권한 구현하기 (0) | 2024.06.14 |
---|---|
[Spring Security] 토큰 기반 로그인/로그아웃 구현하기 (0) | 2024.06.08 |
[TIL / Spring] 설정 파일과 프로필 (0) | 2024.04.26 |
[Spring Data JPA] TransactionRequiredException (0) | 2023.08.19 |
[QueryDSL] SpringBoot 3.1.0, QueryDSL 5.0.0 build.gradle (0) | 2023.08.12 |