文档章节

Spring Security 实战干货:动态权限控制(下)实现  

码农小胖哥
 码农小胖哥
发布于 11/29 18:06
字数 2474
阅读 599
收藏 16

1. 前言

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

2.FilterSecurityInterceptor

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

 	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. 元数据加载器

元数据加载器 FilterInvocationSecurityMetadataSourceFilterSecurityInterceptor 的属性,UML 图如下:

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

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

3.1 自定义实现思路

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

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 自定义决策管理器

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

5. 决策投票器

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

5.1 角色投票器

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

5.2 角色分层投票器

通常要求应用程序中的特定角色应自动“包含”其他角色。例如,在具有 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. 配置

配置需要两个方面。一方面是 FilterSecurityInterceptor 的一些必须的初始化组件配置,另一方面是对 HttpSecurityFilterSecurityInterceptor 的注入配置

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

 }

6.2 配置 FilterSecurityInterceptor

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

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

© 著作权归作者所有

码农小胖哥

码农小胖哥

粉丝 87
博文 113
码字总数 134097
作品 1
郑州
程序员
私信 提问
基于 JWT 的权限控制框架 - Light Security

Light Security是一个基于 的权限控制框架,支持与 配合使用。 特点 优点 上手快速 开箱即用 轻量级,代码精简,不到500行代码; 功能实用,市面上安全框架常用能力与套路均已具备: 支持 权...

周立_ITMuch
04/22
4.9K
4
Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密...

小致Daddy
2018/08/03
25.2K
1
Spring Security 实战干货:必须掌握的一些内置 Filter

前言 上一文我们使用 Spring Security 实现了各种登录聚合的场面。其中我们是通过在 之前一个自定义的过滤器实现的。我怎么知道自定义过滤器要加在 之前。我在这个系列开篇说了 Spring Secur...

码农小胖哥
10/22
146
0
Spring Security 实战干货:玩转自定义登录

前言 前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(),认证的第一步就是登录。...

码农小胖哥
10/18
120
0
Spring Security 实战干货: 登录后返回 JWT Token

前言 欢迎阅读 Spring Security 实战干货 系列文章,上一文 我们实现了 JWT 工具。本篇我们将一起探讨如何将 JWT 与 Spring Security 结合起来,在认证成功后不再跳转到指定页面而是直接返回...

码农小胖哥
10/28
1K
3

没有更多内容

加载失败,请刷新页面

加载更多

Spring Cloud简介,微服务架构,以及与Dubbo的详细比较

什么是Spring Cloud Spring Cloud 是一套完整的微服务解决方案,基于 Spring Boot 框架,准确的说,它不是一个框架,而是一个大的容器,它将市面上较好的微服务框架集成进来,从而简化了开发...

一只会编程的狼
2分钟前
1
0
检查Ruby中的数组中是否存在值

我有一个值'Dog'和一个阵列['Cat', 'Dog', 'Bird'] 。 如何在没有循环的情况下检查数组中是否存在? 是否有一种简单的方法来检查值是否存在,仅此而已? #1楼 有一个in? 方法中ActiveSupport......

技术盛宴
3分钟前
2
0
技术分享 | 使用 Perf 和火焰图分析软件

作者:Agustín 翻译:孟维克 原文:https://www.percona.com/blog/2019/11/20/profiling-software-using-perf-and-flame-graphs/ 在这篇博文中,我们将探讨如何一起使用perf和火焰图。它们用...

爱可生
5分钟前
2
0
什么是区块链和节点?

首先你要知道,节点(node)只是一个词,在网络相关的文献里很常见,在不同的语境,不同的系统里会有不同的意思,并不是所有的区块链乃至p2p网络里提到节点都指的一个东西。 其次,区块链这东...

专注的阿熊
8分钟前
2
0
解决传导干扰八大绝招

引言 电磁干扰EMI中电子设备产生的干扰信号是通过导线或公共电源线进行传输,互相产生干扰称为传导干扰。传导干扰给不少电子工程师带来困惑,如何解决传导干扰?找对方法,你会发现,传导干扰...

demyar
8分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部