Spring Security - 动态权限控制(下)实现

1. Spring Security 提供的轮子

Spring Security 实战干货:内置 Filter 全解析 中提到的第 32Filter 不知道你是否有印象。它决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限? 它就是 FilterSecurityInterceptor ,正是我们需要的那个轮子。

2. FilterSecurityInterceptor

过滤器排行榜第 32 位!肩负对接口权限认证的重要职责。我们来看它的过滤逻辑:

     public void doFilter(ServletRequest request, ServletResponse response,
             FilterChain chain) throws IOException, ServletException {
         FilterInvocation fi = new FilterInvocation(request, response, chain);
         invoke(fi);
     }

初始化了一个 FilterInvocation 然后被 invoke 方法处理:

    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);
            }

            InterceptorStatusToken token = super.beforeInvocation(fi);

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

            super.afterInvocation(token, null);
        }
    }

每一次请求被 Filter 过滤都会被打上标记 FILTER_APPLIED ,没有被打上标记的 走了父类的 beforeInvocation 方法然后再进入过滤器链,看上去是走了一个前置的处理。那么前置处理了什么呢?
首先会通过 this.obtainSecurityMetadataSource().getAttributes(Object object) 拿受保护对象(就是当前请求的URI)所有的映射角色( ConfigAttribute 直接理解为角色的进一步抽象) 。然后使用访问决策管理器 AccessDecisionManager 进行投票决策来确定是否放行。 我们来看一下这两个接口。

安全拦截器和“安全对象”模型参考:

3. FilterInvocationSecurityMetadataSource

这个接口是 FilterSecurityInterceptor 的属性,UML图如下:

FilterInvocationSecurityMetadataSource 是一个标记接口,其抽象方法继承自 SecurityMetadataSource``AopInfrastructureBean 。它的作用是来获取我们上一篇文章所描述的 资源角色元数据

  • Collection getAttributes(Object object) 根据提供的受保护对象的信息,其实就是URI,获取该URI 配置的所有角色
  • Collection getAllConfigAttributes() 这个就是获取全部角色
  • boolean supports(Class<?> clazz) 对特定的安全对象是否提供 ConfigAttribute 支持

3.1 自定义实现 FilterInvocationSecurityMetadataSource 的思路分析

所有的思路仅供参考,实际以你的业务为准!

Collection<ConfigAttribute> getAttributes(Object object) 方法的实现:肯定是获取请求中的 URI 来和 所有的 资源配置中的 Ant Pattern 进行匹配以获取对应的资源配置, 这里需要将资源查询接口查询的资源配置封装为 AntPathRequestMatcher 以方便进行 Ant Match
这里需要特别提一下如果你使用 Restful 风格,这里 增删改查 将非常方便你来对资源的管控。参考的实现:

 @Bean
 public RequestMatcherCreator requestMatcherCreator() {
   return metaResources -> metaResources.stream()
           .map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
           .collect(Collectors.toSet());
 } 

HttpRequest 匹配到对应的资源配置后就能根据资源配置去取对应的角色集合。这些角色将交给访问决策管理器 AccessDecisionManager 进行投票表决以决定是否放行。

4. AccessDecisionManager

决策管理器,用来投票决定是否放行请求。

  public interface AccessDecisionManager {
    // 决策 主要通过其持有的 AccessDecisionVoter 来进行投票决策
       void decide(Authentication authentication, Object object,
               Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
               InsufficientAuthenticationException;
   // 以确定AccessDecisionManager是否可以处理传递的ConfigAttribute
       boolean supports(ConfigAttribute attribute);
   //以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。
       boolean supports(Class<?> clazz);
   }

AccessDecisionManager 有三个默认实现:

  • AffirmativeBased 基于肯定的决策器。 用户持有一个同意访问的角色就能通过。
  • ConsensusBased 基于共识的决策器。 用户持有同意的角色数量多于禁止的角色数。
  • UnanimousBased 基于一致的决策器。 用户持有的所有角色都同意访问才能放行。

投票决策模型参考:

4.1 自定义 AccessDecisionManager

动态控制权限就需要我们实现自己的访问决策器。我们上面说了默认有三个实现,这里我选择基于肯定的决策器 AffirmativeBased ,只要用户持有一个持有一个角色包含想要访问的资源就能访问该资源。接下来就是投票器 AccessDecisionVoter 的定义了,其实我们可以选择内置的

5. AccessDecisionVoter

AccessDecisionVoter 将安全配置属性 ConfigAttribute 以特定的逻辑进行解析并基于特定的策略来进行投票,投赞成票时总票数 +1 ,反对票总票数 -1 ,弃权时总票数 +0 , 然后由 AccessDecisionManager 根据具体的计票策略来决定是否放行。

5.1 角色投票器 RoleVoter

Spring Security 提供的最常用的投票器是角色投票器 RoleVoter ,它将安全配置属性 ConfigAttribute 视为简单的角色名称,并在用户被分配了该角色时授予访问权限。
如果任何 ConfigAttribute 以前缀 ROLE_ 开头,它将投票。如果有一个 GrantedAuthority 返回一个字符串(通过 getAuthority() 方法)正好等于一个或多个从前缀 ROLE_ 开始的 ConfigAttributes ,它将投票授予访问权限。如果没有任何以 ROLE_ 开头的 ConfigAttributes 匹配,则 RoleVoter 将投票拒绝访问。如果没有 ConfigAttribute 以ROLE_为前缀,将弃权。
这正是我们想要的投票器。

5.2 角色分层投票器 RoleHierarchyVoter

通常要求应用程序中的特定角色应自动“包含”其他角色。例如,在具有 ROLE_ADMINROLE_USER 角色概念的应用中,您可能希望管理员能够执行普通用户可以执行的所有操作。你不得不进行各种复杂的逻辑嵌套来满足这一需求。现在幸好有了 RoleHierarchyVoter 可以帮你减少这种负担。
它由上面的 RoleVoter 派生,通过配置了一个 RoleHierarchy 就可以实现 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST 这种层次包含结构, 左边的一定能访问右边可以访问的资源 。具体的配置规则为:角色从左到右、从高到低以 > 相连(注意两个空格),以换行符 \n 为分割线。举个例子

   ROLE_ADMIN > ROLE_STAFF
   ROLE_STAFF > ROLE_USER
   ROLE_USER > ROLE_GUEST

请注意动态配置中你需要自行实现角色分层的逻辑。 DEMO 中并未对该风格进行实现。

6. 配置

配置需要两个方面。

6.1 自定义组件的配置

我们需要将元数据加载器 和 访问决策器注入 Spring IoC

 /**
  * 动态权限组件配置
  *
  * @author Felordcn
  */
 @Configuration
 public class DynamicAccessControlConfiguration {
     /**
      *  RequestMatcher 生成器
      * @return RequestMatcher
      */
     @Bean
     public RequestMatcherCreator requestMatcherCreator() {
         return metaResources -> metaResources.stream()
                 .map(metaResource -> new AntPathRequestMatcher(metaResource.getPattern(), metaResource.getMethod()))
                 .collect(Collectors.toSet());
     }

     /**
      * 元数据加载器
      * 
      * @return dynamicFilterInvocationSecurityMetadataSource
      */
     @Bean
     public FilterInvocationSecurityMetadataSource dynamicFilterInvocationSecurityMetadataSource() {
         return new DynamicFilterInvocationSecurityMetadataSource();
     }

     /**
      *  角色投票器
      * @return roleVoter
      */
     @Bean
     public RoleVoter roleVoter() {
         return new RoleVoter();
     }

     /**
      *  基于肯定的访问决策器
      *  
      * @param decisionVoters  AccessDecisionVoter类型的 Bean 会自动注入到 decisionVoters
      * @return affirmativeBased
      */
     @Bean
     public AccessDecisionManager affirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
         return new AffirmativeBased(decisionVoters);
     }

 }

Spring SecurityJava Configuration 不会公开它配置的每个 object 的每个 property 。这简化了大多数用户的配置。
虽然有充分的理由不直接公开每个 property ,但用户可能仍需要像本文一样的取实现个性化需求。为了解决这个问题, Spring Security 引入了 ObjectPostProcessor 的概念,它可用于修改或替换 Java Configuration 创建的许多 Object 实例。 FilterSecurityInterceptor 的替换配置正是通过这种方式来进行:

@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
    private static final String LOGIN_PROCESSING_URL = "/process";

    /**
     * Json login post processor json login post processor.
     *
     * @return the json login post processor
     */
    @Bean
    public JsonLoginPostProcessor jsonLoginPostProcessor() {
        return new JsonLoginPostProcessor();
    }

    /**
     * Pre login filter pre login filter.
     *
     * @param loginPostProcessors the login post processors
     * @return the pre login filter
     */
    @Bean
    public PreLoginFilter preLoginFilter(Collection<LoginPostProcessor> loginPostProcessors) {
        return new PreLoginFilter(LOGIN_PROCESSING_URL, loginPostProcessors);
    }

    /**
     * Jwt 认证过滤器.
     *
     * @param jwtTokenGenerator jwt 工具类 负责 生成 验证 解析
     * @param jwtTokenStorage   jwt 缓存存储接口
     * @return the jwt authentication filter
     */
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) {
        return new JwtAuthenticationFilter(jwtTokenGenerator, jwtTokenStorage);
    }

    /**
     * The type Default configurer adapter.
     */
    @Configuration
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Autowired
        private JwtAuthenticationFilter jwtAuthenticationFilter;
        @Autowired
        private PreLoginFilter preLoginFilter;
        @Autowired
        private AuthenticationSuccessHandler authenticationSuccessHandler;
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
        @Autowired
        private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
        @Autowired
        private AccessDecisionManager accessDecisionManager;

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
        }

        @Override
        public void configure(WebSecurity web) {
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .cors()
                    .and()
                    // session 生成策略用无状态策略
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                    .and()
                    //   动态权限配置
                    .authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(filterSecurityInterceptorObjectPostProcessor())
                    .and()
                    .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
                    // jwt 必须配置于 UsernamePasswordAuthenticationFilter 之前
                    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                    // 登录  成功后返回jwt token  失败后返回 错误信息
                    .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
                    .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());

        }

        /**
         * 自定义 FilterSecurityInterceptor  ObjectPostProcessor 以替换默认配置达到动态权限的目的
         *
         * @return ObjectPostProcessor
         */
        private ObjectPostProcessor<FilterSecurityInterceptor> filterSecurityInterceptorObjectPostProcessor() {
            return new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                    object.setAccessDecisionManager(accessDecisionManager);
                    object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
                    return object;
                }
            };
        }

    }
} 

然后你编写一个 Controller 方法就将其在数据库注册为一个资源进行动态的访问控制了。 无须注解或者更详细的 Java Config 配置

7. 总结

从最开始到现在一共10个 DEMO 。我们循序渐进地从如何学习 Spring Security 到目前实现了基于 RBAC 、动态的权限资源访问控制。如果你能坚持到现在那么已经能满足了一些基本开发定制的需要。当然 Spring Security 还有很多局部的一些概念,我也会在以后抽时间进行讲解。

8. roadmap

我先喘口气休几天。后续的一些 Spring Security 教程将围绕目前更加流行的 OAuth2.0SSOOpenID 展开。敬请关注 felord.cn

老规矩, 关注 Felordcn 回复 day10 获取 DEMO


原文:https://felord.cn/spring-security-dynamic-rbac-b.html