前面的部分,我们关注了 Spring Security 是如何完成认证工作的,但是另外一部分核心的内容:过滤器,一直没有提到,我们已经知道 Spring Security 使用了 springSecurityFillterChian 作为了安全过滤的入口,这一节主要分析一下这个过滤器链都包含了哪些关键的过滤器,并且各自的使命是什么。
4 过滤器详解 4.1 核心过滤器概述 由于过滤器链路中的过滤较多,即使是 Spring Security 的官方文档中也并未对所有的过滤器进行介绍,在之前,《Spring Security(二)–Guides》入门指南中我们配置了一个表单登录的 demo,以此为例,来看看这过程中 Spring Security 都帮我们自动配置了哪些过滤器。
1 2 3 4 5 6 7 8 9 10 11 12 Creating filter chain: o.s.s.web.util.matcher.AnyRequestMatcher@1 , [o.s.s.web.context.SecurityContextPersistenceFilter@8851ce1, o.s.s.web.header.HeaderWriterFilter@6a472566, o.s.s.web.csrf.CsrfFilter@61cd1c71, o.s.s.web.authentication.logout.LogoutFilter@5e1d03d7, o.s.s.web.authentication.UsernamePasswordAuthenticationFilter@122d6c22, o.s.s.web.savedrequest.RequestCacheAwareFilter@5ef6fd7f, o.s.s.web.servletapi.SecurityContextHolderAwareRequestFilter@4beaf6bd, o.s.s.web.authentication.AnonymousAuthenticationFilter@6edcad64, o.s.s.web.session.SessionManagementFilter@5e65afb6, o.s.s.web.access.ExceptionTranslationFilter@5b9396d3, o.s.s.web.access.intercept.FilterSecurityInterceptor@3c5dbdf8 ]
上述的 log 信息是我从 springboot 启动的日志中 CV 所得,spring security 的过滤器日志有一个特点:log 打印顺序与实际配置顺序符合,也就意味着 SecurityContextPersistenceFilter
是整个过滤器链的第一个过滤器,而 FilterSecurityInterceptor
则是末置的过滤器。另外通过观察过滤器的名称,和所在的包名,可以大致地分析出他们各自的作用,如 UsernamePasswordAuthenticationFilter
明显便是与使用用户名和密码登录相关的过滤器,而 FilterSecurityInterceptor
我们似乎看不出它的作用,但是其位于 web.access
包下,大致可以分析出他与访问限制相关。第四篇文章主要就是介绍这些常用的过滤器,对其中关键的过滤器进行一些源码分析。先大致介绍下每个过滤器的作用:
SecurityContextPersistenceFilter 两个主要职责:请求来临时,创建 SecurityContext
安全上下文信息,请求结束时清空 SecurityContextHolder
。
HeaderWriterFilter (文档中并未介绍,非核心过滤器) 用来给 http 响应添加一些 Header, 比如 X-Frame-Options, X-XSS-Protection*,X-Content-Type-Options.
CsrfFilter 在 spring4 这个版本中被默认开启的一个过滤器,用于防止 csrf 攻击,了解前后端分离的人一定不会对这个攻击方式感到陌生,前后端使用 json 交互需要注意的一个问题。
LogoutFilter 顾名思义,处理注销的过滤器
UsernamePasswordAuthenticationFilter 这个会重点分析,表单提交了 username 和 password,被封装成 token 进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。
RequestCacheAwareFilter (文档中并未介绍,非核心过滤器) 内部维护了一个 RequestCache,用于缓存 request 请求
SecurityContextHolderAwareRequestFilter 此过滤器对 ServletRequest 进行了一次包装,使得 request 具有更加丰富的 API
AnonymousAuthenticationFilter 匿名身份过滤器,这个过滤器个人认为很重要,需要将它与 UsernamePasswordAuthenticationFilter 放在一起比较理解,spring security 为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
SessionManagementFilter 和 session 相关的过滤器,内部维护了一个 SessionAuthenticationStrategy,两者组合使用,常用来防止 session-fixation protection attack
,以及限制同一用户开启多个会话的数量
ExceptionTranslationFilter 直译成异常翻译过滤器,还是比较形象的,这个过滤器本身不处理异常,而是将认证过程中出现的异常交给内部维护的一些类去处理,具体是那些类下面详细介绍
FilterSecurityInterceptor 这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。
其中加粗的过滤器可以被认为是 Spring Security 的核心过滤器,将在下面,一个过滤器对应一个小节来讲解。
4.2 SecurityContextPersistenceFilter 试想一下,如果我们不使用 Spring Security,如果保存用户信息呢,大多数情况下会考虑使用 Session 对吧?在 Spring Security 中也是如此,用户在登录过一次之后,后续的访问便是通过 sessionId 来识别,从而认为用户已经被认证。具体在何处存放用户信息,便是第一篇文章中提到的 SecurityContextHolder;认证相关的信息是如何被存放到其中的,便是通过 SecurityContextPersistenceFilter。在 4.1 概述中也提到了,SecurityContextPersistenceFilter 的两个主要作用便是请求来临时,创建 SecurityContext
安全上下文信息和请求结束时清空 SecurityContextHolder
。顺带提一下:微服务的一个设计理念需要实现服务通信的无状态,而 http 协议中的无状态意味着不允许存在 session,这可以通过 setAllowSessionCreation(false)
实现,这并不意味着 SecurityContextPersistenceFilter 变得无用,因为它还需要负责清除用户信息。在 Spring Security 中,虽然安全上下文信息被存储于 Session 中,但我们在实际使用中不应该直接操作 Session,而应当使用 SecurityContextHolder。
源码分析 org.springframework.security.web.context.SecurityContextPersistenceFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class SecurityContextPersistenceFilter extends GenericFilterBean { static final String FILTER_APPLIED = "__spring_security_scpf_applied" ; private SecurityContextRepository repo; public SecurityContextPersistenceFilter () { this (new HttpSessionSecurityContextRepository()); } public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (request.getAttribute(FILTER_APPLIED) != null ) { chain.doFilter(request, response); return ; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); SecurityContextHolder.clearContext(); repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed" ); } } } }
过滤器一般负责核心的处理流程,而具体的业务实现,通常交给其中聚合的其他实体类,这在 Filter 的设计中很常见,同时也符合职责分离模式。例如存储安全上下文和读取安全上下文的工作完全委托给了 HttpSessionSecurityContextRepository 去处理,而这个类中也有几个方法可以稍微解读下,方便我们理解内部的工作流程
org.springframework.security.web.context.HttpSessionSecurityContextRepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class HttpSessionSecurityContextRepository implements SecurityContextRepository { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT" ; ... private final Object contextObject = SecurityContextHolder.createEmptyContext(); private boolean allowSessionCreation = true ; private boolean disableUrlRewriting = false ; private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY; private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); 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(); } ... return context; } ... public boolean containsContext (HttpServletRequest request) { HttpSession session = request.getSession(false ); if (session == null ) { return false ; } return session.getAttribute(springSecurityContextKey) != null ; } private SecurityContext readSecurityContextFromSession (HttpSession httpSession) { if (httpSession == null ) { return null ; } ... Object contextFromSession = httpSession.getAttribute(springSecurityContextKey); if (contextFromSession == null ) { return null ; } ... return (SecurityContext) contextFromSession; } protected SecurityContext generateNewContext () { return SecurityContextHolder.createEmptyContext(); } }
SecurityContextPersistenceFilter 和 HttpSessionSecurityContextRepository 配合使用,构成了 Spring Security 整个调用链路的入口,为什么将它放在最开始的地方也是显而易见的,后续的过滤器中大概率会依赖 Session 信息和安全上下文信息。
4.3 UsernamePasswordAuthenticationFilter 表单认证是最常用的一个认证方式,一个最直观的业务场景便是允许用户在表单中输入用户名和密码进行登录,而这背后的 UsernamePasswordAuthenticationFilter,在整个 Spring Security 的认证体系中则扮演着至关重要的角色。
上述的时序图,可以看出 UsernamePasswordAuthenticationFilter 主要肩负起了调用身份认证器,校验身份的作用,至于认证的细节,在前面几章花了很大篇幅进行了介绍,到这里,其实 Spring Security 的基本流程就已经走通了。
源码分析 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = obtainUsername(request); String password = obtainPassword(request); ... username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest); }
UsernamePasswordAuthenticationFilter
本身的代码只包含了上述这么一个方法,非常简略,而在其父类 AbstractAuthenticationProcessingFilter
中包含了大量的细节,值得我们分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { private AuthenticationManager authenticationManager; private RememberMeServices rememberMeServices = new NullRememberMeServices(); private RequestMatcher requiresAuthenticationRequestMatcher; private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; ... Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null ) { return ; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { unsuccessfulAuthentication(request, response, failed); return ; } catch (AuthenticationException failed) { unsuccessfulAuthentication(request, response, failed); return ; } if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); } }
整个流程理解起来也并不难,主要就是内部调用了 authenticationManager 完成认证,根据认证结果执行 successfulAuthentication 或者 unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理,不再细究 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,有兴趣的朋友,可以去看看两者的实现类。
4.4 AnonymousAuthenticationFilter 匿名认证过滤器,可能有人会想:匿名了还有身份?我自己对于 Anonymous 匿名身份的理解是 Spirng Security 为了整体逻辑的统一性,即使是未通过认证的用户,也给予了一个匿名身份。而 AnonymousAuthenticationFilter
该过滤器的位置也是非常的科学的,它位于常用的身份认证过滤器(如 UsernamePasswordAuthenticationFilter
、BasicAuthenticationFilter
、RememberMeAuthenticationFilter
)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext 依旧没有用户信息,AnonymousAuthenticationFilter
该过滤器才会有意义 —- 基于用户一个匿名身份。
源码分析 org.springframework.security.web.authentication.AnonymousAuthenticationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean { private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); private String key; private Object principal; private List<GrantedAuthority> authorities; public AnonymousAuthenticationFilter (String key) { this (key, "anonymousUser" , AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS" )); } public AnonymousAuthenticationFilter (String key, Object principal, List<GrantedAuthority> authorities) { Assert.hasLength(key, "key cannot be null or empty" ); Assert.notNull(principal, "Anonymous authentication principal must be set" ); Assert.notNull(authorities, "Anonymous authorities must be set" ); this .key = key; this .principal = principal; this .authorities = authorities; } ... public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (SecurityContextHolder.getContext().getAuthentication()== null ) { SecurityContextHolder.getContext().setAuthentication( createAuthentication((HttpServletRequest) req)); } chain.doFilter(req, res); } protected Authentication createAuthentication (HttpServletRequest request) { AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, principal, authorities); auth.setDetails(authenticationDetailsSource.buildDetails(request)); return auth; } ... }
其实对比 AnonymousAuthenticationFilter 和 UsernamePasswordAuthenticationFilter 就可以发现一些门道了,UsernamePasswordAuthenticationToken 对应 AnonymousAuthenticationToken,他们都是 Authentication 的实现类,而 Authentication 则是被 SecurityContextHolder(SecurityContext) 持有的,一切都被串联在了一起。
4.5 ExceptionTranslationFilter ExceptionTranslationFilter 异常转换过滤器位于整个 springSecurityFilterChain 的后方,用来转换整个链路中出现的异常,将其转化,顾名思义,转化以意味本身并不处理。一般其只处理两大类异常:AccessDeniedException 访问异常和 AuthenticationException 认证异常。
这个过滤器非常重要,因为它将 Java 中的异常和 HTTP 的响应连接在了一起,这样在处理异常时,我们不用考虑密码错误该跳到什么页面,账号锁定该如何,只需要关注自己的业务逻辑,抛出相应的异常便可。如果该过滤器检测到 AuthenticationException,则将会交给内部的 AuthenticationEntryPoint 去处理,如果检测到 AccessDeniedException,需要先判断当前用户是不是匿名用户,如果是匿名访问,则和前面一样运行 AuthenticationEntryPoint,否则会委托给 AccessDeniedHandler 去处理,而 AccessDeniedHandler 的默认实现,是 AccessDeniedHandlerImpl。所以 ExceptionTranslationFilter 内部的 AuthenticationEntryPoint 是至关重要的,顾名思义:认证的入口点。
源码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class ExceptionTranslationFilter extends GenericFilterBean { private void handleSpringSecurityException (HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { sendStartAuthentication(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( "Full authentication is required to access this resource" )); } else { accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); } } } }
剩下的便是要搞懂 AuthenticationEntryPoint 和 AccessDeniedHandler 就可以了。
选择了几个常用的登录端点,以其中第一个为例来介绍,看名字就能猜到是认证失败之后,让用户跳转到登录页面。还记得我们一开始怎么配置表单登录页面的吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/" , "/home" ).permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login" ) .permitAll() .and() .logout() .permitAll(); } }
我们顺着 formLogin 返回的 FormLoginConfigurer 往下找,看看能发现什么,最终在 FormLoginConfigurer 的父类 AbstractAuthenticationFilterConfigurer 中有了不小的收获:
1 2 3 4 5 6 7 8 public abstract class AbstractAuthenticationFilterConfigurer extends ... { ... private LoginUrlAuthenticationEntryPoint authenticationEntryPoint; private AuthenticationFailureHandler failureHandler; ... }
具体如何配置的就不看了,我们得出了结论,formLogin() 配置了之后最起码做了两件事,其一,为 UsernamePasswordAuthenticationFilter 设置了相关的配置,其二配置了 AuthenticationEntryPoint。
登录端点还有 Http401AuthenticationEntryPoint,Http403ForbiddenEntryPoint 这些都是很简单的实现,有时候我们访问受限页面,又没有配置登录,就看到了一个空荡荡的默认错误页面,上面显示着 401,403,就是这两个入口起了作用。
还剩下一个 AccessDeniedHandler 访问决策器未被讲解,简单提一下:AccessDeniedHandlerImpl 这个默认实现类会根据 errorPage 和状态码来判断,最终决定跳转的页面
org.springframework.security.web.access.AccessDeniedHandlerImpl#handle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { if (!response.isCommitted()) { if (errorPage != null ) { request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); response.setStatus(HttpServletResponse.SC_FORBIDDEN); RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); dispatcher.forward(request, response); } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } } }
4.6 FilterSecurityInterceptor 想想整个认证安全控制流程还缺了什么?我们已经有了认证,有了请求的封装,有了 Session 的关联… 还缺一个:由什么控制哪些资源是受限的,这些受限的资源需要什么权限,需要什么角色… 这一切和访问控制相关的操作,都是由 FilterSecurityInterceptor 完成的。
FilterSecurityInterceptor 的工作流程用笔者的理解可以理解如下:FilterSecurityInterceptor 从 SecurityContextHolder 中获取 Authentication 对象,然后比对用户拥有的权限和资源所需的权限。前者可以通过 Authentication 对象直接获得,而后者则需要引入我们之前一直未提到过的两个类:SecurityMetadataSource,AccessDecisionManager。理解清楚决策管理器的整个创建流程和 SecurityMetadataSource 的作用需要花很大一笔功夫,这里,暂时只介绍其大概的作用。
在 JavaConfig 的配置中,我们通常如下配置路径的访问控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override protected void configure (HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/resources/**" , "/signup" , "/about" ).permitAll() .antMatchers("/admin/**" ).hasRole("ADMIN" ) .antMatchers("/db/**" ).access("hasRole('ADMIN') and hasRole('DBA')" ) .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { public <O extends FilterSecurityInterceptor> O postProcess ( O fsi) { fsi.setPublishAuthorizationSuccess(true ); return fsi; } }); }
在 ObjectPostProcessor 的泛型中看到了 FilterSecurityInterceptor,以笔者的经验,目前并没有太多机会需要修改 FilterSecurityInterceptor 的配置。
总结 本篇文章在介绍过滤器时,顺便进行了一些源码的分析,目的是方便理解整个 Spring Security 的工作流。伴随着整个过滤器链的介绍,安全框架的轮廓应该已经浮出水面了,下面的章节,主要打算通过自定义一些需求,再次分析其他组件的源码,学习应该如何改造 Spring Security,为我们所用。
** 欢迎关注我的微信公众号:「Kirito 的技术分享」,关于文章的任何疑问都会得到回复,带来更多 Java 相关的技术分享。**