1、架构
Spring Security中认证
、授权
等功能都是基于过滤器
实现的。
在一个web项目中,请求从客户端发起(例如浏览器),然后穿过层层Filter
,最终来到Servlet
上,被Servlet
处理。
上图中的Filter
我们称之为Web Filter
,Spring Security中的Filter
我们称之为Security Filter
,关系图如下:
可以看到,Spring Security Filter0…n 并不是直接嵌入到 Web Filter 中的,而是通过FilterChainProxy
来统一管理 Spring Security Filter0…n,FilterChainProxy
本身则通过 Spring 提供的DelegatingFilterProxy
代理过滤器嵌入到 Web Filter 之中。FilterChainProxy
可以存在多个过滤器链,如下图:
可以看到,当请求到达FilterChainProxy
之后,FilterChainProxy
会根据请求的路径,将请求转发到不同的SecurityFilterChain
上面去,不同的SecurityFilterChain
包含了不同的过滤器,也就是不同的请求会经过不同的过滤器。
🤔其中 Spring Security 给我们提供了哪些过滤器呢?默认情况下那些过滤器会被加载呢?
过滤器 | 过滤器作用 | 默认是否加载 |
---|---|---|
ChannelProcessingFilter | 过滤请求协议 HTTP 、HTTPS | NO |
WebAsyncManagerIntegrationFilter |
将 WebAsyncManger 与 SpringSecurity 上下文进行集成 | YES |
SecurityContextPersistenceFilter |
在处理请求之前,将安全信息加载到 SecurityContextHolder 中 | YES |
HeaderWriterFilter |
处理头信息加入响应中 | YES |
CorsFilter | 处理跨域问题 | NO |
CsrfFilter |
处理 CSRF 攻击 | YES |
LogoutFilter |
处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter | 处理 OAuth2 认证重定向 | NO |
Saml2WebSsoAuthenticationRequestFilter | 处理 SAML 认证 | NO |
X509AuthenticationFilter | 处理 X509 认证 | NO |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | NO |
CasAuthenticationFilter | 处理 CAS 单点登录 | NO |
OAuth2LoginAuthenticationFilter | 处理 OAuth2 认证 | NO |
Saml2WebSsoAuthenticationFilter | 处理 SAML 认证 | NO |
UsernamePasswordAuthenticationFilter |
处理表单登录 | YES |
OpenIDAuthenticationFilter | 处理 OpenID 认证 | NO |
DefaultLoginPageGeneratingFilter |
配置默认登录页面 | YES |
DefaultLogoutPageGeneratingFilter |
配置默认注销页面 | YES |
ConcurrentSessionFilter | 处理 Session 有效期 | NO |
DigestAuthenticationFilter | 处理 HTTP 摘要认证 | NO |
BearerTokenAuthenticationFilter | 处理 OAuth2 认证的 Access Token | NO |
BasicAuthenticationFilter |
处理 HttpBasic 登录 | YES |
RequestCacheAwareFilter |
处理请求缓存 | YES |
SecurityContextHolderAwareRequestFilter |
包装原始请求 | YES |
JaasApiIntegrationFilter | 处理 JAAS 认证 | NO |
RememberMeAuthenticationFilter | 处理 RememberMe 登录 | NO |
AnonymousAuthenticationFilter |
配置匿名认证 | YES |
OAuth2AuthorizationCodeGrantFilter | 处理OAuth2认证中授权码 | NO |
SessionManagementFilter |
处理 session 并发问题 | YES |
ExceptionTranslationFilter |
处理认证/授权中的异常 | YES |
FilterSecurityInterceptor |
处理授权相关 | YES |
SwitchUserFilter | 处理账户切换 | NO |
2、源码分析
先把FilterChainProxy
源码亮出来,我们一部分一部分来,先从它声明的全局属性上开始:
private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
- FILTER_APPLIED 变量是一个标记,用来标记过滤器是否已经执行过了。
- filterChains 是过滤器链集合,其中的泛型是过滤器链,而不是一个个过滤器,配置的多个过滤器链就保存在 filterChains 变量中。
- filterChainValidator 是
FilterChainProxy
配置完成后的校验方法,默认使用的 NullFilterChainValidator 实际上对应了一个空方法,也就是不做任何校验。
接下来我们看一个过滤器中最重要的doFilter
方法:
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
在doFilter
方法中,正常来说,clearContext 参数每次都应该为 true,于是每次都先给 request 标记上 FILTER_APPLIED 属性,然后执行doFilterInternal
方法去走过滤器,执行完毕后,最后在 finally 代码块中清除SecurityContextHolder
中保存的用户信息,同时移除 request 中的标记。
按着这个顺序,我们来看看doFilterInternal
方法:
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
doFilterInternal
方法就比较重要了!
- 首先将请求封装成一个
FirewalledRequest
对象,在这个封装的过程中,也会判断请求是否合法。 - 对响应进行封装。
- 调用
getFilters()
方法找到过滤器链。该方法就是根据当前的请求,从 filterChains 中找到对应的过滤器链,然后由该过滤器链去处理请求。 - 如果找出来的 filters 为 null,或者集合中没有元素,那就是说明当前请求不需要经过过滤器。直接执行
chain.doFilter
,这个时候就又回到原生过滤器中去了。那么什么时候会发生这种情况呢?那就是针对项目中的静态资源,如果我们配置了资源放行,如web.ignoring().antMatchers("/hello")
,那么当你请求 /hello 接口时就会走到这里来,也就是说不经过 Spring Security Filters。 - 如果查询到的 filters 是有值的,那么这个 filters 集合中存放的就是我们要经过的过滤器链了。此时它会构造出一个虚拟的过滤器链
VirtualFilterChain
出来,并执行其中的doFilter
方法。
那么接下来我们就来看看VirtualFilterChain
:
private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain;
private final List<Filter> additionalFilters;
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0;
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
}
VirtualFilterChain
类中首先声明了5个全局属性,originalChain 表示原生的过滤器链,也就是 Web Filter;additionalFilters 表示 Spring Security 中的过滤器链;firewalledRequest 表示当前请求;size 表示 Spring Security 中的过滤器链中过滤器的个数;currentPosition 则是过滤器链遍历时候的下标。doFilter
方法就是 Spring Security 中过滤器挨个执行的过程,如果currentPosition == size
,表示过滤器链已经执行完毕,此时通过调用originalChain.doFilter
进入到原生过滤器链方法中,同时退出了 Spring Security 过滤器链。否则就从 additionalFilters 取出 Spring Security 过滤器链中的一个个过滤器,挨个调用 doFilter 方法。