学习理解SpringSecurity的几个关键问题

1、默认存在哪些filter,具体执行顺序

  • ChannelProcessingFilter,如果你访问的 channel 错了,那首先就会在channel 之间进行跳转,如 http 变为 https。

  • SecurityContextPersistenceFilter,这样的话在一开始进行 request 的时候就可以在 SecurityContextHolder 中建立一个SecurityContext,然后在请求结束的时候,任何对 SecurityContext 的改变都可以被 copy 到 HttpSession。

  • ConcurrentSessionFilter,因为它需要使用 SecurityContextHolder 的功能,而且更新对应 session 的最后更

  • 新时间,以及通过 SessionRegistry 获取当前的 SessionInformation 以检查当前的 session 是否已经过期,过期则会调用 LogoutHandler。

  • HeaderWriterFilter

  • CsrfFilter

  • LogoutFilter

  • X509AuthenticationFilter

  • AbstractPreAuthenticatedProcessingFilter

  • 认证处理机制,如 UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter 等,以至于 SecurityContextHolder 可以被更新为包含一个有效的 Authentication 请求。

  • SecurityContextHolderAwareRequestFilter,它将会把 HttpServletRequest 封装成一个继承自 HttpServletRequestWrapper 的 SecurityContextHolderAwareRequestWrapper,同时使用 SecurityContext 实现了 HttpServletRequest 中与安全相关的方法。

    扫描二维码关注公众号,回复: 4348837 查看本文章
  • JaasApiIntegrationFilter,如果 SecurityContextHolder 中拥有的 Authentication 是一个 JaasAuthenticationToken,那么该 Filter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain。

  • RememberMeAuthenticationFilter,如果之前的认证处理机制没有更新 SecurityContextHolder,并且用户请求包含了一个 Remember-Me 对应的 cookie,那么一个对应的 Authentication 将会设给 SecurityContextHolder。

  • AnonymousAuthenticationFilter,如果之前的认证机制都没有更新 SecurityContextHolder 拥有的 Authentication,那么一个 AnonymousAuthenticationToken 将会设给 SecurityContextHolder。

  • SessionManagementFilter

  • ExceptionTransactionFilter,用于处理在 FilterChain 范围内抛出的 AccessDeniedException 和 AuthenticationException,并把它们转换为对应的 Http 错误码返回或者对应的页面。

  • FilterSecurityInterceptor,保护 Web URI,并且在访问被拒绝时抛出异常。


2、重点了解SecurityContextPersistenceFilter,session是如何进行操作,做到任何对SecurityContext的改变都可以被copy到HttpSession中。

先查看其中doFilter方法的代码

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        .......
        //省略部分代码
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        //获取SecurityContext,其中是从httpsession中获取,如果httpsession中没有,则新建并返回一个新的
       SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
 
        try {
            //将返回的securityContext保存到本地线程中,方便将来访问
           SecurityContextHolder.setContext(contextBeforeChainExecution);
 
            chain.doFilter(holder.getRequest(), holder.getResponse());
 
        } finally {
            //获取之前保存的securityContext
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything else.
            //清空SecurityContextHolder中的Context            
            SecurityContextHolder.clearContext();
            //将Security保存到HttpSession中,下次请求的时候就可以利用保存好的SecurityContext
           repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);
 
            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }

再看一下loadContext方法

    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpServletResponse response = requestResponseHolder.getResponse();
        HttpSession httpSession = request.getSession(false);
        //从这就可以看出来,SecurityContext的获取就是从Httpsession中获得的,所以根据java的传址特性,就可以判断出Httpsession会同步SecurityContext的变化
        SecurityContext context = readSecurityContextFromSession(httpSession);
 
        if (context == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("No SecurityContext was available from the HttpSession: " + httpSession +". " +
                        "A new one will be created.");
            }
            //如果当然session中没有SecurityContext,则重新创建一个新的Context 
           context = generateNewContext();
 
        }
 
        requestResponseHolder.setResponse(
                new SaveToSessionResponseWrapper(response, request, httpSession != null, context));
 
        return context;
    }

3、重点了解AbstractAuthenticationProcessingFilter,了解一下授权

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
--> 1       authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
--> 2       unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
--> 3      unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

--> 4   successfulAuthentication(request, response, chain, authResult);
    }

—>3 处代码表示身份校验失败之后调用方法unsuccessfulAuthentication()

该方法如下

protected void unsuccessfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        SecurityContextHolder.clearContext();

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication request failed: " + failed.toString(), failed);
            logger.debug("Updated SecurityContextHolder to contain null Authentication");
            logger.debug("Delegating to authentication failure handler " + failureHandler);
        }

        rememberMeServices.loginFail(request, response);

        failureHandler.onAuthenticationFailure(request, response, failed);
    }

最重要的一条语句是

failureHandler.onAuthenticationFailure(request,response,failed);

—>4 处代码表示验证身份信息成功后,调用successfulAuthentication()方法

该方法代码如下

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }

        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

最重要的一行代码是

successfulHandler.onAuthenticationSuccess(request,response,authResult);

image


4、重点了解FilterSecurityInterceptor,了解没有授权情况下,security是如何处理的

1

  • FilterSecurityInterceptor的创建是在WebSecurityConfigurerAdapter的configure方法,当我们调用http.authorizeRequests()时,会创建一个ExpressionUrlAuthorizationConfigurer添加到httpsecurity的configurers中,所有的configurers会在httpsecurity调用build方法的时候配置。

  • FilterSecurityInterceptor的逻辑

    • 默认的AccessDecisionManager是AffirmativeBased

    • 默认的AccessDecisionVoter是WebExpressionVoter(对于ExpressionUrlAuthorizationConfigurer)

      • AffirmativeBased :至少一个投票者必须决定授予访问权限
      • ConsensusBased :多数投票者必须授予访问权限
      • UnanimousBased:所有投票者都必须投票或放弃投票授予访问权限(无投票表决拒绝访问)
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
        Filter {

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null)
                && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && observeOncePerRequest) {
            // filter already applied to this request and user wants us to observe
            // once-per-request handling, so don't re-do security checking
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        else {
            // first time this request being called, so perform security checking
            if (fi.getRequest() != null) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, null);
        }
    }
    //......
}  

如果这个filter之前没有执行过的话,那么首先执行的

InterceptorStatusToken token = super.beforeInvocation(fi);

这个是由AbstractSecurityInterceptor提供。

它就是springsecurity处理鉴权的入口。

AbstractSercurityInterceptor
protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        final boolean debug = logger.isDebugEnabled();

        if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
            throw new IllegalArgumentException(
                    "Security invocation attempted for object "
                            + object.getClass().getName()
                            + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                            + getSecureObjectClass());
        }

        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

        if (attributes == null || attributes.isEmpty()) {
            if (rejectPublicInvocations) {
                throw new IllegalArgumentException(
                        "Secure object invocation "
                                + object
                                + " was denied as public invocations are not allowed via this interceptor. "
                                + "This indicates a configuration error because the "
                                + "rejectPublicInvocations property is set to 'true'");
            }

            if (debug) {
                logger.debug("Public object - authentication not attempted");
            }

            publishEvent(new PublicInvocationEvent(object));

            return null; // no further work post-invocation
        }

        if (debug) {
            logger.debug("Secure object: " + object + "; Attributes: " + attributes);
        }

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            credentialsNotFound(messages.getMessage(
                    "AbstractSecurityInterceptor.authenticationNotFound",
                    "An Authentication object was not found in the SecurityContext"),
                    object, attributes);
        }

        Authentication authenticated = authenticateIfRequired();

        // Attempt authorization
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }

        if (debug) {
            logger.debug("Authorization successful");
        }

        if (publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
                attributes);

        if (runAs == null) {
            if (debug) {
                logger.debug("RunAsManager did not change Authentication object");
            }

            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                    attributes, object);
        }
        else {
            if (debug) {
                logger.debug("Switching to RunAs Authentication: " + runAs);
            }

            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);

            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }
  • 这里调用了accessDecisionManager来进行判断,如果没有登录态,在到达这个filter之前会先经过AnonymousAuthenticationFilter.java,其Authentication的值为AnonymousAuthenticationToken。如果匿名请求需要登录态的url,或者权限不够,则抛出AccessDeniedException。

5、了解spring security如何进行资源访问权限控制

参考一下两个链接

SpringBoot + SpringSecurity 控制授权

Spring Security 控制授权


6、添加自定义Filter到FilterChain

public class BeforeLoginFilter extends GenericFilterBean { 
  @Override 
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException { 
      
         System.out.println("This is a filter before UsernamePasswordAuthenticationFilter.");
        // 继续调用 Filter 链
        filterChain.doFilter(servletRequest, servletResponse); 
   }
}


配置自定义Filter在SpringSecurity过滤器链中的位置

protected void configure(HttpSecurity http) throws Exception{
    http
         .authorizeRequests()
         .antMatchers("/").permitAll()
         .antMatchers("/user/**").hasRole("USER")
         .and()
         .formLogin().loginPage("/login").defaultSuccessUrl("/user")
         .and()
         .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
    
    // 在 UsernamePasswordAuthenticationFilter 前添加 BeforeLoginFilter
    http.addFilterBefore(new BeforeLoginFilter(), UsernamePasswordAuthenticationFilter.class);
    
    // 在 CsrfFilter 后添加 AfterCsrfFilter 
    http.addFilterAfter(new AfterCsrfFilter(), CsrfFilter.class);

    
}


说明: HttpSecurity 有三个常用方法来配置:
  • addFilterBefore(Filter filter, Class beforeFilter) 在 beforeFilter 之前添加 filter

  • addFilterAfter(Filter filter, Class afterFilter) 在 afterFilter 之后添加 filter

  • addFilterAt(Filter filter, Class atFilter) 在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter

通过在不同 Filter 的 doFilter() 方法中加断点调试,可以判断哪个 filter 先执行,从而判断 filter 的执行顺序 。

猜你喜欢

转载自blog.csdn.net/weixin_40862011/article/details/84728347