스프링 시큐리티 Spring Security
Spring Security는 Spring기반 애플리케이션을 보호하기 위한 표준으로 직접 커스텀이 가능한 인증 및 Access Control 프레임워크이다. 이는 Java 애플리케이션에 인증과 인가(권한 부여)를 모두 제공하는 데 중점을 뒀다. Spring Security는 다른 Spring프로젝트와 마찬가지로 사용자 요구에 맞춰 쉽게 확장할 수 있다는 점이 최대 장점이다.
특징
- 인증 및 인가(권한 부여)에 대해 포괄적이고 확장 가능한 지원
- session fixation(세션 고정 공격), clickjacking(클릭재킹), csrf(cross site request forgery; 사이트 간 요청 위조) 등과 같은 공격으로부터 보호
- Servlet API 통합
- Spring Web MVC와 선택적(Optional) 통합
- 등등...
스프링 부트 자동 구성 Spring Boot Auto Configuration
- Spring Security의 기본 구성을 활성화하여 springSecurityFilterChain라는서블릿 Filter 빈을 생성한다.
- 이 빈은 응용 프로그램 내의 모든 보안(애플리케이션 URL 보호, 제출된 사용자 이름 및 암호 validation, 로그인 양식으로 리디렉션 등)을 담당한다.
- 콘솔에 기록되는 무작위로 생성되는 암호와 user라는 username을 가진 UserDetailsService Bean을 생성한다.
- 모든 요청에 대해 서블릿 컨테이너에 빈의 이름이 springSecurityFilterChain인 Filter를 등록한다.
우리가 개발을 할 때는 많은 설정을 필요로 하지 않지만 백단에서는 많은 작업이 수행된다.
- 애플리케이션과의 모든 상호 작용을 위해 인증된 사용자 필요
- 기본 로그인 양식 생성
- 사용자가 username인 ‘user’와 인증하기 위해 콘솔에 입력되는 비밀번호로 form-기반 인증 제공
- BCrypt로 암호 저장소 보호
- 사용자 로그아웃 기능 제공
- CSRF 공격 방지
- 세션 고정 공격(session fixation) 보호
- 보안 헤더 통합
- 보안 요청을 위한 Http Strict Transport Security
- X-Content-Type-Options 통합
- 캐시 제어(나중에 애플리케이션 재정의하여 정적 리소스를 캐싱할 수 있음)
- X-XSS-Protection 통합
- 클릭재킹을 방지하는 X-Frame-Options 통합
- login(), logout()과 같은 Servlet API 메서드와 통합
Spring Security 아키텍처
A Review of Filters
스프링 시큐리티가 지닌 서블릿은 서블릿 Filter들을 기반으로 하므로 일반적으로 Filter들의 역할을 먼저 살펴보는 것이 좋다. 아래 그림은 HTTP 요청이 들어왔을 때 그에 따른 핸들러들의 전형적인 계층 흐름을 보여준다. 클라이언트가 애플리케이션에 요청을 보내고 컨테이너는 요청 URI의 경로를 기반으로 HttpServletRequest를 처리해야 하는 필터들과 서블릿을 포함하고 있는 FilterChain을 생성한다.
Spring MVC가 사용하는 Servlet은 DispatcherServlet이다. Servlet 인스턴스 하나당 HttpServletRequest, HttpServletResponse 인스턴스 각 하나씩 들고있지만 두 개 이상의 필터를 사용하면 다음과 같이 유동적은 흐름을 수행할 수 있다.
- 특정 조건을 만족하는 경우 다운스트림(Cliet → Servlet 방향) 필터 또는 서블릿이 호출되지 않도록 할 수 있다. 그렇게 해당 서블릿 필터가 속하는 지점에서 멈출 경우 필터는 HttpServletResponse를 작성한다.
- 필터에서 아래에 있는 다운스트림 필터 및 서블릿으로 이동하면서 각 사용하는 HttpServletRequest 또는 HttpServletResponse를 연동하여 수정할 수 있다.
Filter의 진정한 면모는 스트림을 통과하며 동작하는 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라는 필터 구현을 제공한다. 서블릿 컨테이너는 자체 표준을 사용하여 필터를 등록할 수 있지만 스프링에서 정의한 빈을 인식하지 못한다. 그런데 DelegatingFilterProxy는 스프링 빈을 관리하는 표준 서블릿 필터를 구현하고 있으며 내부에 위임대상(FilterChainProxy)을 갖고있다.그래서 스프링 빈을 인식할 수 있도록 Filter를 구현하는 스프링 시큐리티 빈에 이 모든 작업을 위임한다.
- 즉, DelegatingFilterProxy는 표준 서블릿 필터인데 Spring ApplicationContext가 관리하는 FilterChainProxy(Filter Bean)을 갖고 있다. 이 객체 안에서 Security와 관련된 일들이 벌어지게 되므로 스프링 빈 인식이 가능하게 Security Filter에 등록하여 브릿징 역할을 수행해준다고 말할 수 있다.
다음은 DelegatingFilterProxy가 FilterChain에 어떻게 들어있는지 보여주는 그림이다. Bean Filter(0)은 FilterChainProxy이 된다.
DelegatingFilterProxy는 ApplicationContext에서 Bean Filter(0)을 찾은 다음 Bean Filter(0)을 호출한다. DelegatingFilterProxy의 의사 코드는 아래에서 볼 수 있다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazy하게 스프링 빈으로 등록되어 있는 필터를 가져온다.
// DelegatingFilterProxy 예에서는 delgate 대상은 Bean Filter0 인스턴스(FilterChainProxy)이다.
Filter delegate = getFilterBean(someBeanName);
// (FilterChainProxy)을 Spring Bean에 작업을 위임한다.
delegate.doFilter(request, response);
}
FilterChainProxy
Spring Security의 모든 서블릿 지원은 FilterChainProxy에 포함되어 있다. FilterChainProxy는 SecurityFilterChain을 통해 많은 Filter 인스턴스들에게 위임을 허용하는 Spring Security가 제공하는 특별한 Filter이다.
- FilterChainProxy는 Bean이므로 일반적으로 DelegatingFilterProxy에 래핑된다.
SecurityFilterChain
SecurityFilterChain은 FilterChainProxy가 어떤 요청에 대해 호출해야 하는 Spring Security Filter를 결정할 때 사용된다.
Security Filter들이 FilterChainProxy에 등록하면 좋은 이유
SecurityFilterChain의 Security Filter들은 다 전형적인 Bean이지만 DelegatingFilterProxy을 대신하여 FilterChainProxy에 등록된다. 이러한 FilterChainProxy에 등록하는 방식은 많은 이점을 제공한다.
- Spring Security에서 동작하는 모든 서블릿의 시작점이다. 이러한 이유로 Spring Security의 서블릿 지원에 대한 문제가 발생했을 때 디버그 지점을 FilterChainProxy에서 시작하면 좋다.
- FilterChainProxy는 Spring Security 사용의 핵심이기 때문에 선택적으로 보이지 않는 작업을 수행할 수 있다. 예를 들어, 메모리 누수를 방지하기 위해 SecurityContext를 지울 수 있다. 또한 Spring Security의 HttpFirewall을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호한다.
- SecurityFilterChain이 호출되어야 하는 시기를 결정하는 데 더 많은 유연성을 제공한다. 서블릿 컨테이너에서 필터는 URL만을 기반으로 호출되지만 FilterChainProxy는 RequestMatcher 인터페이스를 활용하여 HttpServletRequest의 모든 항목을 기반으로 호출을 결정할 수 있다.
Multiple SecurityFilterChain
실제로 FilterChainProxy를 사용하여 동작하는 SecurityFilterChain의 종류 결정할 수 있다. 이를 통해 애플리케이션의 여러 조각에 대해 완전히 별도의 구성을 제공할 수 있다.
위의 Multiple SecurityFilterChain 그림에서 FilterChainProxy는 사용해야 하는 SecurityFilterChain을 상황에 맞춰 결정한다. 여러 Chain들이 조건에 부합할 수 있지만 가장 첫 번째로 일치하는 SecurityFilterChain만 호출된다. 그래서 순서가 중요한 것이다.
- /api/messages/의 URL이 요청되면 먼저 SecurityFilterChain(0)의 /api/** 패턴과 일치하므로 SecurityFilterChain(0)만 호출된다.
- SecurityFilterChain(n)에서도 일치하지만 늦은 순서로 무시된다. switch문처럼 일치하면 바로 return된다고 보면 된다.
- /messages/의 URL이 요청되면 SecurityFilterChain0의 /api/** 패턴과 일치하지 않으므로 FilterChainProxy는 다른 SecurityFilterChain을 계속 시도한다. SecurityFilterChain 인스턴스와 일치하는 다른 인스턴스가 없다고 가정하면 마지막에 위치하는 모든 URL을 만족하는 SecurityFilterChain(n) 인스턴스가 호출된다.
Security Filters
보안 필터(Security Filter)는 SecurityFilterChain API를 사용하여 FilterChainProxy에 삽입된다. 필터의 순서가 중요하다. 일반적으로 Spring Security의 필터 순서를 알 필요는 없지만 순서를 아는 것이 유익할 경우도 있다.
Spring Security Filter 리스트 순서 보러가기
Security 예외 처리하기 (Handling Security Exceptions)
ExceptionTranslationFilter를 사용하면 AccessDeniedException 및 AuthenticationException을 HTTP 응답으로 변환할 수 있다. ExceptionTranslationFilter는 보안 필터 중 하나로 FilterChainProxy에 삽입된다.
동작과정
- 먼저 ExceptionTranslationFilter는 FilterChain.doFilter(request, response)를 호출하여 나머지 애플리케이션을 호출한다.
- 사용자가 인증되지 않았거나 AuthenticationException인 경우 인증을 시작한다.
- SecurityContextHolder가 지워진다.
- HttpServletRequest는 RequestCache에 저장된다. 사용자가 성공적으로 인증되면 RequestCache를 사용하여 원래 요청을 재생한다.
- AuthenticationEntryPoint는 클라이언트에서 자격 증명을 요청하는 데 사용된다. 예를 들어 로그인 페이지로 리다이렉션하거나 WWW-Authenticate 헤더를 보낼 수 있다.
- 그렇지 않고 AccessDeniedException이면 액세스가 거부된다. AccessDeniedHandler는 액세스 거부를 처리하기 위해 호출된다.
ExceptionTranslationFilter 코드
애플리케이션이 AccessDeniedException 또는 AuthenticationException을 throw하지 않으면 ExceptionTranslationFilter는 아무 작업도 수행하지 않는다. ExceptionTranslationFilter의 수도 코드는 다음과 같다.
- A Review of FIlters에서 다룬 FilterChain.doFilter(request, response)를 호출하는 것은 남은 애플리케이션 작업을 호출하는 것과 동일하다.
- 즉, 애플리케이션의 다른 부분(예: FilterSecurityInterceptor 또는 메서드 보안)이 AuthenticationException 또는 AccessDeniedException을 throw하면 여기에서 catch되고 처리된다.
- 사용자가 인증되지 않았거나 AuthenticationException인 경우 인증을 시작한다.
- 그렇지 않으면 액세스를 거부한다.
// ExceptionTranslationFilter 수도코드
try {
filterChain.doFilter(request, response); // 남은 애플리케이션 작업 호출
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
https://spring.io/projects/spring-security
https://docs.spring.io/spring-security/reference/servlet/getting-started.html