1. 前言 中提到的第 32 个 Filter
不知道你是否有印象。它决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限? 它就是 FilterSecurityInterceptor
,正是我们需要的那个轮子。
2. FilterSecurityInterceptor 过滤器排行榜第 32 位!肩负对接口权限认证的重要职责。我们来看它的过滤逻辑:
1 2 3 4 5 public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation (request, response, chain); invoke(fi); }
初始化了一个 FilterInvocation
然后被 invoke
方法处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public void invoke (FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null ) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null ) && observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { 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
进行投票决策来确定是否放行。 我们来看一下这两个接口。
安全拦截器和“安全对象”模型参考:
这个接口是 FilterSecurityInterceptor
的属性,UML图如下:
FilterInvocationSecurityMetadataSource
是一个标记接口,其抽象方法继承自 SecurityMetadataSource
和AopInfrastructureBean
。它的作用是来获取我们上一篇文章所描述的资源角色元数据。
Collection getAttributes(Object object) 根据提供的受保护对象的信息,其实就是URI,获取该URI 配置的所有角色
Collection getAllConfigAttributes() 这个就是获取全部角色
boolean supports(Class<?> clazz) 对特定的安全对象是否提供 ConfigAttribute
支持
所有的思路仅供参考,实际以你的业务为准!
Collection<ConfigAttribute> getAttributes(Object object)
方法的实现:肯定是获取请求中的 URI
来和 所有的 资源配置中的 Ant Pattern
进行匹配以获取对应的资源配置, 这里需要将资源查询接口查询的资源配置封装为 AntPathRequestMatcher
以方便进行 Ant Match
。
这里需要特别提一下如果你使用 Restful 风格,这里 增删改查 将非常方便你来对资源的管控。参考的实现:
1 2 3 4 5 6 @Bean public RequestMatcherCreator requestMatcherCreator () {return metaResources -> metaResources.stream() .map(metaResource -> new AntPathRequestMatcher (metaResource.getPattern(), metaResource.getMethod())) .collect(Collectors.toSet()); }
HttpRequest
匹配到对应的资源配置后就能根据资源配置去取对应的角色集合。这些角色将交给访问决策管理器 AccessDecisionManager
进行投票表决以决定是否放行。
4. AccessDecisionManager 决策管理器,用来投票决定是否放行请求。
1 2 3 4 5 6 7 8 9 10 public interface AccessDecisionManager { void decide (Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; boolean supports (ConfigAttribute attribute) ; 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_ADMIN
和 ROLE_USER
角色概念的应用中,您可能希望管理员能够执行普通用户可以执行的所有操作。你不得不进行各种复杂的逻辑嵌套来满足这一需求。现在幸好有了 RoleHierarchyVoter
可以帮你减少这种负担。
它由上面的 RoleVoter
派生,通过配置了一个 RoleHierarchy
就可以实现 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST
这种层次包含结构,左边的一定能访问右边可以访问的资源 。具体的配置规则为:角色从左到右、从高到低以 >
相连(注意两个空格),以换行符 \n
为分割线。举个例子
1 2 3 ROLE_ADMIN > ROLE_STAFF ROLE_STAFF > ROLE_USER ROLE_USER > ROLE_GUEST
请注意动态配置中你需要自行实现角色分层的逻辑。DEMO 中并未对该风格进行实现。
6. 配置 配置需要两个方面
6.1 自定义组件的配置 我们需要将元数据加载器 和 访问决策器注入 Spring IoC :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Configuration public class DynamicAccessControlConfiguration { @Bean public RequestMatcherCreator requestMatcherCreator () { return metaResources -> metaResources.stream() .map(metaResource -> new AntPathRequestMatcher (metaResource.getPattern(), metaResource.getMethod())) .collect(Collectors.toSet()); } @Bean public FilterInvocationSecurityMetadataSource dynamicFilterInvocationSecurityMetadataSource () { return new DynamicFilterInvocationSecurityMetadataSource (); } @Bean public RoleVoter roleVoter () { return new RoleVoter (); } @Bean public AccessDecisionManager affirmativeBased (List<AccessDecisionVoter<?>> decisionVoters) { return new AffirmativeBased (decisionVoters); } }
Spring Security 的 Java Configuration 不会公开它配置的每个 object 的每个 property 。这简化了大多数用户的配置。
虽然有充分的理由不直接公开每个 property ,但用户可能仍需要像本文一样的取实现个性化需求。为了解决这个问题,Spring Security 引入了 ObjectPostProcessor
的概念,它可用于修改或替换 Java Configuration 创建的许多 Object
实例。 FilterSecurityInterceptor
的替换配置正是通过这种方式来进行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 @Configuration @ConditionalOnClass(WebSecurityConfigurerAdapter.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class CustomSpringBootWebSecurityConfiguration { private static final String LOGIN_PROCESSING_URL = "/process" ; @Bean public JsonLoginPostProcessor jsonLoginPostProcessor () { return new JsonLoginPostProcessor (); } @Bean public PreLoginFilter preLoginFilter (Collection<LoginPostProcessor> loginPostProcessors) { return new PreLoginFilter (LOGIN_PROCESSING_URL, loginPostProcessors); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter (JwtTokenGenerator jwtTokenGenerator, JwtTokenStorage jwtTokenStorage) { return new JwtAuthenticationFilter (jwtTokenGenerator, jwtTokenStorage); } @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() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler ()).authenticationEntryPoint(new SimpleAuthenticationEntryPoint ()) .and() .authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(filterSecurityInterceptorObjectPostProcessor()) .and() .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler) .and().logout().addLogoutHandler(new CustomLogoutHandler ()).logoutSuccessHandler(new CustomLogoutSuccessHandler ()); } 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 还有很多局部的一些概念,我也会在以后抽时间进行讲解。
本节代码在day10
分支
转载自@felord.cn