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

1. 前言

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

2.FilterSecurityInterceptor

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

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

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

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 进行投票决策来肯定是否放行。 咱们来看一下这两个接口。spring

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

3. 元数据加载器

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

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

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

3.1 自定义实现思路

全部的思路仅供参考,实际以你的业务为准!安全

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

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

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

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. 配置

配置须要两个方面。

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

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

我的博客:https://felord.cn

相关文章
相关标签/搜索