如果对该文章感兴趣欢迎关注我的小博客 Forest,我会分享更多学习时遇到的知识 (●’◡’●)

参考资料

SpringSecurity原理剖析与权限系统设计
SpringSecurity动态鉴权流程解析 | 掘金新人第二弹
官方文档 Part II. Servlet Applications

上篇笔记详细的介绍了 SpringSecurity 的认证过程,现在这部分来补充它的动态鉴权部分

鉴权原理

经常能看到下面这张图

整个认证的过程其实一直在围绕图中过滤链的绿色部分,而动态鉴权主要是围绕其橙色部分,也就是图上标的:FilterSecurityInterceptor。

实际上通过 Spring Security 内置的一个 Security Interceptor 委托给各个具体的 AccessDecisionManager

FilterSecurityInterceptor

上图已经说明了 Spring Security 里面有个 Security Interceptor 会委托给各个具体的 AccessDecisionManager 来做具体的工作,这个过滤器就是 FilterSecurityInterceptor

一个请求完成了认证,且没有抛出异常之后就会到达 FilterSecurityInterceptor 所负责的鉴权部分,也就是说鉴权的入口就在 FilterSecurityInterceptor。

类图如下所示:

先来看看 FilterSecurityInterceptor 的定义和主要方法:

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {// 这个 FilterInvocation 可以当作它封装了 request,它的主要工作就是拿请求里面的信息,比如请求的 URI。FilterInvocation fi = new FilterInvocation(request, response, chain);invoke(fi);}
}

关键就在于这个 invoke 方法

public void invoke(FilterInvocation fi) throws IOException, ServletException {// 健壮性判断略...// 进入鉴权  ⭐InterceptorStatusToken token = super.beforeInvocation(fi); try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} finally {super.finallyInvocation(token);}super.afterInvocation(token, null);
}

可以发现它分别调用了父类(AbstractSecurityInterceptor)的三个 Invocation 方法

  • beforeInvocation
  • finallyInvocation
  • afterInvocation

这里就只讲 beforeInvocation,其它的几个都大同小异,beforeInvocation 顾名思义,它是第一个被执行的,学习访问控制时,可以看到有下面两个注解

  • @PreAuthorize 在方法执行前再进行权限验证
  • @PostAuthorize 在方法执行后再进行权限验证

实际上就是对应着上面的各个 Invocation 执行的时期

那来看下 Invocation 里面具体做了什么

// 注意,新版这里有点变化调用 accessDecisionManager 使用的是 attemptAuthorization 方法
// 不过也是在 beforeInvocation 内部调用的,所以实际影响不大
protected InterceptorStatusToken beforeInvocation(Object object) {// 健壮性判断略...// 这个对象是一个 List,里面就是在配置文件中配置的过滤规则Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);Authentication authenticated = authenticateIfRequired();try {// 鉴权需要调用的接口this.accessDecisionManager.decide(authenticated, object, attributes);} catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));throw accessDeniedException;}}

源码较长,精简了部分代码,这段代码大致可以分为三步:

1 拿到了一个 Collection<ConfigAttribute> 对象,这个对象是一个 List,其实里面就是我们在配置文件中配置的过滤规则。

2 拿到了 Authentication,这里是调用 authenticateIfRequired 方法拿到了,其实方法里面还是通过 调用 SecurityContextHolder 拿到的

3 调用了 accessDecisionManager.decide(authenticated, object, attributes),前两步都是对 decide 方法做参数的准备,第三步才是正式去到鉴权的逻辑,既然这里面才是真正鉴权的逻辑,那也就是说鉴权其实是 accessDecisionManager 在做。

AccessDecisionManager

前面通过源码我们看到了鉴权的真正处理者:AccessDecisionManager

它是如何工作的呢?

AccessDecisionManager 是一个接口,它声明了三个方法,除了第一个鉴权方法以外,还有两个是辅助性的方法,其作用都是甄别 decide方法中参数的有效性。

public interface AccessDecisionManager {// 主要鉴权方法void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,InsufficientAuthenticationException;boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);
}

那既然是一个接口,上文中所调用的肯定是他的实现类了,我们来看看这个接口的结构树:

从图中我们可以看到它主要有三个实现类,分别代表了三种不同的鉴权逻辑:

  • AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
  • UnanimousBased:一票反对,只要有一票反对就不能通过。
  • ConsensusBased:少数票服从多数票。

这里的表述为什么要用票呢?因为在实现类里面采用了委托的形式,将请求委托给投票器(AccessDecisionVoter),每个投票器拿着这个请求根据自身的逻辑来计算出能不能通过然后进行投票,所以会有上面的表述。

也就是说这三个实现类,其实还不是真正判断请求能不能通过的类,真正判断请求是否通过的是投票器,然后实现类把投票器的结果综合起来来决定到底能不能通过。

AffirmativeBased

刚刚已经说过,实现类把投票器的结果综合起来进行决定,也就是说投票器可以放入多个,每个实现类里的投票器数量取决于构造的时候放入了多少投票器,我们可以看看默认的 AffirmativeBased 的源码。

public class AffirmativeBased extends AbstractAccessDecisionManager {public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {super(decisionVoters);}// 拿到所有的投票器,循环遍历进行投票public void decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {int deny = 0;// 遍历投票器for (AccessDecisionVoter voter : getDecisionVoters()) {// 可以注意到实际是委托给 AccessDecisionVoter 的 vote 方法进行认证int result = voter.vote(authentication, object, configAttributes);if (logger.isDebugEnabled()) {logger.debug("Voter: " + voter + ", returned: " + result);}switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();}
}

AffirmativeBased 的构造是传入投票器 List,其主要鉴权逻辑交给投票器(AccessDecisionVoter)去判断,投票器返回不同的数字代表不同的结果,然后 AffirmativeBased 根据自身一票通过的策略决定放行还是抛出异常。

AffirmativeBased 默认传入的构造器只有一个 WebExpressionVoter,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果。

所以 SpringSecurity 默认的鉴权逻辑就是根据配置文件中的配置进行鉴权,这是符合我们现有认知的。

使用此方法,将根据授权决策轮询一系列 AccessDecisionVoter 实现。然后 AccessDecisionManager 根据其对投票的评估来决定是否抛出 AccessDeniedException

AccessDecisionVoter

上面说了访问控制的过程和认证的过程很详细,都是通过委托给第三方去实现的,在认证中,这个 “第三方” 是 AuthenticationProvider 而到了访问控制中 “第三方” 则是这个 AccessDecisionVoter(投票器)

它有如下几个实现类

这里就只介绍一下 RoleVoter 这个投票器,其它的都差不多,顾名思义,它基于任何一个以 “ROLE_” 开头的配置属性进行投票。如果符合条件,则搜索认证对象的 GrantedAuthority 列表。

// 看上一节的源码,认证执行的是这个方法
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {if (authentication == null) {return ACCESS_DENIED;}int result = ACCESS_ABSTAIN;// 这里是比对下当前要访问的用户角色是否满足 API 所支持的 GrantedAuthorityCollection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);for (ConfigAttribute attribute : attributes) {if (this.supports(attribute)) {result = ACCESS_DENIED;// Attempt to find a matching granted authorityfor (GrantedAuthority authority : authorities) {if (attribute.getAttribute().equals(authority.getAuthority())) {// 找到了满足的角色抛出成功return ACCESS_GRANTED;}}}}return result;
}

动态鉴权实现

通过上面一步步的讲述,我想你也应该理解了 SpringSecurity 到底是什么实现鉴权的,那我们想要做到动态的给予某个角色不同的访问权限应该怎么做呢?

动态鉴权几种方案

既然是动态鉴权了,那我们的权限 URI 肯定是放在数据库中了,我们要做的就是实时的在数据库中去读取不同角色对应的权限然后与当前登录的用户做个比较。

那我们要做到这一步可以想些方案,比如:

1 直接重写一个 AccessDecisionManager,将它用作默认的 AccessDecisionManager,并在里面直接写好鉴权逻辑。

2 重写一个投票器,将它放到默认的 AccessDecisionManager 里面,和之前一样用投票器鉴权。

3 直接去做 FilterSecurityInterceptor 的改动。

本文采用第二种方式

重写投票器思路

那么我们需要写一个新的投票器,在这个投票器里面拿到当前用户的角色,使其和当前请求所需要的角色做个对比。

单单是这样还不够,因为我们可能在配置文件中也配置的有一些放行的权限,比如登录 URI 就是放行的,所以我们还需要继续使用我们上文所提到的 WebExpressionVoter,也就是说需要 “自定义权限” + “配置文件” 双行的模式,所以我们的 AccessDecisionManager 里面就会有两个投票器:WebExpressionVoter 和自定义的投票器。

紧接着 还需要考虑去使用什么样的投票策略,这里使用的是 UnanimousBased 一票反对策略,而没有使用默认的一票通过策略,因为在我们的配置中配置了除了登录请求以外的其他请求都是需要认证的,这个逻辑会被 WebExpressionVoter 处理,如果使用了一票通过策略,那我们去访问被保护的 API 的时候,WebExpressionVoter 发现当前请求认证了,就直接投了赞成票,且因为是一票通过策略,这个请求就走不到我们自定义的投票器了。

注:也可以不用配置文件中的配置,将自定义权限配置都放在数据库中,然后统一交给一个投票器来处理,但是这样有点极端,配置文件直接就无效了,一般还是采用双行的模式

重新构造 AccessDecisionManager

首先重新构造 AccessDecisionManager, 因为投票器是系统启动的时候自动添加进去的,所以我们想多加入一个构造器必须自己重新构建 AccessDecisionManager,然后将它放到配置中去。

而且我们的投票策略已经改变了,要由 AffirmativeBased 换成 UnanimousBased,所以这一步是必不可少的。

并且我们还要自定义一个投票器起来,将它注册成Bean,AccessDecisionProcessor 就是我们需要自定义的投票器。

// 注册自定义的投票器(这个投票器的实现看下面)
@Bean
public AccessDecisionVoter<FilterInvocation> accessDecisionProcessor() {return new AccessDecisionProcessor();
}@Bean
public AccessDecisionManager accessDecisionManager() {// 构造一个新的 AccessDecisionManager 放入两个投票器List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(new WebExpressionVoter(), accessDecisionProcessor());return new UnanimousBased(decisionVoters);
}

定义完 AccessDecisionManager 之后,我们将它放入启动配置:

@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 放行所有OPTIONS请求.antMatchers(HttpMethod.OPTIONS).permitAll()// 放行登录方法.antMatchers("/api/auth/login").permitAll()// 其他请求都需要认证后才能访问.anyRequest().authenticated()// 使用自定义的 accessDecisionManager ⭐.accessDecisionManager(accessDecisionManager()).and()// 添加未登录与权限不足异常处理器.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler()).authenticationEntryPoint(restAuthenticationEntryPoint()).and()// 将自定义的JWT过滤器放到过滤链中.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)// 打开Spring Security的跨域.cors().and()// 关闭CSRF.csrf().disable()// 关闭Session机制.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

这样之后,SpringSecurity 里面的 AccessDecisionManager 就会被替换成我们自定义的 AccessDecisionManager了。

自定义鉴权实现

上文配置中放入了两个投票器,其中第二个投票器就是需要创建的投票器,起名为 AccessDecisionProcessor。

投票其也是有一个接口规范的,只需要实现这个 AccessDecisionVoter 接口就行了,然后实现它的方法。

具体的实现返回 int,可能的值反映在 AccessDecisionVoter 静态字段

  • ACCESS_ABSTAIN
  • ACCESS_DENIED
  • ACCESS_GRANTED

如果投票实施对授权决定没有意见,则将返回 ACCESS_ABSTAIN。如果确实有意见,则必须返回 ACCESS_DENIED 或 ACCESS_GRANTED

@Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoter<FilterInvocation> {@Autowiredprivate Cache caffeineCache; // 这个缓存是自定义的,细节看作者的源码// https://github.com/he-erduo/spring-boot-learning-demo/blob/master/spring-security-demo/src/main/java/org/example/security/auth/cache/CaffeineCache.java// 投票@Overridepublic int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {assert authentication != null;assert object != null;// 拿到当前请求uriString requestUrl = object.getRequestUrl();String method = object.getRequest().getMethod();log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);String key = requestUrl + ":" + method;// 如果没有缓存中没有此权限也就是未保护此API,弃权PermissionInfoBO permission = caffeineCache.get(CacheName.PERMISSION, key, PermissionInfoBO.class);if (permission == null) {return ACCESS_ABSTAIN;}// 拿到当前用户所具有的权限List<String> roles = ((UserDetail) authentication.getPrincipal()).getRoles();if (roles.contains(permission.getRoleCode())) {return ACCESS_GRANTED;}else{return ACCESS_DENIED;}}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}
}

大致逻辑是这样:我们以 URI + METHOD 为 key 去缓存中查找权限相关的信息,如果没有找到此 URI,则证明这个 URI 没有被保护,投票器可以直接弃权。

如果找到了这个 URI 相关权限信息,则用其与用户自带的角色信息做一个对比,根据对比结果返回 ACCESS_GRANTED 或 ACCESS_DENIED。

当然这样做有一个前提,那就是在系统启动的时候就把 URI 权限数据都放到缓存中了,系统一般在启动的时候都会把热点数据放入缓存中,以提高系统的访问效率。

@Component
public class InitProcessor {@Autowiredprivate PermissionService permissionService;@Autowiredprivate Cache caffeineCache;@PostConstructpublic void init() {List<PermissionInfoBO> permissionInfoList = permissionService.listPermissionInfoBO();permissionInfoList.forEach(permissionInfo -> {caffeineCache.put(CacheName.PERMISSION, permissionInfo.getPermissionUri() + ":" + permissionInfo.getPermissionMethod(), permissionInfo);});}
}

这里考虑到权限 URI 可能非常多,所以将权限 URI 作为 key 放到缓存中,因为一般缓存中通过 key 读取数据的速度是 O(1)O(1)O(1),所以这样会非常快。

至此就执行完毕了,鉴权的逻辑到底如何处理,其实是开发者自己来定义的,要根据系统需求和数据库表设计进行综合考量,这里只是给出一个思路。

Spring Security 鉴权流程相关推荐

  1. spring security鉴权

    1.SpringSecurity 鉴权 - [重点] RBAC 基于角色访问控制 Role-Based Access Control组成部分:RBAC模型里面,有3个基础组成部分,分别是:用户user ...

  2. Gateway 整合 Spring Security鉴权

    目录 Spring-Security Spring-Webflux 注意 编码 项目环境版本 gradle 依赖 Spring-Security配置 1. Security核心配置 2.用户认证 3. ...

  3. 结合zuul网关的鉴权流程

    结合Zuul的鉴权流程 我们逐步演进系统架构设计.需要注意的是:secret是签名的关键,因此一定要保密,我们放到鉴权中心保存,其它任何服务中都不能获取secret. 没有RSA加密时 在微服务架构中 ...

  4. 01 飞书开发-网页应用JSSDK鉴权流程

    本次飞书网页应用代码示例采用uni-app框架,注重代码的思想,触类旁通 1.1 网页应用 JSSDK鉴权流程 1.1.1 获取tenant_access_token \src\manifest.js ...

  5. 5G UE鉴权流程详解 UE Authentication

    名词 SUCI (Subscription Concealed Identifier)订阅隐藏标识符 SUPI (Subscription Permanent Identifier)订阅永久标识符 S ...

  6. 最简单易懂的Spring Security 身份认证流程讲解

    最简单易懂的Spring Security 身份认证流程讲解 导言 相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,其实这是一个误区,Spring Security确 ...

  7. LTE(4G) AUTHENTICATION(鉴权)流程

    authentication(鉴权) 鉴权的目的是为了保证接入网络的UE是否合法的UE,网络时合法的网络,LTE的鉴权有双重鉴权的概念,网络对于UE进行鉴权,UE也会对网络进行鉴权 MME触发,且网络 ...

  8. spring gateway 鉴权_通过spring实现service变成controller,代码得到了简化

    在网上发现了一个牛X的思路,在做restful的时候,如果业务改变,需要每次都修改controller,后来方便了,直接透传的方式,其实也比较麻烦,每次都要写controller.需求变了接口也发生了 ...

  9. Spring Security 认证执行流程

    本文基于 Spring Security 5.x 推荐阅读: 项目集成Spring Security SpringSecurity 整合 JWT 一.外层-正常登陆调用 项目启动后会自动寻找 User ...

最新文章

  1. Myeclipse在启动tomcat的时候的模式改变
  2. 个人网站搭建---godaddy域名+freewebhostingarea免费空间
  3. hdu2102 水搜索
  4. JavaScript原型学习
  5. python open方法下file模块_python 文件操作
  6. IT项目管理总结:第五章 项目范围管理
  7. 获取Ip所在城市名与详细
  8. 用户认证-什么是认证
  9. SVM入门(八)松弛变量(转)
  10. python 文本相似度计算函数_四种计算文本相似度的方法对比
  11. 修复漏洞的Istio 1.1.1 发布了
  12. linux java缓存失效_转载:Linux服务器Cache占用过多内存导致系统内存不足最终java应用程序崩溃解决方案...
  13. In App Purchases 入门
  14. Spring Cloud Stream 简单使用
  15. Python微信、QQ自动发消息
  16. 电容式麦克风和动圈式麦克风的工作原理
  17. 蒋鑫鸿:9.7国际黄金、纸白银行情走势分析、原油操作建议
  18. 无限制神器aria2懒人包及Aria2配置/Web管理面板教程
  19. Ubuntu 常用小命令(持续更新~)
  20. 101条计算机经典名言(英汉对照版)

热门文章

  1. 【力扣周赛】第345场周赛
  2. 转自:如何自学Android(强烈推荐)
  3. 传记-《成为米歇尔·奥巴马》书中的精髓:米歇尔·奥巴马是怎样克服种族歧视、冲破阶层天花板,成为美国首位非洲裔第一夫人的。
  4. 再探勒索病毒之删除卷影副本的方法
  5. 【图像检索】基于HSV的图像检索附matlab代码
  6. 亿万级别商品详情页面设计
  7. python中的del含义
  8. python(27) 抓取淘宝买家秀
  9. 三国时期齐名人物一览
  10. Python学习-批量提取图片名称保存至txt文件