토이 프로젝트 3에서 뷰는 따로 구현하지 않고 REST API만 구현을 했는데, Spring Security에서 form-data가 아닌 JSON으로 로그인 요청을 받고 싶어서 도전해보았었다. 그러나 Spring Security 5에서 6으로의 변경사항들도 있었고, 인증 과정을 제대로 이해하지 못해 결국 구현에 성공하지는 못하고 기한에 맞추기 위해 기본 login을 사용하였다.
이에 다음 프로젝트에서는 JWT나 OAuth를 사용한 인증 및 인가를 제대로 구현할 수 있도록 Spring Security를 공부해보고자 한다. 필터를 새로 만들거나 기존의 필터를 상속받아 바꿔치기하는 등의 과정에 집중해본다.
참고: Java Doc
https://docs.spring.io/spring-security/reference/servlet/architecture.html#servlet-securityfilterchain
Spring Security를 적용하고 애플리케이션을 구동하면 INFO에 필터 목록이 나열된다. 다음은 Spring Security 문서에서 가져왔다. (24.03.04 기준)
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]
세션 ID가 URL에 포함되면 HTTP access log 등을 통해 유출될 수 있기 때문에 HttpServletResponse를 사용해 이를 막는다.
Disables encoding URLs using the HttpServletResponse to prevent including the session id in URLs which is not considered URL because the session id can be leaked in things like HTTP access logs.
Since: 5.7
Author: Rob Winch
기본적으로 ThreadLocal 기반으로 동작하는 SpringSecurityContextHolder가 비동기 요청에도 사용 가능하도록 해주는 필터이다.
Provides integration between the SecurityContext and Spring Web's WebAsyncManager by using the SecurityContextCallableProcessingInterceptor.beforeConcurrentHandling(org.springframework.web.context.request.NativeWebRequest, Callable) to populate the SecurityContext on the Callable.
See Also: SecurityContextCallableProcessingInterceptor
Author: Rob Winch
SecurityContextRespository를 사용해 SecurityContext를 생성하고 SecurityContextHolder에 저장하는 필터이다.
SecurityContextPersistenceFilter와 유사하지만 SecurityContext를 저장하기 위해 saveContext() 를 직접 호출해야 한다는 점이 다르다.
A jakarta.servlet.Filter that uses the SecurityContextRepository to obtain the SecurityContext and set it on the SecurityContextHolder. This is similar to SecurityContextPersistenceFilter except that the SecurityContextRepository.saveContext(SecurityContext, HttpServletRequest, HttpServletResponse) must be explicitly invoked to save the SecurityContext. This improves the efficiency and provides better flexibility by allowing different authentication mechanisms to choose individually if authentication should be persisted.
Since: 5.7
Author: Rob Winch, Marcus da Coregio
현재 응답에 헤더 값을 설정해주는 필터이다. X-Frame-Options, X-XSS-Protection, X-Content-Type-Options 등의 브라우저 보안 관련 헤더를 추가하는 데 사용할 수 있다.
Filter implementation to add headers to the current response. Can be useful to add certain headers which enable browser protection. Like X-Frame-Options, X-XSS-Protection and X-Content-Type-Options.
Since: 3.2
Author: Marten Deinum, Josh Cummings, Ankur Pathak
CSRF 공격을 방어하고자 synchronizer 토큰 패턴을 적용하는 필터이다. 개발자들은 상태 변화가 일어나는 요청에는 CsrfFilter를 적용할 것이 요구되며, 이 말은 곧 REST 규칙에 맞게 웹 애플리케이션을 구현하라는 말과 상통한다.
CsrfTokenRepository의 구현체는 일반적으로 LazyCsrfTokenRepository로 감싸진 HttpSessionCsrfTokenRepository로 CsrfToken을 HttpSession에 저장한다. 이는 클라이언트에서 수정할 수 있는 쿠키에 토큰을 저장하는 것보다 선호된다.
- OncePerRequestFilter를 상속받는다.
Applies CSRF protection using a synchronizer token pattern. Developers are required to ensure that CsrfFilter is invoked for any request that allows state to change. Typically this just means that they should ensure their web application follows proper REST semantics (i.e. do not change state with the HTTP methods GET, HEAD, TRACE, OPTIONS).
Typically the CsrfTokenRepository implementation chooses to store the CsrfToken in HttpSession with HttpSessionCsrfTokenRepository wrapped by a LazyCsrfTokenRepository. This is preferred to storing the token in a cookie which can be modified by a client application.
Since: 3.2
Author: Rob Winch, Steve Riesenberg
로그아웃을 처리한다.
일렬의 LogoutHandler들을 생성하는데, 이 핸들러들은 필요한 순서대로 정의된다. 일반적으로 TokenBasedRememberMeServices나 SecurityContextLogoutHandler를 순서대로 부르게 된다.
로그아웃 후, 어떤 생성자를 사용했는가에 따라 LogoutSuccessHandler에서 정해진 url이나 logoutSuccessUrl로 redirect가 실행된다.
Logs a principal out.
Polls a series of LogoutHandlers. The handlers should be specified in the order they are required. Generally you will want to call logout handlers TokenBasedRememberMeServices and SecurityContextLogoutHandler (in that order).
After logout, a redirect will be performed to the URL determined by either the configured LogoutSuccessHandler or the logoutSuccessUrl, depending on which constructor was used.
Author:
Ben Alex, Eddú Meléndez
form을 기반으로 인증을 처리하는 필터이다. 3.0 이전 버전에서는 AuthenticationProcessingFilter이라는 이름으로 존재했다.
로그인 폼은 username과 password라는 두 파라미터를 필수로 요구한다. 기본적으로 이 두 파라미터의 이름은 SPRING_SECURITY_FORM_USERNAME_KEY, SPRING_SECURITY_FORM_PASSWORD_KEY로 static 필드로 저장되어있다. 이 파라미터 이름들은 usernameParameter와 passwordParameter로 수정할 수 있다.
이 필터는 /login url에 디폴트로 맵핑되어 있다.
Processes an authentication form submission. Called AuthenticationProcessingFilter prior to Spring Security 3.0.
Login forms must present two parameters to this filter: a username and password. The default parameter names to use are contained in the static fields SPRING_SECURITY_FORM_USERNAME_KEY and SPRING_SECURITY_FORM_PASSWORD_KEY. The parameter names can also be changed by setting the usernameParameter and passwordParameter properties.
This filter by default responds to the URL /login.
Since: 3.0
Author: Ben Alex, Colin Sampaleanu, Luke Taylor
사용자가 로그인 페이지를 설정해주지 않았을 경우 사용되는 필터이다. 설정 코드에서 체인에 해당 필터를 삽입한다. 로그인 페이지로 리다이렉트가 걸릴 경우에만 작동한다.
For internal use with namespace configuration in the case where a user doesn't configure a login page. The configuration code will insert this filter in the chain instead. Will only work if a redirect is used to the login page.
Since: 2.0
Author: Luke Taylor
기본 로그아웃 페이지를 생성한다.
Generates a default log out page.
Since: 5.1
Author: Rob Winch
HTTP 요청의 BASIC 인증을 처리하고, 그 결과를 SecurityContextHolder에 저장한다.
Authorization 헤더가 들어가는 요청(base64로 인코딩된 아이디, 비밀번호로 만든 토큰이 Basic scheme으로 첨부됨)은 전부 이 필터를 거친다.
인증이 성공하면 인증 객체는 SecurityContextHolder에 저장된다. 반대로 실패하면 AuthenticationEntryPoint 구현체가 호출되어 사용자에게 다시 인증창을 보낸다.
널리 쓰이는 필터이지만, 비밀번호를 텍스트로 전송하기 때문에 많은 상황에서는 바람직하지 않다.
RememberMeServices가 설정된 경우, 이 필터는 클라이언트에게 자동으로 remember-me 정보를 전송하게 되어있다. 따라서 추후의 요청들은 remember-me를 통해 인증될 것이므로 BASIC 인증 헤더가 필요가 없다.
Processes a HTTP request's BASIC authorization headers, putting the result into the SecurityContextHolder.
For a detailed background on what this filter is designed to process, refer to RFC 1945, Section 11.1 . Any realm name presented in the HTTP request is ignored.
In summary, this filter is responsible for processing any request that has a HTTP request header of Authorization with an authentication scheme of Basic and a Base64-encoded username:password token. For example, to authenticate user "Aladdin" with password "open sesame" the following header would be presented:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
This filter can be used to provide BASIC authentication services to both remoting protocol clients (such as Hessian and SOAP) as well as standard user agents (such as Internet Explorer and Netscape).
If authentication is successful, the resulting Authentication object will be placed into the SecurityContextHolder.
If authentication fails and ignoreFailure is false (the default), an AuthenticationEntryPoint implementation is called (unless the ignoreFailure property is set to true). Usually this should be BasicAuthenticationEntryPoint, which will prompt the user to authenticate again via BASIC authentication.
Basic authentication is an attractive protocol because it is simple and widely deployed. However, it still transmits a password in clear text and as such is undesirable in many situations.
Note that if a RememberMeServices is set, this filter will automatically send back remember-me details to the client. Therefore, subsequent requests will not need to present a BASIC authentication header as they will be authenticated using the remember-me mechanism.
Author: Ben Alex
현재 요청과 캐시로 저장된 요청이 일치할 경우 해당 요청을 재구성하는 필터이다. (=인증처리 후 첫 요청을 재구성한다) 캐시된 요청의 getMatchingRequest를 호출하며, 이 메서드가 값을 반환할 경우 필터의 doFilter 메서드로 값을 넘긴다. 캐시된 요청이 null을 반환할 경우, 원래 요청을 사용하며 필터는 아무 효과가 없다.
Responsible for reconstituting the saved request if one is cached and it matches the current request.
It will call getMatchingRequest on the configured RequestCache. If the method returns a value (a wrapper of the saved request), it will pass this to the filter chain's doFilter method. If null is returned by the cache, the original request is used and the filter has no effect.
Since: 3.0
Author: Luke Taylor
API 보안 메서드를 구현한 요청 래퍼를 ServletRequest에 적용하는 필터이다. 이 래퍼는
- authenticate: 사용자가 인증되었는지 확인하고, 인증되지 않았으면 로그인 페이지로 보냄
- login: 사용자가 AuthenticationManager를 통해 인증하도록 함
- logout: LogoutHandler를 통해 사용자가 로그아웃하도록 함
- start: AsyncContext를 invoke한 스레드에서 SecurityContext를 자동으로 복사해옴
의 4가지 메서드를 추가로 상속한다.
A Filter which populates the ServletRequest with a request wrapper which implements the servlet API security methods.
SecurityContextHolderAwareRequestWrapper is extended to provide the following additional methods:
HttpServletRequest.authenticate(HttpServletResponse) - Allows the user to determine if they are authenticated and if not send the user to the login page. See setAuthenticationEntryPoint(AuthenticationEntryPoint).
HttpServletRequest.login(String, String) - Allows the user to authenticate using the AuthenticationManager. See setAuthenticationManager(AuthenticationManager).
HttpServletRequest.logout() - Allows the user to logout using the LogoutHandlers configured in Spring Security. See setLogoutHandlers(List).
AsyncContext.start(Runnable) - Automatically copy the SecurityContext from the SecurityContextHolder found on the Thread that invoked AsyncContext.start(Runnable) to the Thread that processes the Runnable.
Author: Orlando Garcia Carmona, Ben Alex, Luke Taylor, Rob Winch, Eddú Meléndez
SecurityContextHolder에 인증 객체가 없는지 감지하고, 없으면 하나 생성한다. 이 때 생성하는 인증 객체는 anonymousUser라는 아이디를 가지고, 'ROLE_ANONYMOUS' 인가를 가지고 있다.
Detects if there is no Authentication object in the SecurityContextHolder, and populates it with one if needed.
Author: Ben Alex, Luke Taylor, Evgeniy Cheban
필터체인 내에서 던져진 AccessDeniedException, AuthenticationException를 처리한다. Java 예외와 HTTP 응답을 연결해주기 때문에 필요하다. 사용자 인터페이스를 유지하는 데에 집중되어 있으며, 이 필터 자체로는 보안이 강화되거나 하지 않는다.
AuthenticationException가 감지되면 필터는 authenticationEntryPoint를 호출한다. 이를 통해 인증 실패들을 공통적으로 관리할 수 있다.
AccessDeniedException가 감지되면 필터는 사용자가 anonymous user인지 결정하고, 맞을 경우에는 authenticationEntryPoint가 호출된다. anonymous user가 아닐 시에는 AccessDeniedHandler로 위임한다.
Handles any AccessDeniedException and AuthenticationException thrown within the filter chain.
This filter is necessary because it provides the bridge between Java exceptions and HTTP responses. It is solely concerned with maintaining the user interface. This filter does not do any actual security enforcement.
If an AuthenticationException is detected, the filter will launch the authenticationEntryPoint. This allows common handling of authentication failures originating from any subclass of org.springframework.security.access.intercept.AbstractSecurityInterceptor.
If an AccessDeniedException is detected, the filter will determine whether or not the user is an anonymous user. If they are an anonymous user, the authenticationEntryPoint will be launched. If they are not an anonymous user, the filter will delegate to the AccessDeniedHandler. By default the filter will use AccessDeniedHandlerImpl.
To use this filter, it is necessary to specify the following properties:
authenticationEntryPoint indicates the handler that should commence the authentication process if an AuthenticationException is detected. Note that this may also switch the current protocol from http to https for an SSL login.
requestCache determines the strategy used to save a request during the authentication process in order that it may be retrieved and reused once the user has authenticated. The default implementation is HttpSessionRequestCache.
Author: Ben Alex, colin sampaleanu
AuthorizationManager를 사용하여 url에 대한 접근을 제한한다.
An authorization filter that restricts access to the URL using AuthorizationManager.
Since: 5.5
Author: Evgeniy Cheban
Instead of implementing Filter, you can extend from OncePerRequestFilter which is a base class for filters that are only invoked once per request and provides a doFilterInternal method with the HttpServletRequest and HttpServletResponse parameters.
위에 언급된 15개의 기본 필터에서 하나를 구현하기보다, 요청 한 번에 한 번 호출되는 필터인 OncePerRequestFilter를 상속하여 HttpServletRequest와 HttpServletResponse를 파라미터로 받는 doFilterInternal 메서드를 재정의하는 방법도 있다.
Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments.
A filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore, subclasses can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types.
Subclasses may use isAsyncDispatch(HttpServletRequest) to determine when a filter is invoked as part of an async dispatch, and use isAsyncStarted(HttpServletRequest) to determine when the request has been placed in async mode and therefore the current dispatch won't be the last one for the given request.
Yet another dispatch type that also occurs in its own thread is ERROR. Subclasses can override shouldNotFilterErrorDispatch() if they wish to declare statically if they should be invoked once during error dispatches.
The getAlreadyFilteredAttributeName method determines how to identify that a request is already filtered. The default implementation is based on the configured name of the concrete filter instance.
Since: 06.12.2003
Author: Juergen Hoeller, Rossen Stoyanchev, Sam Brannen