Spring Security核心Filter执行流程

2022-08-08 10:07:30

这文章主要用来分析Spring Security中的过滤器链包含了哪些关键的过滤器,并且各自的作用是什么。

一、 Filter顺序

Spring Security的官方文档向我们提供了filter的顺序,无论实际应用中你用到了哪些,整体的顺序是保持不变的:

  1. ChannelProcessingFilter,重定向到其他协议的过滤器。也就是说如果你访问的channel错了,那首先就会在channel之间进行跳转,如http变为https。
  2. SecurityContextPersistenceFilter,请求来临时在SecurityContextHolder中建立一个SecurityContext,然后在请求结束的时候,清空SecurityContextHolder。并且任何对SecurityContext的改变都可以被copy到HttpSession。
  3. ConcurrentSessionFilter,因为它需要使用SecurityContextHolder的功能,而且更新对应session的最后更新时间,以及通过SessionRegistry获取当前的SessionInformation以检查当前的session是否已经过期,过期则会调用LogoutHandler。
  4. 认证处理机制,如UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,以至于SecurityContextHolder可以被更新为包含一个有效的Authentication请求。
  5. SecurityContextHolderAwareRequestFilter,它将会把HttpServletRequest封装成一个继承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,同时使用SecurityContext实现了HttpServletRequest中与安全相关的方法。
  6. JaasApiIntegrationFilter,如果SecurityContextHolder中拥有的Authentication是一个JaasAuthenticationToken,那么该Filter将使用包含在JaasAuthenticationToken中的Subject继续执行FilterChain。
  7. RememberMeAuthenticationFilter,如果之前的认证处理机制没有更新SecurityContextHolder,并且用户请求包含了一个Remember-Me对应的cookie,那么一个对应的Authentication将会设给SecurityContextHolder。
  8. AnonymousAuthenticationFilter,如果之前的认证机制都没有更新SecurityContextHolder拥有的Authentication,那么一个AnonymousAuthenticationToken将会设给SecurityContextHolder
  9. ExceptionTransactionFilter,用于处理在FilterChain范围内抛出的AccessDeniedException和AuthenticationException,并把它们转换为对应的Http错误码返回或者对应的页面。
  10. FilterSecurityInterceptor,保护Web URI,进行权限认证,并且在访问被拒绝时抛出异常。

以下顺序,由上往下
在这里插入图片描述

二、讲解一下几个重要的过滤器

  1. SecurityContextPersistenceFilter

    试想一下,如果我们不使用Spring Security,如果保存用户信息呢,大多数情况下会考虑使用Session对吧?在Spring Security中也是如此,用户在登录过一次之后,后续的访问便是通过sessionId来识别,从而认为用户已经被认证。具体在何处存放用户信息,便是SecurityContextHolder;认证相关的信息是如何被存放到其中的,便是通过SecurityContextPersistenceFilter。上面我们已经提到过,SecurityContextPersistenceFilter的两个主要作用便是请求来临时,创建SecurityContext安全上下文信息和请求结束时清空SecurityContextHolder。在使用NameSpace时,Spring Security默认会将SecurityContext保存在HttpSession中。但如果是基于微服务的话,对应在http的无状态也就意味着不允许存在session。这可以通过setAllowSessionCreation(false) 实现

publicclassSecurityContextPersistenceFilterextendsGenericFilterBean{staticfinalString FILTER_APPLIED="__spring_security_scpf_applied";//安全上下文存储的仓库privateSecurityContextRepository repo;publicSecurityContextPersistenceFilter(){//HttpSessionSecurityContextRepository是SecurityContextRepository接口的一个实现类//使用HttpSession来存储SecurityContextthis(newHttpSessionSecurityContextRepository());}publicvoiddoFilter(ServletRequest req,ServletResponse res,FilterChain chain)throwsIOException,ServletException{HttpServletRequest request=(HttpServletRequest) req;HttpServletResponse response=(HttpServletResponse) res;if(request.getAttribute(FILTER_APPLIED)!=null){// ensure that filter is only applied once per request
         chain.doFilter(request, response);return;}finalboolean debug= logger.isDebugEnabled();
      request.setAttribute(FILTER_APPLIED,Boolean.TRUE);if(forceEagerSessionCreation){HttpSession session= request.getSession();if(debug&& session.isNew()){
                logger.debug("Eagerly created session: "+ session.getId());}}HttpRequestResponseHolder holder=newHttpRequestResponseHolder(request,
            response);//从Session中获取安全上下文信息SecurityContext contextBeforeChainExecution= repo.loadContext(holder);try{//请求开始时,设置安全上下文信息,这样就避免了用户直接从Session中获取安全上下文信息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");}}}}
  1. 过滤器一般负责核心的处理流程,而具体的业务实现,通常交给其中聚合的其他实体类。例如存储安全上下文和读取安全上下文的工作完全委托给了HttpSessionSecurityContextRepository去处理:
publicclassHttpSessionSecurityContextRepositoryimplementsSecurityContextRepository{// 'SPRING_SECURITY_CONTEXT'是安全上下文默认存储在Session中的键值publicstaticfinalString SPRING_SECURITY_CONTEXT_KEY="SPRING_SECURITY_CONTEXT";...privatefinalObject contextObject=SecurityContextHolder.createEmptyContext();privateboolean allowSessionCreation=true;privateboolean disableUrlRewriting=false;privateString springSecurityContextKey= SPRING_SECURITY_CONTEXT_KEY;privateAuthenticationTrustResolver trustResolver=newAuthenticationTrustResolverImpl();//从当前request中取出安全上下文,如果session为空,则会返回一个新的安全上下文publicSecurityContextloadContext(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;}...publicbooleancontainsContext(HttpServletRequest request){HttpSession session= request.getSession(false);if(session==null){returnfalse;}return session.getAttribute(springSecurityContextKey)!=null;}privateSecurityContextreadSecurityContextFromSession(HttpSession httpSession){if(httpSession==null){returnnull;}...// Session存在的情况下,尝试获取其中的SecurityContextObject contextFromSession= httpSession.getAttribute(springSecurityContextKey);if(contextFromSession==null){returnnull;}...return(SecurityContext) contextFromSession;}//初次请求时创建一个新的SecurityContext实例protectedSecurityContextgenerateNewContext(){returnSecurityContextHolder.createEmptyContext();}}
  1. UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。

publicclassUsernamePasswordAuthenticationFilterextendsAbstractAuthenticationProcessingFilter{//用户名默认的参数名 可通过setUsernameParameter修改privateString usernameParameter= SPRING_SECURITY_FORM_USERNAME_KEY;//密码默认的参数名 可通过setPasswordParameter修改privateString passwordParameter= SPRING_SECURITY_FORM_PASSWORD_KEY;//是否只允许post请求privateboolean postOnly=true;publicAuthenticationattemptAuthentication(HttpServletRequest request,HttpServletResponse response)throwsAuthenticationException{if(postOnly&&!request.getMethod().equals("POST")){thrownewAuthenticationServiceException("Authentication method not supported: "+ request.getMethod());}//获取表单中的用户名和密码String username=obtainUsername(request);String password=obtainPassword(request);if(username==null){
            username="";}if(password==null){
            password="";}
        username= username.trim();//组装成username+password形式的tokenUsernamePasswordAuthenticationToken authRequest=newUsernamePasswordAuthenticationToken(
                username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);//交给内部的AuthenticationManager去认证,并返回认证后的信息returnthis.getAuthenticationManager().authenticate(authRequest);}}

UsernamePasswordAuthenticationFilter本身的代码只包含了上述这么一个方法,非常简略,而在其父类AbstractAuthenticationProcessingFilter中包含了大量的细节:

publicabstractclassAbstractAuthenticationProcessingFilterextendsGenericFilterBeanimplementsApplicationEventPublisherAware,MessageSourceAware{//包含了一个身份认证器privateAuthenticationManager authenticationManager;//用于实现remeberMeprivateRememberMeServices rememberMeServices=newNullRememberMeServices();privateRequestMatcher requiresAuthenticationRequestMatcher;//这两个Handler分别代表了认证成功和失败相应的处理器privateAuthenticationSuccessHandler successHandler=newSavedRequestAwareAuthenticationSuccessHandler();privateAuthenticationFailureHandler failureHandler=newSimpleUrlAuthenticationFailureHandler();publicvoiddoFilter(ServletRequest req,ServletResponse res,FilterChain chain)throwsIOException,ServletException{HttpServletRequest request=(HttpServletRequest) req;HttpServletResponse response=(HttpServletResponse) res;...Authentication authResult;try{//此处实际上就是调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法
            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);}//注意,认证成功后过滤器把authResult结果也传递给了成功处理器successfulAuthentication(request, response, chain, authResult);}}

整个流程主要就是调用了authenticationManager完成认证,根据认证结果执行successfulAuthentication或者unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理。

protectedvoidsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult)throwsIOException,ServletException{...
            successHandler.onAuthenticationSuccess(request, response, authResult);}

successHandler重定向到DefaultSavedRequest url

publicclassSavedRequestAwareAuthenticationSuccessHandlerextendsSimpleUrlAuthenticationSuccessHandler{@OverridepublicvoidonAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throwsServletException,IOException{SavedRequest savedRequest= requestCache.getRequest(request, response);if(savedRequest==null){super.onAuthenticationSuccess(request, response, authentication);return;}String targetUrlParameter=getTargetUrlParameter();if(isAlwaysUseDefaultTargetUrl()||(targetUrlParameter!=null&&StringUtils.hasText(request.getParameter(targetUrlParameter)))){
            requestCache.removeRequest(request, response);super.onAuthenticationSuccess(request, response, authentication);return;}//清理认证信息clearAuthenticationAttributes(request);// Use the DefaultSavedRequest URLString targetUrl= savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: "+ targetUrl);getRedirectStrategy().sendRedirect(request, response, targetUrl);}}

而它的父类SimpleUrlAuthenticationSuccessHandler里:

publicclassSimpleUrlAuthenticationSuccessHandlerextendsAbstractAuthenticationTargetUrlRequestHandler{publicvoidonAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throwsIOException,ServletException{handle(request, response, authentication);clearAuthenticationAttributes(request);}}

这个handle来自于AbstractAuthenticationTargetUrlRequestHandler:

publicabstractclassAbstractAuthenticationTargetUrlRequestHandler{protectedvoidhandle(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throwsIOException,ServletException{...
        redirectStrategy.sendRedirect(request, response, targetUrl);}}

failureHandler结构和successHander类似,有兴趣的可以研究一下。

在文章开头我们指出,配置了http.formLogin()后会自动加载UsernamePasswordAuthenticationFilter,那么是在什么时候进行加载filter呢?在FormLoginConfigurer中找到了利用父类AbstractAuthenticationFilterConfigurer进行了对filter的配置

publicFormLoginConfigurer(){super(newUsernamePasswordAuthenticationFilter(),null);usernameParameter("username");passwordParameter("password");}

而AbstractAuthenticationFilterConfigurer中:

publicabstractclassAbstractAuthenticationFilterConfigurerextends...{...//formLogin不出所料配置了AuthenticationEntryPointprivateLoginUrlAuthenticationEntryPoint authenticationEntryPoint;//认证失败的处理器privateAuthenticationFailureHandler failureHandler;...protectedAbstractAuthenticationFilterConfigurer(F authenticationFilter,String defaultLoginProcessingUrl){this();this.authFilter= authenticationFilter;if(defaultLoginProcessingUrl!=null){loginProcessingUrl(defaultLoginProcessingUrl);}}}

也就是说,formLogin()配置了之后最起码做了两件事,其一,为UsernamePasswordAuthenticationFilter设置了相关的配置,其二配置了AuthenticationEntryPoint。AuthenticationEntryPoint在下面的章节详细分析。

  1. AnonymousAuthenticationFilter

匿名认证过滤器,可能有人会想:匿名了还有身份?
Spring Security为了整体逻辑的统一性,即使是未通过认证的用户,也给予了一个匿名身份。而AnonymousAuthenticationFilter位于常用的身份认证过滤器(如UsernamePasswordAuthenticationFilter、RememberMeAuthenticationFilter)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext依旧没有用户信息,AnonymousAuthenticationFilter会给予用户一个匿名身份。

publicclassAnonymousAuthenticationFilterextendsGenericFilterBeanimplementsInitializingBean{privateAuthenticationDetailsSource<HttpServletRequest,?> authenticationDetailsSource=newWebAuthenticationDetailsSource();privateString key;privateObject principal;privateList<GrantedAuthority> authorities;//自动创建一个"anonymousUser"的匿名用户,其具有ANONYMOUS角色publicAnonymousAuthenticationFilter(String key){this(key,"anonymousUser",AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));}...publicvoiddoFilter(ServletRequest req,ServletResponse res,FilterChain chain)throwsIOException,ServletException{//过滤器链都执行到匿名认证过滤器还没有身份信息,塞一个匿名身份进去if(SecurityContextHolder.getContext().getAuthentication()==null){SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));}
      chain.doFilter(req, res);}protectedAuthenticationcreateAuthentication(HttpServletRequest request){//创建一个AnonymousAuthenticationTokenAnonymousAuthenticationToken auth=newAnonymousAuthenticationToken(key,
            principal, authorities);
      auth.setDetails(authenticationDetailsSource.buildDetails(request));return auth;}...}

到这里可以看出,AnonymousAuthenticationFilter和UsernamePasswordAuthenticationFilter都是对Authentication进行一系列操作,这就印证了前面说要创建一个全局的SecurityContext,来把一系列的过滤器串联起来。

  1. ExceptionTranslationFilter

通过前面的介绍我们知道在Spring Security的Filter链表中ExceptionTranslationFilter就放在FilterSecurityInterceptor的前面。而ExceptionTranslationFilter是捕获来自FilterChain的异常,并对这些异常做处理。ExceptionTranslationFilter能够捕获来自FilterChain所有的异常,但是它只会处理两类异常,AuthenticationException和AccessDeniedException,其它的异常它会继续抛出。如果捕获到的是AuthenticationException,那么将会使用其对应的AuthenticationEntryPoint的commence()处理。如果捕获的异常是一个AccessDeniedException,那么将视当前访问的用户是否已经登录认证做不同的处理,如果未登录,则会使用关联的AuthenticationEntryPoint的commence()方法进行处理,否则将使用关联的AccessDeniedHandler的handle()方法进行处理。

publicclassExceptionTranslationFilterextendsGenericFilterBean{//处理异常转换的核心方法privatevoidhandleSpringSecurityException(HttpServletRequest request,HttpServletRespon
  • 作者:cristianoxm
  • 原文链接:https://ximeneschen.blog.csdn.net/article/details/123903425
    更新时间:2022-08-08 10:07:30