SpringSecurity过滤器分析
WebAsyncManagerIntegrationFilter
比较详细的使用可以看这篇或者SpringMVC执行流程,源码分析
该过滤器配合SpringMVC的异步模式,SecurityContext是和线程绑定在一起的并且是在SecurityContextPersistenceFilter被设置,当进入Controller返回结果需要异步执行时,会直接走DispatcherServlet和过滤器链,当前线程退出,response并不返回。
当异步有结果后,此请求会再被转发到DispatcherServlet(不经过滤器链)。如果异步执行时需要用到SecurityContext,必须将已经设置的SecurityContext保存到request域中的WebAsynvManager中的SecurityContextCallableProcessingInterceptor中,这个类比较简单,就是在异步处理前后准备好设置好SecurityContextHolder。
该过滤器的源码如下,逻辑十分简单。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
if (securityProcessingInterceptor == null) {
/* 设置异步处理的拦截器 */
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
new SecurityContextCallableProcessingInterceptor());
}
filterChain.doFilter(request, response);
}
由于是为了配合SpringMVC,该过滤器无法禁用
SecurityContextPersistenceFilter
通过SecurityContextRepository加载、保存SecurityContext。默认是基于Session的,如需自定义context的管理,实习SecurityContextRepository接口即可。
其中的HttpSessionSecurityContextRepository基本帮我们做好了所有事情,我们只需要放心的设置、修改context、authentication,这些都会被其检测到,然后更新session,并保存。通过以下两个类完成监测:
- SaveToSessionResponseWrapper
- SaveToSessionRequestWrapper
SecurityContextPersistenceFilter逻辑比较简单,下面是默认的loadContext方法:
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
context = generateNewContext();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Created %s", context));
}
}
/* 保存了当前response、request、session和context以实现检测 */
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
return context;
}
该过滤器可以禁用,但一般情况下我们可以选择实现SecurityContextRepository而不是重写过滤器。
HeaderWriterFilter
向当前响应添加响应头,像X-Frame-Options、X-XSS-Protection 和 X-Content-Type-Options,自实现也比较简单。
该过滤器可以被禁用。
CsrfFilter
csrf防护,一般情况下用现成的就行,可自定义csfrToken的管理方式,该过滤器可以被禁用。
LogoutFilter
用于实现登出的,较为简单。如果使用此过滤器登出,只需要实现LogoutHandler,LogoutSuccessHandler,请求不会到达Controller。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Logging out [%s]", auth));
}
this.handler.logout(request, response, auth);
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
UsernamePasswordAuthenticationFilter
该过滤器默认被禁用,除非了配置了formLogin,实际上用处也不大。用与用户名密码登录校验,生成authentication。
如果需要编写自己的AuthenticationFilter,这是个很好的参考。
DefaultLoginPageGeneratingFilter
配置了formLogin但没配置loginPage时用于生成默认登录页
DefaultLogoutPageGeneratingFilter
配置了formLogin没logoutPage时生成默认登出页
RememberMeAuthenticationFilter
配置rememberMe时使用
RequestCacheAwareFilter
请求缓存,主要逻辑在HttpSessionRequestCache中,可以自定义RequestCache,更改缓存行为。该过滤器可以被禁用。
在SpringSecurity中,缓存主要用于权限不足被重定向到认证流程,通过之后复用缓存请求,如果选择处理新请求,该过滤器没用
SecurityContextHolderAwareRequestFilter
将request再包装为Servlet3SecurityContextHolderAwareRequestWrapper,提供以下附加方法:
HttpServletRequest.authenticate(HttpServletResponse) - 允许用户确定他们是否通过身份验证,如果没有,则将用户发送到登录页面,通过AuthenticationEntryPoint实现。
HttpServletRequest.login(String, String) - 允许用户使用AuthenticationManager进行身份AuthenticationManager。
HttpServletRequest.logout() - 允许用户使用Spring Security中配置的LogoutHandler注销。
AsyncContext.start(Runnable) -自动复制SecurityContext从SecurityContextHolder对调用线程发现AsyncContext.start(Runnable)来处理该线程Runnable 。
Sevlet规范和该过滤器为操作request提供了更大的灵活性和扩展性。更多内容可以查看这里,如使用的都是原生HttpServletRequest,该过滤器可以被禁用。
AnonymousAuthenticationFilter
如果当前context的authentication为null,为当前context设置anonymousAuthentication,默认为:
/* key默认为启动时随机的uuid,可自定义 */
/* principal为anonymousUser */
/* authorities为ROLE_ANONYMOUS */
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(this.key, this.principal,
this.authorities);
SessionManagementFilter
直接上源码
if (!this.securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
// The user has been authenticated during the current request, so call the
// session strategy
try {
this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
}
catch (SessionAuthenticationException ex) {
// The session strategy can reject the authentication
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
SecurityContextHolder.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, ex);
return;
}
// Eagerly save the security context to make it available for any possible
// re-entrant requests which may occur before the current request
// completes. SEC-1396.
this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Request requested invalid session id %s",
request.getRequestedSessionId()));
}
if (this.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
下面讨论的是基于Session的时候。
- 如果当前请求的context尚未保存(此处的SecurityContextReposiry与上文为同一对象),那么该JSESSIONID是第一次请求服务器,直接放行;
- 如果不为空且authentication不为AnonymousAuthentication,可以自定义session的校验处理工作,默认什么也不做,然后保存上下文到repositry中
- 如果没有SecurityContext或者Authentication,检查session是否合法,若不合法,自定义后续动作,默认重定向,然后直接从当前过滤器返回,不放行。
如果没有检查校验session的需求,该过滤器可直接禁用。
ExceptionTranslationFilter
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
如果检测到AuthenticationException ,过滤器将启动authenticationEntryPoint,默认返回UNAUTHORIZED,401状态码
如果检测到AccessDeniedException ,过滤器将确定用户是否为匿名用户。如果他们是匿名用户,则将启动authenticationEntryPoint。如果他们不是匿名用户,过滤器将委托给AccessDeniedHandler。默认情况下,返回FOBBIDEN,403。
当用户需要访问某个页面但权限不足时,此过滤器捕获到异常,通过RequestCache保存当前请求,然后转到认证授权,通过之后,继续访问之前的页面,在RequestCacheAware中获取缓存的请求,继续处理。
如果在全局异常处理中配置了AuthenticationException和AccessDeniedException,该过滤器永远不会捕获到Controller层的异常
FilterSecurityInterceptor
通过前面的authentication判断用户是否有权限访问某个资源,一般情况下不需要改动
总结
无怪网友大多说SpringSecurity使用成本高,但是价值不大,许多功能看起来很鸡肋,添加的过滤器有点多,从一些过滤器来看,有前后端不分离时代的味道。
本人配置的SpringSecurity,几乎没剩下什么了,自己写几个Filter估计也不是太难,网友诚不欺我。
/* 自定义SecurityContextRepositry */
httpSecurity.securityContext().securityContextRepository(jwtSecurityContextRepositry);
/*不需要检测校验session*/
httpSecurity.sessionManagement().disable();
/* 使用HttpServlet,没有包装需求 */
httpSecurity.servletApi().disable();
/* 在全局处理器中处理异常 */
httpSecurity.exceptionHandling().disable();
/*登入登出在Controller中处理*/
httpSecurity.logout().disable();
/* 请求头不需要额外操作 */
httpSecurity.headers().disable();
/*重定向认证后重新处理请求*/
httpSecurity.requestCache().disable();
/* 不配置自动禁用 */
/*httpSecurity.formLogin();*/
/*httpSecurity.rememberMe();*/
最后还剩下不过五个SpringSecurity的Filter。
Q.E.D.