Spring Security 实战干货:动态权限控制(下)实现
1. 前言
Spring Security 实战干货:内置 Filter 全解析 中提到的第 32 个 Filter
不知道你是否有印象。它决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限? 它就是 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. 元数据加载器
元数据加载器 FilterInvocationSecurityMetadataSource
是 FilterSecurityInterceptor
的属性,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_ADMIN
和 ROLE_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 Security 的 Java 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.0、 SSO 、OpenID 展开。敬请关注 felord.cn
老规矩, 关注 Felordcn 回复 day10 获取 DEMO 。
关注公众号:Felordcn获取更多资讯
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
vscode的C++开发环境配置-win10下Linux子系统
前言 最近一直在纠结,每次要开发都要打开虚拟器,启动Linux,然后启动IDE。一圈下来光启动都要好几分钟,而且虚拟机占用内存和磁盘相对较大。想找找其他的方法绕开这个烦人的前戏。然后,打开了许久不用的Windows下的vscode的,看看有啥好玩的插件,突然看到巨硬大佬发布的插件Remote-WSL,就是下面这玩意儿: 这个插件是干什么的呢,简单说就是让vscode可以连接到win10的Linux子系统上去编辑里面的代码等的一个工具。 如果不知道什么是Linux子系统,自行度娘一下。 有了这个插件,那么是不是可以摸索使用Linux子系统去开发了呢,我抱着这样的心态开始了折腾之路。。。 安装Linux 要使用Linux子系统,必须要安装Win10下的Linux-app,现在微软商店上有很多,最常见是Ubuntu,我由于gcc编译器的版本需求是4.x的,所以我选择安装了Ubuntu16.04。下载后,启动app即可,其实这个app就是个终端程序。 安装完成以后就可以启动了;如果启动后提示如下情况: 说明没有启动Linux子系统,在启用或关闭Windows功能中,将适用于Linux的Wind...
- 下一篇
Spring Boot 构建多租户SaaS平台核心技术指南
本次教程所涉及到的源码已上传至Github,如果你不需要继续阅读下面的内容,你可以直接点击此链接获取源码内容。https://github.com/ramostear/una-saas-toturial 1. 概述 笔者从2014年开始接触SaaS(Software as a Service),即多租户(或多承租)软件应用平台;并一直从事相关领域的架构设计及研发工作。机缘巧合,在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究,从中收获颇多。最早接触SaaS时,国内相关资源匮乏,唯一有的参照资料是《互联网时代的软件革命:SaaS架构设计》(叶伟等著)一书。最后课题的实现是基于OSGI(Open Service Gateway Initiative)Java动态模块化系统规范来实现的。 时至今日,五年的时间过去了,软件开发的技术发生了巨大的改变,笔者所实现SaaS平台的技术栈也更新了好几波,真是印证了那就话:“山重水尽疑无路,柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑,以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统,决定写一篇文章聊一聊...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题