- java
- SSL
- 1차원 배열
- spring mvc
- mysql
- PYTHON
- ORM
- 프로그래머스
- @transactional
- Git
- 스프링
- session
- join
- string
- AWS
- 자바
- 데이터베이스
- jpa
- Docker
- Django
- select
- spring
- DI
- spring boot
- 문자열
- spring security 6
- 스프링부트
- springboot
- sql
- nginx
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 공식 문서를 번역했습니다. 필요에 의해 설명을 추가한 부분도 있습니다.
🔖목차🔖
Spring Security Architecture
Spring Security는 인증, 검증, 권한 부여를 편리하게 할 수 있도록 도와주는 Spring Framework의 라이브러리이다.
Filter
Spring Security는 Spring MVC의 `Dispatcher Servlet` 보다 앞인 `Servlet Container` 내부에 존재하는 `Filter`에서 시작한다.
따라서 `Filter`의 역할을 먼저 찾아보는 것이 이해에 도움 된다. 아래 그림은 단일 HTTP 요청에 대한 핸들러의 일반적인 계층을 보여준다.
클라이언트가 애플리케이션으로 요청을 보내면 컨테이너는 요청 URI 경로를 따라 `HttpSevletRequest`를 처리해야 하는 필터 인스턴스와 서블릿을 포함하는 `FtilerChain`을 생성한다.
하나 이상의 `Filter`를 사용하여 다음을 할 수 있다.
- 다운 스트림 필터 인스턴스(Downstream Filter Instance) 또는 서블릿이 호출되지 않도록 한다. 이 경우 필터는 일반적으로 `HttpServletResponse`를 작성한다.
- 다운 스트림 필터 인스턴스 및 서블릿에서 사용하는 `HttpServletRequest` 또는 `HttpServletResponse`를 수정한다.
Note 자신보다 먼저 호출되는 필터를 `Upstream Filter`, 나중에 호출되는 필터를 `Downstream Filter`라고 한다.
여러 개의 필터가 연결되어 이루어진 `FilterChain`은 서블릿 컨테이너 내부에서 클라이언트의 요청이 서블릿에 도달하기 전에 인가 처리, 권한 부여, 요청 로깅, 예외 처리 등과 같은 작업을 처리한다.
아래 코드는 `FilterChain` 예제이다. 모든 필터를 통과하고 애플리케이션에서 할 일을 마친 후 바로 클라이언트로 응답이 가는 것이 아닌, 다시 필터를 통해서 돌아가는 것을 알 수 있다.
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의 `ApplicationContext` 사이를 연결해 주는 `DelegatingFilterProxy`라는 필터 구현체를 제공한다. 일반적인 필터들은 Spring에서 정의된 `Bean`을 인지할 수 없지만, `DelegatingFilterProxy`는 필터이면서 `Bean`에게 작업을 위임할 수 있다.
아래 코드는 `DelegatingFilterProxy`의 의사 코드(pseudo code)이다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
위 코드의 첫 줄은 `Bean`으로 등록된 필터를 Lazy하게 가져오는 작업을 수행한다. `DelegatingFilterProxy`는 필터 빈 인스턴스 조회를 지연시킬 수 있는 장점을 갖고 있다.
필터 인스턴스는 컨테이너가 시작되기 전에 등록되어야 한다. 그러나 Spring은 일반적으로 `ContextLoadListener`를 사용하여 `Spring Bean`을 로드하는데, 이는 필터 인스턴스를 등록한 후에 수행되므로 필터 빈 인스턴스 조회를 지연시킬 수 있다.
FilterChainProxy
SpringSecurity가 제공하는 `FilterChainProxy`는 `SecurityFilterChain`을 통해 많은 필터 인스턴스에 위임할 수 있게 하는 필터이다. 또한 `FilterChainProxy`는 `Bean`이기 때문에 일반적으로 `DelegatingFilterProxy`로 감싸지게 된다.
SecurityFilterChain
`FilterChainProxy`는 현재 요청에 대해 호출되어야 하는 Spring Security 필터 인스턴스를 결정하기 위해 `SecurityFilterChain`을 사용한다.
`SecurityFilterChain` 내부의 `Security Filter`들은 일반적으로 `Bean`이지만, `DelegatingFilterProxy`가 아닌 `FilterChainProxy`에 등록된다. `FilterChainProxy`는 서블릿 컨테이너 또는 `DelegatingFilterProxh`에 직접 등록하는 것보다 여러 가지 이점을 제공한다.
- Spring Security가 서블릿 기반으로 제공하는 모든 기능의 시작점이므로, 트러블 슈팅 시 `FilterChainProxy`에 디버그 포인트를 추가하기 좋다.
- `FilterChainProxy`는 Spring Security 사용의 핵심이다. 이는 `SecurityContext`를 초기화하여 메모리 누수를 피하거나, `HttpFirewall`을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호하도록 한다.
- `SecurityFilterChain`이 언제 호출되어야 하는지 보다 유연하게 결정할 수 있도록 한다. 서블릿 컨테이너 안에서 필터 인스턴스는 URL만을 기반으로 호출되지만, `FilterChainProxy`는 `RequestMatcher` 인터페이스를 사용하여 `HttpServletRequest`의 모든 것을 기반으로 호출을 결정할 수 있다.
다중 SecurityFilterChain
`FilterChainProxy`는 멤버 변수로 `List<SecurityFliterChain> filterChains`를 가직 ㅗ있어 여러 개의 `SecurityFilterChain`을 모두 저장할 수 있으며, 현재 요청에 따라 어떤 `SecurityFilterChain`을 사용할지 결정한다(위 FilterChainProxy의 이점 3번).
아래 그림을 보면, `SecurityFilterChain 0`은 총 3개의 필터로, `SecurityFilterChain n`은 4 개의 필터로 구성되어 있는 것을 확인할 수 있다. 이를 통해 각 `SecurityFilterChain`은 유니크하고 독립적으로 구성될 수 있음을 알 수 있다. `SecurityFilter`를 통해 검사할 필요가 없는 요청에 대해서는 0개의 필터로 구성된 `SecurityFilterChain`을 적용할 수 있다.
아래 코드는 실제 Spring Security의 `FilterChainProxy` 코드 중 일부이다.
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();
}
`while`문의 조건을 보면, 요청과 일치하는 SecurityFilterChain의 차례가 되면 반복문이 종료되는 것을 확인할 수 있다. 이를 통해 가장 먼저 매칭되는 단 하나의 `SecurityFilterChain`만이 호출됨을 알 수 있다. 위에서 언급한 것처럼 순서가 매우 중요하다는 의미이다.
위 그림을 다시 보면, `SecurityFilterChain 0`는 `/api/**`에 대해, `SecurityFilterChain n`은 `/**`에 매칭되도록 설정되어 있다. 예를 들어, `/api/message/` URL이 요청되면 `SecurityFilterChain 0`이 가장 처음으로 매칭되므로 `SecurityFilterChain 0`만이 호출되어 이를 통한 검증이 이루어지게 된다.
Security Filters
`Security Filter`는 `SecurityFilterChain API`를 사용하여 `FilterChainProxy`에 삽입된다 `Security Filter`는 다양한 목적(인증, 인가, 악용 방지 등)으로 사용될 수 있으며, 적시에 호출되도록 특정 순서로 실행된다.
예를 들어, 인증(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 출력하기
추가한 필터가 `Security Filter` 목록에 있는지 확인하고 싶을 때와 같이, 특정 요청에 대해 호출되는 `Security Filter` 목록 확인이 필요할 때가 있다.
필터 목록은 `INFO` 레벨 수준에서 출력되므로, 다음과 같은 결과를 콘솔창에서 볼 수 있다.
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에 사용자 정의 필터 추가하기
대부분 기본 필터로도 충분하지만, 커스텀/사용자 정의 필터가 필요할 수도 있다. 예를 들어 `tenant id header`를 가져오는 필터를 추가하여 현재 사용자가 해당 `tenant`에 액세스 할 수 있는지 확인하려고 한다고 가정해 보자. 이전 설명에서 이미 필터를 추가할 위치에 대한 단서를 제공했는데, 현재 사용자를 알아야 하므로 인증 필터(authentication filter) 다음에 해당 기능을 수행하는 필터를 추가해야 한다.
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 `Filter`를 구현하는 대신 `OncePerRequestFilter`를 구현해도 된다. 이는 요청 당 한 번만 호출되는 필터의 베이스 클래스이며, `HttpServletRequest` 및 `HttpServletResponse` 매개 변수와 함께 `doFilterInternal` 메서드를 제공한다.
이제 위 필터를 `security filter chain`에 등록해야 한다.
`HttpSecurity#addFilterBefore`를 사용하여 사용자 정의 필터인 `TenantFilter`를 `AuthorizationFilter` 앞에 등록한다.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
return http.build();
}
`TenantFilter`가 인증 필터 뒤에 호출되도록 `AuthorizationFilter` 앞에 해당 필터를 등록했다.
`HttpSecurity#addFilterAfter`를 사용하여 특정 필터 뒤에 호출되도록 하거나 `HttpSecurity#addFilterAt`을 사용하여 `filter chain`의 특정 필터 위치에 필터를 등록할 수 있다.
`Filter`를 구현한 사용자 정의 필터를 `@Bean` 또는 `@Component`을 사용하여 `Spring Bean`으로 등록할 때 주의해야 할 점이 있다.
Spring Boot는 해당 커스텀 필터를 자동으로 임베디드 컨테이너에 등록한다. 이로 인해 필터가 다른 순서로 두 번 호출되게 할 수도 있다; 컨테이너에 의해 한 번, Spring Security에 의해 한 번.
하지만 해당 커스텀 필터가 의존관계 주입이 필요한 경우에는 `Spring Bean`으로 명시적으로 선언해야 한다. 이는 위에서 언급한 `Security Filter`와 `Spring Bean` 사이의 초기화 순서와 연관되어 있다.
- Spring Security Filter 초기화
- `Filter`나 `OncePerRequestFilter`를 구현한 커스텀 필터는 애플리케이션 시작 프로세스에서 매우 일찍 초기화되고 등록된다. 메인 애플리케이션 컨텍스트가 완전히 로드되고 모든 `bean`이 인스턴스화되기도 전에.
- Spring Bean 초기화
- 커스텀 필터가 필요로 하는 의존관계를 포함한 `Spring Bean`은 `security filter`가 등록된 후 시작 프로세스 후반에 초기화되고 인스턴스화된다.
- 의존관계 주입
- 커스텀 필터가 의존관계 주입을 필요로 하면 Spring은 의존관계 주입 없이는 필터를 자동으로 인스턴스화하고 등록할 수 없다. 이는 Spring Security가 해당 필터를 초기화하고 등록할 때 아직 의존 관계는 사용할 수 없기 때문이다.
- Spring Bean으로 등록하기
- 커스텀 필터를 `Spring Bean`으로 명시적으로 등록하면, Spring은 커스텀 필터 `bean`의 생명 주기를 관리하고 필터의 의존 관계를 올바르게 해결할 수 있게 된다. 메인 애플리케이션 컨텍스트가 완전히 로드될 때 Spring은 커스텀 필터 `bean`을 인스턴스화하고 필요한 의존 관계를 주입할 수 있다.
의존 관계 주입을 활용하고 중복 호출을 피해야 하는 경우, `FilterRegistrationBean`을 `bean`으로 선언하고 해당 `enabled` 속성을 `false`로 설정하여 컨테이너에 등록하지 않도록 Spring Boot에게 지시할 수 있다.
@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`는 `AccessDeniedException`과 `AuthenticationException`을 HTTP Response로 변환할 수 있다. 해당 예외 필터는 `security filter` 중 하나로 `FilterChainProxy`에 삽입된다.
- `ExceptionTranslationFilter`가 `FilterChain.doFilter(request, response)`를 호출한다.
- 인증되지 않은 사용자 거나 `AuthenticationException`이 발생한 경우, 인증 프로세스를 시작한다.
- SecurityContextHolder를 비운다.
- 인증이 성공하면 원래의 요청을 다시 실행할 수 있도록 `HttpServletRequest`를 저장해둔다.
- `AuthenticationEntryPoint`는 클라이언트로부터 자격 증명(request credentails)을 요청하는 데 사용된다. 예를 들어 로그인 페이지로 리디렉션 되거나 `WWW-Authenticate` 헤더를 보낼 수도 있다.
- 2번이 아닌 `AccessDeniedException`인 경우, 접근이 거부된다(Access Denied). 거부된 접근을 처리하기 위해 `AccessDeniedHandler`가 호출된다.
Note 애플리케이션이 `AccessDeniedException` 또는 `AuthenticationException`을 던지지 않으면, `ExceptionTranslationFilter`는 아무것도 하지 않는다.
아래는 `ExceptionTranslationFilter` 의사코드이다.
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에서는 `RequestCahe` 구현체를 사용한 `HttpServletRequest`를 저장하여 이 작업을 수행한다.
🔹RequestCache
`HttpServletRequest`는`RequestCache`에 저장된다. 사용자가 인증을 성공할 때 `RequestCache`는 원래의 요청을 다시 실행하는 데 사용된다. `RequestCacheAwareFilter`는 사용자가 인증한 후 `RequestCache`를 사용하여 저장된 `HttpServletRequest`를 가져오고, `ExceptionTranslationFilter`는 사용자를 로그인 엔드포인트로 리디렉션 하기 전에 `AuthenticationException`을 감지한 후 `RequestCache`를 사용하여 `HttpServletRequest`를 저장한다.
기본적으로 하나의 `HttpSessionRequestCache`가 사용된다. 아래의 코드는 `continue`라는 매개변수가 있는 경우, 저장된 요청에 대해 `HttpSession`을 확인하는 데 사용되는 `RequestCache` 구현체를 사용자 정의(customize)하는 방법을 보여 준다.
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
🔹요청이 저장되지 않도록 방지하기
세션에 인증되지 않은 요청을 저장하지 않고, 대신 브라우저나 DB로 옮겨 저장하고 싶을 수도 있다. 또는 로그인하지 않은 사용자가 방문하려는 페이지 대신 홈페이지로 리디렉션 하는 것을 원하면 이 기능을 사용하지 않을 수도 있다.
이 경우에는 `NullRequestCache` 구현체를 사용하면 된다.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
🔹RequestCacheAwareFilter
`RequestCacheAwareFilter`는 `RequestCache`를 사용하여 원래의 요청을 다시 실행한다.
Logging
Spring Security는 모든 보안 관련 이벤트에 대해 `DEBUG` 및 `TRACE` 레벨에서 포괄적인 로깅을 제공한다. Spring Security는 요청이 거부된 자세한 이유를 response body에 담지 않기 때문에 이는 애플리케이션을 디버깅할 때 유용하다. 401/403 에러가 발생하면 무슨 일이 일어나고 있는지 이해에 도움이 되는 로그를 발견할 수도 있다.
logging:
level:
org:
springframework:
security: TRACE
'Java > 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 |