Spring Security 源码分析
【基于官方文档】链接
文章目录
1、Architecture
This section discusses Spring Security’s high-level architecture within Servlet based applications. We build on this high-level understanding within the Authentication, Authorization, and Protection Against Exploits sections of the reference.
本章讨论基于 servlet 的应用中 spring security 架构。基于认证,授权。
2、 A Review of Filters
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.
spring security 基于 servlet 过滤器链,下图展示了处理单个 http 请求处理的经典分层。
The client sends a request to the application, and the container creates a
FilterChain
, which contains theFilter
instances andServlet
that should process theHttpServletRequest
, based on the path of the request URI. In a Spring MVC application, theServlet
is an instance ofDispatcherServlet
. At most, oneServlet
can handle a singleHttpServletRequest
andHttpServletResponse
. However, more than oneFilter
can be used to:
客户端向应用发送请求,servlet
容器创建 FilterChain
过滤器链,过滤器链中包含过滤器实例及servlet
,应当根据请求的 URI 处理 `HttpServletRequest。一个“Servlet”可以处理一个“HttpServletRequest”和“HttpServletResponse”,但可以使用多个过滤器。
-
Prevent downstream
Filter
instances or theServlet
from being invoked. In this case, theFilter
typically writes theHttpServletResponse
. -
Modify the
HttpServletRequest
orHttpServletResponse
used by the downstreamFilter
instances and theServlet
.
The power of the
Filter
comes from theFilterChain
that is passed into it.
下游的 HttpServletRequest
或者 HttpServletResponse
可以被过滤器链中的过滤器修改,举例如下。
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
}
3、DelegatingFilterProxy
Spring provides a
Filter
implementation namedDelegatingFilterProxy
that allows bridging between the Servlet container’s lifecycle and Spring’sApplicationContext
. The Servlet container allows registeringFilter
instances by using its own standards, but it is not aware of Spring-defined Beans. You can registerDelegatingFilterProxy
through the standard Servlet container mechanisms but delegate all the work to a Spring Bean that implementsFilter
.
spring 提供了 DelegatingFilterProxy
的过滤器实现,用于将 servlet
容器与 spring 的 ApplicationContext
桥接起来。Servlet 容器运气用齐自己的标准注册过滤器实例,但是其并不知道 spring 的定义。你可以通过标准的Servlet容器机制注册 DelegatingFilterProxy
,但将所有工作委托给实现 Filter
的 Spring Bean。
Here is a picture of how
DelegatingFilterProxy
fits into theFilter
instances and theFilterChain
.
3.1 Source Analysis
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);
}
3.2 DelegatingFilterProxy Bean Register In Springboot
通过 AbstractFilterRegistrationBean
将 DelegatingFilterProxy
注入 servlet
.
3.2.1 SecurityFilterAutoConfiguration
spring boot 下的注入
// springSecurityFilterChain
private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
// 详情见 下述 3.3.2
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
3.2.2 DelegatingFilterProxyRegistrationBean
@Override
public DelegatingFilterProxy getFilter() {
return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {
@Override
protected void initFilterBean() throws ServletException {
// Don't initialize filter bean on init()
}
};
}
3.2.3 AbstractFilterRegistrationBean
调用上述 getFilter() 方法 注入 servletContext 中
@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
Filter filter = getFilter();
return servletContext.addFilter(getOrDeduceName(filter), filter);
}
4、FilterChainProxy
Spring Security’s Servlet support is contained within
FilterChainProxy
.FilterChainProxy
is a specialFilter
provided by Spring Security that allows delegating to manyFilter
instances throughSecurityFilterChain
. SinceFilterChainProxy
is a Bean, it is typically wrapped in a DelegatingFilterProxy.
spring security servlet 支持包含在 FilterChainProxy
中, FilterChainProxy
是 spring security 提供的特殊的 Filter
, 支持代理多个 Filter
。FilterChainProxy
是一个 bean,其被包装在 DelegatingFilterProxy
内部。
4.1 FilterChainProxy Bean Definition In Spring
For Example in WebSecurityConfiguration
将 springSecurityFilterChain
注入 spring 容器中
public static final String DEFAULT_FILTER_NAME = “springSecurityFilterChain”;
/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasFilterChain = !this.securityFilterChains.isEmpty();
if (!hasFilterChain) {
this.webSecurity.addSecurityFilterChainBuilder(() -> {
this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
this.httpSecurity.formLogin(Customizer.withDefaults());
this.httpSecurity.httpBasic(Customizer.withDefaults());
return this.httpSecurity.build();
});
}
for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
}
for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
customizer.customize(this.webSecurity);
}
return this.webSecurity.build();
}
5、SecurityFilterChain
SecurityFilterChain
is used by FilterChainProxy to determine which Spring SecurityFilter
instances should be invoked for the current request.
FilterChainProxy
将决定当前请求调用哪个 Filter
实例.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dq0C8kD2-1677572639308)(/Users/qianyuhua/Documents/mine/painnote/source-code/spring-security/picture/securityfilterchain.png)]
The Security Filters in
SecurityFilterChain
are typically Beans, but they are registered withFilterChainProxy
instead of DelegatingFilterProxy.FilterChainProxy
provides a number of advantages to registering directly with the Servlet container or DelegatingFilterProxy. First, it provides a starting point for all of Spring Security’s Servlet support. For that reason, if you try to troubleshoot Spring Security’s Servlet support, adding a debug point inFilterChainProxy
is a great place to start.
Security Filters 是由 FilterChainProxy
注册的,并不是 DelegatingFilterProxy
Second, since
FilterChainProxy
is central to Spring Security usage, it can perform tasks that are not viewed as optional. For example, it clears out theSecurityContext
to avoid memory leaks. It also applies Spring Security’sHttpFirewall
to protect applications against certain types of attacks.
In addition, it provides more flexibility in determining when a
SecurityFilterChain
should be invoked. In a Servlet container,Filter
instances are invoked based upon the URL alone. However,FilterChainProxy
can determine invocation based upon anything in theHttpServletRequest
by using theRequestMatcher
interface.
FilterChainProxy
中可以通过 RequestMatcher
决定调用哪一条 SecurityFilterChain
In the Multiple SecurityFilterChain figure,
FilterChainProxy
decides whichSecurityFilterChain
should be used. Only the firstSecurityFilterChain
that matches is invoked. If a URL of/api/messages/
is requested, it first matches on theSecurityFilterChain0
pattern of/api/**
, so onlySecurityFilterChain0
is invoked, even though it also matches onSecurityFilterChainn
. If a URL of/messages/
is requested, it does not match on theSecurityFilterChain0
pattern of/api/**
, soFilterChainProxy
continues trying eachSecurityFilterChain
. Assuming that no otherSecurityFilterChain
instances match,SecurityFilterChainn
is invoked.
Notice that
SecurityFilterChain0
has only three securityFilter
instances configured. However,SecurityFilterChainn
has four securityFilter
instanes configured. It is important to note that eachSecurityFilterChain
can be unique and can be configured in isolation. In fact, aSecurityFilterChain
might have zero securityFilter
instances if the application wants Spring Security to ignore certain requests.
5.1 FilterChainProxy
// 过滤器执行方法,FilterChainProxy 本质也是一个过滤器链
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断是否需要清除上下文环境
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
// 不需要清除上下文环境,直接执行核心过滤器
doFilterInternal(request, response, chain);
return;
}
try {
// 标记已应用过滤器
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 执行核心过滤器
doFilterInternal(request, response, chain);
} catch (Exception ex) {
// 处理异常情况
// 获取异常堆栈信息
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
// 查找是否存在RequestRejectedException异常
Throwable requestRejectedException = this.throwableAnalyzer
.getFirstThrowableOfType(RequestRejectedException.class, causeChain);
if (!(requestRejectedException instanceof RequestRejectedException)) {
// 如果没有RequestRejectedException异常则抛出原异常
throw ex;
}
// 如果存在RequestRejectedException异常则使用处理器进行处理
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response,
(RequestRejectedException) requestRejectedException);
} finally {
// 清除上下文环境
this.securityContextHolderStrategy.clearContext();
// 移除已应用过滤器的标记
request.removeAttribute(FILTER_APPLIED);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 获取防火墙信息
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
// 通过防火墙信息,找到过滤器,执行,为空不进行安全处理,直接将请求传递给下一个过滤器。
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
//如果过滤器列表不为空,则将请求交给FilterChainDecorator对象来执行安全过滤器。这里使用了一个名为reset的局部FilterChain对象,用于在安全过滤器链中执行完毕后重置路径过滤器。在reset对象中,将请求和响应对象传递给chain,然后在chain对象中执行安全过滤器。
FilterChain reset = (req, res) -> {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
}
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}
/**
* Internal {@code FilterChain} implementation that is used to pass a request through
* the additional internal list of filters which match the request.
*/
private static final class VirtualFilterChain implements FilterChain {
...
// 循环链表,属于过滤器链的所有过滤器依次执行
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// 过滤器位置已经达到了过滤器链的末尾,即 currentPosition == size,那么直接调用原始过滤器链的 doFilter 方法处理请求。
if (this.currentPosition == this.size) {
// Servlet 里的 Filter,
this.originalChain.doFilter(request, response);
return;
}
// 获取下一个需要执行的过滤器,然后调用该过滤器的 doFilter 方法
this.currentPosition++;
Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
if (logger.isTraceEnabled()) {
String name = nextFilter.getClass().getSimpleName();
logger.trace(LogMessage.format("Invoking %s (%d/%d)", name, this.currentPosition, this.size));
}
// 调用
nextFilter.doFilter(request, response, this);
}
}
6. Handling Security Exceptions
The
ExceptionTranslationFilter
allows translation ofAccessDeniedException
andAuthenticationException
into HTTP responses.
ExceptionTranslationFilter
可以把 AccessDeniedException
and AuthenticationException
转换成 http 响应数据
ExceptionTranslationFilter
is inserted into the FilterChainProxy as one of the Security Filters.
The following image shows the relationship of
ExceptionTranslationFilter
to other components:
-
First, the
ExceptionTranslationFilter
invokesFilterChain.doFilter(request, response)
to invoke the rest of the application. -
If the user is not authenticated or it is an
AuthenticationException
, then Start Authentication.如果用户没有经过身份验证或者它是一个’ AuthenticationException ',那么启动身份验证。
- The SecurityContextHolder is cleared out.
- The
HttpServletRequest
is saved so that it can be used to replay the original request once authentication is successful.
-
The
AuthenticationEntryPoint
is used to request credentials from the client. For example, it might redirect to a log in page or send aWWW-Authenticate
header.Otherwise, if it is an
AccessDeniedException
, then Access Denied. TheAccessDeniedHandler
is invoked to handle access denied. 否则,如果它是’ AccessDeniedException ‘,则Access Denied。调用’ AccessDeniedHandler '来处理被拒绝的访问。
6.1 ExceptionTranslationFilter
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
// 如果用户没有经过身份验证或者它是一个' AuthenticationException ',那么启动身份验证。
if (exception instanceof AuthenticationException) {
this.logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception);
// 如果它是' AccessDeniedException ',则*Access Denied*。调用' AccessDeniedHandler '来处理被拒绝的访问。
} else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) {
this.logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception);
this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
} else {
this.logger.debug("Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception);
this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
}
}
}
7. Saving Requests Between Authentication
As illustrated in Handling Security Exceptions, when a request has no authentication and is for a resource that requires authentication, there is a need to save the request for the authenticated resource to re-request after authentication is successful. In Spring Security this is done by saving the HttpServletRequest
using a RequestCache
implementation.
当请求没有身份验证,并且是针对需要身份验证的资源时,需要保存请求,以便经过身份验证的资源在身份验证成功后重新请求。
7.1. RequestCache
The
HttpServletRequest
is saved in theRequestCache
. When the user successfully authenticates, theRequestCache
is used to replay the original request. TheRequestCacheAwareFilter
is what uses theRequestCache
to save theHttpServletRequest
.
By default, an
HttpSessionRequestCache
is used. The code below demonstrates how to customize theRequestCache
implementation that is used to check theHttpSession
for a saved request if the parameter namedcontinue
is present.
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
7.2 Prevent the Request From Being Saved
There are a number of reasons you may want to not store the user’s unauthenticated request in the session. You may want to offload that storage onto the user’s browser or store it in a database. Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login.
To do that, you can use the NullRequestCache
implementation.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
7.3 Prevent the Request From Being Saved
There are a number of reasons you may want to not store the user’s unauthenticated request in the session. You may want to offload that storage onto the user’s browser or store it in a database. Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login.
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
8.RequestCacheAwareFilter
The
RequestCacheAwareFilter
uses theRequestCache
to save theHttpServletRequest
.