欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解

程序员文章站 2022-06-02 23:22:39
...

FilterSecurityInterceptor 作为 Spring Security Filter Chain 的最后一个 Filter,承担着非常重要的作用。如获取当前 request 对应的权限配置调用访问控制器进行鉴权操作等,都是核心功能。

先简单看一下 FilterSecurityInterceptor 类的主要功用。

 

史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解
获取当前 request 对应的权限配置,首先是调用基类的 beforeInvocation 方法 

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    if ((fi.getRequest() != null)
        ......
    }
    else {
        ......
        InterceptorStatusToken token = super.beforeInvocation(fi);
        ......
    }
}

 

再来看一下基类的 beforeInvocation 方法,从配置好的 SecurityMetadataSource 中获取当前 request 所对应的 ConfigAttribute即权限信息

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......
    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'");
        }
        ......
    }
}

 

这里需要注意一下 rejectPublicInvocations 属性,默认为 false。此属性含义为拒绝公共请求。如果从配置好的 SecurityMetadataSource 中获取不到当前 request 所对应的 ConfigAttribute 时,即认为当前请求为公共请求。如配置 rejectPublicInvocations 属性为 true,则系统会抛出 IllegalArgumentException 异常,即当前请求需要配置权限信息。

接下来,就要判断是否需要进行身份认证了,即调用 authenticateIfRequired 方法。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......
​
    Authentication authenticated = authenticateIfRequired();
​
    ......
}

 

而判断及身份认证逻辑也并不复杂,首先会判断当前用户是否已通过身份认证,如果已通过身份认证,则直接返回;如果尚未通过身份认证,则调用身份认证管理器 AuthenticationManager 进行认证,就如同登录时一样。认证通过后,同样会在当前的安全上下文中存储一份认证后的 authentication

private Authentication authenticateIfRequired() {
    Authentication authentication = SecurityContextHolder.getContext()
        .getAuthentication();
    if (authentication.isAuthenticated() && !alwaysReauthenticate) {
        if (logger.isDebugEnabled()) {
            logger.debug("Previously Authenticated: " + authentication);
        }
        return authentication;
    }
    authentication = authenticationManager.authenticate(authentication);
    // We don't authenticated.setAuthentication(true), because each provider should do
    // that
    if (logger.isDebugEnabled()) {
        logger.debug("Successfully Authenticated: " + authentication);
    }
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return authentication;
}

 

然后,使用获取到的 ConfigAttribute ,继续调用访问控制器 AccessDecisionManager 对当前请求进行鉴权。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......
    // 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));
    }
}

 

注意,无论鉴权通过或是不通后,Spring Security 框架均使用了观察者模式,来通知其它Bean,当前请求的鉴权结果。

如果鉴权不通过,则会抛出 AccessDeniedException 异常,即访问受限,然后会被 ExceptionTranslationFilter 捕获,最终解析后调转到对应的鉴权失败页面。

如果鉴权通过,AbstractSecurityInterceptor 通常会继续请求。但是,在极少数情况下,用户可能希望使用不同的 Authentication 来替换 SecurityContext 中的 Authentication。该身份认证就会由 RunAsManager 来处理。这在某些业务场景下可能很有用,录入服务层方法需要调用远程系统并呈现不同的身份。因为 Spring Security 会自动将安全标识从一个服务器传播到另一个服务器(假设使用的是正确配置的 RMI 或 HttpInvoker 远程协议客户端),这就可能很有用。

在 AccessDecisionManager 鉴权成功后,将通过 RunAsManager 在现有 Authentication 基础上构建一个新的Authentication,如果新的 Authentication 不为空则将产生一个新的 SecurityContext,并把新产生的Authentication 存放在其中。这样在请求受保护资源时从 SecurityContext中 获取到的 Authentication 就是新产生的 Authentication。

protected InterceptorStatusToken beforeInvocation(Object object) {
    ......
​
        // 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);
    }
}

 

注意,AbstractSecurityInterceptor 默认持有的是 RunAsManager 的空实现 NullRunAsManager。

public abstract class AbstractSecurityInterceptor implements InitializingBean,
    ApplicationEventPublisherAware, MessageSourceAware {
  ......
  private RunAsManager runAsManager = new NullRunAsManager();
    ......
 }

 

待请求完成后会在 finallyInvocation() 中将原来的 SecurityContext 重新设置给SecurityContextHolder。

protected void finallyInvocation(InterceptorStatusToken token) {
    if (token != null && token.isContextHolderRefreshRequired()) {
        if (logger.isDebugEnabled()) {
            logger.debug("Reverting to original Authentication: "
                         + token.getSecurityContext().getAuthentication());
        }
​
        SecurityContextHolder.setContext(token.getSecurityContext());
    }
}

 

然而,无论正常调用,亦或是请求异常等,都会触发 finallyInvocation()。

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    if ((fi.getRequest() != null)
       ......
    }
    else {
        ......
​
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        finally {
            // 无论是否成功、抛异常与否,均会执行
            super.finallyInvocation(token);
        }
​
        // 正常请求结束,最后也会执行(afterInvocation 内部会调用finallyInvocation )
        super.afterInvocation(token, null);
    }
}

 

即便是正常执行结束,依然会执行 finallyInvocation()(afterInvocation 内部会调用finallyInvocation )。

protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
    ......
​
    finallyInvocation(token); // continue to clean in this method for passivity
​
    ......
}

 

此外,Spring Security 对 RunAsManager 有一个还有一个非空实现类 RunAsManagerImpl,其构造新 Authentication 的逻辑如下:

如果受保护对象对应的 ConfigAttribute 中拥有以“RUN_AS_”开头的配置属性,则在该属性前加上“ROLE_”,然后再把它作为一个 SimpleGrantedAuthority 赋给将要创建的 Authentication(如ConfigAttribute 中拥有一个“RUN_AS_ADMIN”的属性,则将构建一个“ROLE_RUN_AS_ADMIN”的SimpleGrantedAuthority),最后再利用原 Authentication 的 principal、权限等信息构建一个新的 Authentication 并返回;如果不存在任何以“RUN_AS_”开头的 ConfigAttribute,则直接返回null。

public Authentication buildRunAs(Authentication authentication, Object object,
      Collection<ConfigAttribute> attributes) {
    List<GrantedAuthority> newAuthorities = new ArrayList<>();
​
    for (ConfigAttribute attribute : attributes) {
        if (this.supports(attribute)) {
            GrantedAuthority extraAuthority = new SimpleGrantedAuthority(
                getRolePrefix() + attribute.getAttribute());
            newAuthorities.add(extraAuthority);
        }
    }
​
    if (newAuthorities.size() == 0) {
        return null;
    }
​
    // Add existing authorities
    newAuthorities.addAll(authentication.getAuthorities());
​
    return new RunAsUserToken(this.key, authentication.getPrincipal(),
                              authentication.getCredentials(), newAuthorities,
                              authentication.getClass());
}

 

AccessDecisionManager 是在访问受保护的对象之前判断用户是否拥有该对象的访问权限。然而,有时候我们可能会希望在请求执行完成后对返回值做一些修改或者权限校验,当然,也可以简单的通过AOP来实现这一功能。

同样的,Spring Security 提供了 AfterInvocationManager 接口,它允许我们在受保护对象访问完成后对返回值进行修改或者进行权限校验,权限校验不通过时抛出 AccessDeniedException,并使用观察者模式通知其它Bean。

protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
    ......
​
    if (afterInvocationManager != null) {
        ......
        catch (AccessDeniedException accessDeniedException) {
            AuthorizationFailureEvent event = new AuthorizationFailureEvent(
                token.getSecureObject(), token.getAttributes(), token
                .getSecurityContext().getAuthentication(),
                accessDeniedException);
            publishEvent(event);
​
            throw accessDeniedException;
        }
    }
  ......  
}

 

其将由 AbstractSecurityInterceptor 的子类进行调用,如默认子类 FilterSecurityInterceptor 。

需要特别注意的是,AfterInvocationManager 需要在受保护对象成功被访问后才能执行。

Spring Security 官方文档提供的 AfterInvocationManager 构造图如下:

史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解

类似于AuthenticationManagerAfterInvocationManager 同样也有一个默认的实现类AfterInvocationProviderManager,其中有一个由 AfterInvocationProvider 组成的集合属性。

public class AfterInvocationProviderManager implements AfterInvocationManager,
    InitializingBean {
  ......
​
  private List<AfterInvocationProvider> providers;
​
    ......
}

 

非常有趣的是,AfterInvocationProvider 与 AfterInvocationManager 具有相同的方法定义。此一来,在调用AfterInvocationProviderManager 中的方法时,实际上就是依次调用其中成员属性 providers 中的AfterInvocationProvider  接口对应的方法。

public Object decide(Authentication authentication, Object object,
                     Collection<ConfigAttribute> config, Object returnedObject)
    throws AccessDeniedException {
    Object result = returnedObject;
    for (AfterInvocationProvider provider : providers) {
        result = provider.decide(authentication, object, config, result);
    }
    return result;
}

 

而 AfterInvocationProvider 的默认实现类 PostInvocationAdviceProvider 中的 PostInvocationAuthorizationAdvice,其默认实现类 ExpressionBasedPostInvocationAdvice,不正是对应着后置权限注解 @PostAuthorize 吗?

最后,关于 FILTER_APPLIED 常量,在 FilterSecurityInterceptor 中是这么使用的:

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 && observeOncePerRequest) {
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }
​
        ......
    }
}

 

其主要作用,是用于阻止请求的重复安全检查。就是注释中的这么两段话:

filter already applied to this request and user wants us to observe once-per-request handling, so don't re-do security checking

原理也简单,第一次执行时,检查 request 中 FILTER_APPLIED 属性值为空,则放入值;后续该 request 再次请求时,FILTER_APPLIED 属性值不为空,代表已经进行过安全检查,则该请求直接通过,不再重复进行安全检查

真想问一句,Spring Security 的设计和开发者,吃什么长大的?

到此,FilterSecurityInterceptor 的内容就基本讲解完毕,不得不说,一个类就这么多内容,整个框架更是不得了。佩服,佩服!

 

源码

 

github

 

https://github.com/liuminglei/SpringSecurityLearning/tree/master/16

 

gitee

 

https://gitee.com/xbd521/SpringSecurityLearning/tree/master/16

 

 

 

 

史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解

回复以下关键字,获取更多资源

 

SpringCloud进阶之路 | Java 基础 | 微服务 | JAVA WEB | JAVA 进阶 | JAVA 面试 | MK 精讲

史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解

 

 

 

笔者开通了个人微信公众号【银河架构师】,分享工作、生活过程中的心得体会,填坑指南,技术感悟等内容,会比博客提前更新,欢迎订阅。

技术资料领取方法:关注公众号,回复微服务,领取微服务相关电子书;回复MK精讲,领取MK精讲系列电子书;回复JAVA 进阶,领取JAVA进阶知识相关电子书;回复JAVA面试,领取JAVA面试相关电子书,回复JAVA WEB领取JAVA WEB相关电子书。

史上最简单的Spring Security教程(十六):FilterSecurityInterceptor详解