Spring🍃/Spring Security🛡️

Spring Security Architecture(1~5) + Form Login + Servlet Authenticaion Architecture(1~3) 해석

wannaDevelopIt 2023. 1. 4. 15:58
728x90

https://docs.spring.io/spring-security/reference/servlet/architecture.html

 

Architecture :: Spring Security

Spring Security’s Servlet support is based on Servlet Filters, so it is helpful to look at the role of Filters generally first. The following image shows the typical layering of the handlers for a single HTTP request. The client sends a request to the ap

docs.spring.io

출처 : 스프링 공식문서 6.0.1 버전

 

제 이해를 위해 스프링 시큐리티 아키텍처 항목의 1 ~ 5번, Authentication 항목의 FormLogin, Servlet Authenticaion

Architecture항목의 1 ~ 3번 항목을 해석해 봤습니다.

 

편의상 FormLogin은 5-1번에, 이후 Servlet Authenticaion Architecture는 7-1, 7-2, 7-3번으로 정했습니다.

 

 

1. 필터 검토(A Review of Filters)

사용자의 요청이 있고, 가장 먼저 서블릿 컨테이너는 Filter 인스턴스를 가진 FilterChain과 HttpServleyRequest(요청 URL에 기반한)을 처리해야하는 Servlet을 만든다.

 

Spring MVC application에서는, 서블릿은 DispatcherServlet 대신 사용된다.

 

대부분, 하나의 서블릿은 각 한 개의 HttpServletRequest와 HttpServletResponse를 다룰 수 있다.

하나 이상의 Filter는 다음에 사용된다 :

- Filter 인스턴스 다운스트림이나 서블릿이 호출되는 것을 예방한다. 이 경우에 Filter는 통상 HttpServletResponse를 쓴다.

- 다운스트림 Filter 인스턴스와 서블릿에 사용된 HttpServletRequest나 HttpServletResponse를 수정한다.

 

Filter의 힘은 그것을 전달한(통과한) FilterChain에서 온다?

->The power of the Filter comes from the FilterChain that is passed into it.

~필터는 필터체인 안에 있을 때 효력을 발휘한다(한글 번역문서 도움)

https://godekdls.github.io/Spring%20Security/servletsecuritythebigpicture/#92-delegatingfilterproxy

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
}

Filter는 다운스트림 Filter와 서블릿에만 영향을 주기 때문에, 각 Filter의 호출 질서는 매우 중요하다.

2. 필터 프록시 위임(DelegatingFilterProxy)

스프링은 서블릿 컨테이너의 생명주기와 스프링의 ApplicationContext를 연결하는 DelgatingFilterProxy 라는 구현 필터를 제공한다. 서블릿 컨테이너는 자체 기준에 의한 Filter 인스턴스의 등록을 허용하고 있지만, 이를 스프링이 정의한 Beans는 알지 못한다.

개발자는 DelegatingFilterProxy를 서블릿 컨테이너 매커니즘 기준에 의해 등록하지만, 모든 작업은 이 필터를 구현하는 스프링 빈으로 위임하게 된다.

~ 필터만 등록해도 스프링 빈에 작업을 위임할 수 있게 되어 스프링 빈이 필터를 통해 서블릿 컨테이너의 변화를 알 수 있게 된다.(의역)

 

DelegatingFilterProxy는 ApplicationContext에서 Bean Filter0을 찾고 호출한다.

다음은 DelegatingFilterProxy의 슈도코드다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// Lazily get Filter that was registered as a Spring Bean
	// For the example in DelegatingFilterProxy
delegate
 is an instance of Bean Filter0
	Filter delegate = getFilterBean(someBeanName);
	// delegate work to the Spring Bean
	delegate.doFilter(request, response);
}

DelegatingFilterProxy의 또다른 장점은 Filter bean 인스턴스 참조를 지연할 수 있다는 점이다.

컨테이너가 시작하기 전에 Filter인스턴스의 등록을 필요로 하는 컨테이너가 있기 때문에 이는 중요하다.

그러나 스프링은 통상 ContextLoaderListener를 사용해 스프링 빈을 불러온다.

즉, Filter 인스턴스등록이 완료되기 전에는 스프링 빈을 불러오는 것이 끝나지 않는다.

3. FilterChainProxy

스프링 시큐리티의 서블릿 지원에는 FilterChainProxy도 포함되어 있다. FilterChainProxy는 많은 FIlter 인스턴스를 SecurityFileterChain으로 위임할 수 있도록 승인하는 스프링 시큐리티가 지원하는 특별한 Filter다.

FilterChainProxy는 빈이기 때문에, 통상 2번의 DelegatingFilterProxy로 감싸진다.

4. SecurityFilterChain

SecurityFilterChain은 스프링 시큐리티 FIlter 인스턴스를 파악하기 위해 FilterChainProxy에 의해 사용되며, Filter 인스턴스는 현재 요청을 호출할 수 있어야 한다.

SecurityFilterChain 안에 있는 SecurityFilter는 일반적으로 빈이다. 그러나 그들은 DelegatingFilterProxy를 대신해 FilterChainProxy와 함께 등록된다.(F.C.P가 D.F.P로 감싸져 있기 때문에?)

 

FilterChainProxy는 서블릿 컨테이너 또는 DelegatingFilterProxy와 함께 바로 등록하는 경우 많은 이점을 제공한다.

첫째, 스프링 시큐리티 서블릿 지원의 시작점을 제공한다. 만약 개발자가 스프링 시큐리티 서블릿 지원을 트러블 슈팅하고 싶다면, FilterChainProxy은 디버깅을 시작하기에 좋은 지점이다.

둘째, FilterChainProxy는 스프링 시큐리티 사용의 중심이기 때문에, 옵셔널로 보이지 않는(필수로 해야하는) 작업을 수행할 수 있다. 예를 들어 SecurityContext의 메모리 누수를 피하기 위해 SecurityContext를 비운다. 또한 스프링 시큐리티의 HttpFirewall을 특정 타입의 공격들로부터 어플리케이션들을 보호하기 위해 적용한다.

게다가, SecurityFilterChain을 호출해야하는 시기를 결정할 때 더 많은 유연성을 제공합니다. 서블릿 컨테이너 안에서 Filter 인스턴스는 URL만을 기반으로 호출된다. 그러나 FilterChainProxy는 RequestMatcher 인터페이스를 사용한 HttpServletRequest의 모든 것을 기반으로 호출을 실행할 수 있다.

 

다수의 SecurityFilterChain안에서, FilterChainProxy는 어떤 SecurityFilterChain이 사용될지를 결정한다. 조건에 일치하는 첫번째 SecurityFilterChain만 호출된다. 만약 /api/message/ URL을 요청한다면, 조건에 일치하는 것은 SecurityFilterChain(n)의 /** URL과 SecurityFilterChain(0)의 /api/** URL이 있지만, SecurityFilterChain(0)의 /api/** 패턴과 제일 먼저 조건에 일치하므로 호출되는 것은 SecurityFIlterChain(0)이다. /message URL로 요청한다면, SecurityFilterChain(0)의 /api/** URL과는 일치하지 않으므로 FIlterChainProxy는 다른 SecurityFilterChain을 찾아보고, 다른 것이 없는 경우 SecurityFilterChain(n)의 /** URL이 호출된다.

 

SecurityFilterChain(0)의 보안 인스턴스는 3개만 설정되었다는 것을 인지해라. 그러나, SecurityFilterChain(n)의 보안 인스턴스는 4개로 설정되어 있다. 각각의 SecurityFilterChain은 독립적이고 고유한 설정을 가질 수 있다. 경우에 따라, 어플리케리션의 특정 요청에 대해 스프링 시큐리티가 그 요청을 무조건 통과시키길 바란다면(의역), SecurityFilterChain의 보안 Filter(보안 인스턴스)를 0개로 설정하는 것도 가능하다.

5. Security Filters

보안 Filter는 SecurityFilterChain API와 함께(를 사용해서) FilterChainProxy에 주입된다. Filter 인스턴스의 순서는 중요하다. 일반적으로 스프링 시큐리티 Filter 인스턴스의 순서를 알아야 할 필요는 없다. 그러나, 순서를 알고 있다면 유용한 순간이 있다.

스프링 시큐리티 FIlter 인스턴스의 순서 :

 

Form Login :: Spring Security

When the username and password are submitted, the UsernamePasswordAuthenticationFilter authenticates the username and password. The UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter, so the following diagram should look pr

docs.spring.io

5-1. Form Login

스프링 시큐리티는 HTML 폼 기반 사용자이름/ 비밀번호 인증을 지원한다. 이 섹션은 스프링 시큐리티에서 작동하는 Form 기반 인증 방식을 설명한다. 먼저 사용자는 로그인 폼으로 리다이렉트된다.

(1) 사용자가 /private 리소스에 대해 권한이 없는 요청을 보내고, 이 요청은 해당 리소스에 대한 권한이 없기 때문에 인가되지 않는다.

(2) 스프링 시큐리티의 FilterSecurityInterceptor가 그 권한이 없는 요청은 인가되지 않았다고 AccessDiniedException을 발생시킴으로서 알린다.

(3) 사용자는 인가되지 않았기 때문에, ExceptionTranslationFilter는 인가 작업을 시작하고, AuthenticaionEntryPoint로 설정해둔 로그인 페이지로 리다이렉트한다. 대부분의 경우, AuthenticaionEntryPoint는 LoginUrlAuthenticationEntryPoint의 인스턴스이다.

(4) 브라우저는 리다이렉트된 로그인 페이지에 대한 조회 요청을 보낸다

(5) 어플리케이션에선 로그인 페이지를 렌더링해야 한다.

 

Username과 password를 제출하면 UsernamePasswordAuthenticationFilter가 이 값을 인증한다. UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter을 상속한 필터이기 때문에, 다이어그램도 비슷한 모양이다.

(1) 사용자가 username과 password를 제출하면, UsernamePasswordAuthenticationFilter는 username과 password를 HttpServletRequest 인스턴스로부터 추출하여 인증 타입의 UsernamePasswordAuthenticaionToken을 생성한다.

(2) UsernamePasswordAuthenticaionToken은 AuthenticaionManager 인스턴스를 통과하며(로 넘겨지며) 인증된다. AuthenticaionManager의 모양은 사용자정보의 저장 형태를 따른다.

 

(3) 인증에 실패한 경우,

(3-1) SecurityContextHolder는 비워진다.

(3-2) RememberMeServices.loginFail이 호출된다. 만약 remember me를 설정했다면, 호출되지 않는다. 해당 인터페이스의 자바문서를 참조해라. -> RememberMeServices

(3-3) AuthenticaionFailureHandler가 호출된다. 참조문서 -> AuthenticationFailureHandler(404)

 

(4) 인증에 성공한 경우,

(4-1) SessionAuthenticaionStrategy은 새로운 로그인이 발생했음을 알린다. 참조문서 -> SessionAuthenticationStrategy(404)

(4-2) SecurityContextHolder에 Authenticaion을 세팅한다. 참조 SecurityContextPersistenceFilter(404)

(4-3) RememberMeServices.loginSuccess이 호출된다. 만약 remember me를 설정했다면, 호출되지 않는다. 해당 인터페이스의 자바문서를 참조 RememberMeServices

(4-4) ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 발행한다.

(4-5) AuthenticaionSuccessHandler가 호출된다. 일반적으로, 우리가 로그인 페이지를 리다이렉트하는 경우엔, ExceptionTranslationFilter에서 저장한 요청으로 리다이렉트하는 SimpleUrlAuthenticaionSuccessHandler가 호출된다.

(후략)

6. Handling Security Exceptions(추후 해석예정)

7. Servlet Authenticaion Architecture

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-authentication

 

Servlet Authentication Architecture :: Spring Security

ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProvider instances. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful,

docs.spring.io

7-1. SecurityContextHolder

스프링 시큐리티의 인증 모델의 중심엔 SecurityContextHolder가 있다. 이것은 SecurityContext를 포함한다.  

SecurityContextHoler는 스프링 시큐리티가 어떤 사용자가 인가되었는지에 대한 자세한 내용을 저장하는 곳이다. 스프링 시큐리티는 SecurityContextHoler에 어떻게 값이 채워지는 지는 상관하지 않는다. Holder에 만약 값을 포함되어 있다면, 인증한 사용자로 이용된다

~ (값이 있다면(로그인에 성공해서 값이 있기 때문에) 그 사용자는 인증된 것으로 간주한다는 뜻 같다)

사용자가 인증되었음을 나타내는 가장 쉬운 방법은 직접 SecurityContextHolder를 설정하는 것이다.

 

직접 설정 예시 : 예시 49

SecurityContext context = SecurityContextHolder.createEmptyContext(); // 1번
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); // 2번
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context); // 3번

(1) 비어있는 SecurityContext를 만든다. 다중 스레드의 경합을 피하기 위해 SecurityContextHolder.getContext().setAuthentication(authentication)를 사용하는 것 대신 새로운 SecurityContext를 생성해야 한다.

(2) 다음으로, 새로운 Authentication 객체를 만든다. 스프링 시큐리티는 SecurityContext에 세팅된 Authentication 구현체의 타입은 상관하지 않는다. 예시에서는 TestingAuthenticationToken이 사용하기 매우 간편해 예시로 들었다. 일반적인 프로덕션 환경에서는 UsernamePassewordAuthenticationToken(UserDetails, password, authorities)를 주로 사용한다.

(3) 마지막으로 SecurityContext를 SecurityContextHolder 안에 세팅한다. 스프링 시큐리티는 인가를 위해 이 정보를 사용한다.

 

인증된 주체(authenticated principal)에 대한 정보를 얻으려면, SecurityContextHolder에 접근해라.

 

 예시 50. 현재 인증된 사용자에게 접근

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

기본적으로, SecurityContextHolder는 ThreadLocal을 사용해 정보를 저장하기 때문에, 메소드에 직접 SecurityContext를 파라미터로 주지 않아도 동일한 스레드라면 항상 SecurityContext에 접근할 수 있다. 당신이 현재 principal의 요청이 진행된 후 스레드를 비우기만 한다면, ThreadLocal을 사용해도 안전하다. 스프링 시큐리티의 FIlterChianProxy는 SecurityContext가 언제나 비워지는 것을 보장한다(비워준다).

 

 어떤 어플리케이션은 스레드와 함께 작동하는 특정 방식 때문에 ThreadLocal을 사용하는 데 적합하지 않을 수 있다. 예를 들어, 스윙 클라이언트에선 하나의 SecurityContext를 위해 JVM 안의 모든 스레드를  사용하기를 원할 수도 있다. 개발자는 SecurityContextHolder를 실행할 때 어떻게 Context가 저장될지 저장전략을 설정할 수 있다. Standalone 어플리케이션을 위해, 개발자는 SecurityContextHolder.MODE_GLOBAL 전략을 사용할 수도 있다. 다른 어플리케이션은 보안 스레드에 의해 생성된 스레드들 또한 같은 보안 스레드로 가정(설정)할 수 있고, 이 과정은 개발자가 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL을 사용하며 성취(설정)할 수 있다. 개발자는 기본 SecurityContextHolder.MODE_THREADLOCAL의 모드를 두 가지 방식을 변경할 수 있다. 첫째는 시스템 프로퍼티를 설정하는 것이고, 두번째는 SecurityContextHolder의 스태틱 메서드를 호출하는 것이다. 대부분의 어플리케이션은 기본을 바꿀 필요가 없다. 더 자세한 정보는 JavaDoc의 SecurityContextHolder 항목 참조.

7-2. SecurityContext

SecurityContext는 SecurityContextHolder안에 존재(obtained)하며, SecurityContext는 Authentication 객체를 갖고 있다.

7-3. Authentication

Authentication 인터페이스는 스프링 시큐리티와 함께 두 가지의 주 목적을 제공한다 :

1. 사용자가 인증하기 위해 제공한 credentials을 제공하기 위한 AuthenticationManager의 입력이다. 이 상황에선 isAuthenticated()는 false를 반환한다.

2. 현재 인증된 사용자를 대표한다. 개발자는 현재의 Authenticaion을 SecurityContext 안에서 획득(obtain)할 수 있다.

Authenticaion은 다음을 포함한다 :

1. Principal : 사용자를 식별한다. username/password를 이용한 인증방식에서 주로 UserDetails의 인스턴스가 되는 경우가 많다.

2. Credentials : 주로 비밀번호. 많은 경우에, 사용자가 인증되고 나면 비워진다. 유출되지 않도록 주의할 것.

3. Authorities : GrandAuthority 인스턴스는 사용자가 허용되었음을 뜻하는 높은 등급의 권한(허가)들이다. 역할과 스코프가 대표적인 예시이다. 

728x90