说来惭愧,Spring Security 系列前前后后写了 60 多篇文章了,竟然漏掉了如此重要的一块。
平时在公司项目中,都是能用则用,先把需求整出来,有的时候也不去深究它的原理,这无形中就给自己挖了大坑。
松哥这次就是,不过也因此又发现了 Spring Security 的一个用法,今天就和小伙伴们分享下。
1.缘起
事情是这样的,看过 vhr 项目(https://github.com/lenve/vhr)的小伙伴都知道 vhr 里边有一个动态权限管理功能,实现的思路就是重写了 FilterInvocationSecurityMetadataSource 以及决策管理器 AccessDecisionManager,代码就类似下面这样(小伙伴们可以在 GitHub 上查看完整代码):
1 | @Override |
昨天我想再写一个类似的功能,本想着很简单,三下五除二就搞定,大家看下:
1 | http |
小伙伴们能看出这两段代码的差别吗?
写完之后,启动项目,一启动就报错了!
我就有点懵。之前的 vhr 启动是没问题的,但是这次新的项目启动就有问题。
在 IDEA 中,通过 Ctrl+Shift+F 全局搜索,找到了异常抛出的位置:
如果 requestMap 变量为空,就会抛出异常。requestMap 就是我们在 configure 方法中配置的请求和权限的映射,不过在上面的案例中,我是想像 vhr 那样做动态权限管理,所以请求和角色的映射关系我是保存在数据库中,没有必要在代码中配置。
但是根据异常提示,我就先随便加一个映射,果然启动就不报错了:
1 | http |
现在让我有两个郁闷的地方:
- vhr 为什么可以启动而不报错
- 这里代码中多出来的一行映射明显是累赘,最好能够去掉
2.问题分析
第一个问题很好办,仔细对比最开始贴出来的代码就能找到端倪,vhr 中的代码多了一个 permitAll 方法,这就是一个表达式,有了该方法,就意味着里边多了一个映射关系,/login -> permitAll
,这样 requestMap 就不会为空,所以启动时就不报错了。
按照这个思路,我去修改了自己的代码,去除冗余的映射,加上 permitAll,这次果然可以了。
问题虽然解决了,但是心里还是有点膈应。
为什么呢?因为用户-角色-资源的映射关系我都保存在数据库中了,权限数据库类似下面这样:
既然所有的权限管理都保存在数据库中了,再在代码中配置似乎就不太合适!但是不配置,项目启动又会出错,看来还是得从源码入手。
于是松哥花了点时间,把这里涉及到的相关源码仔细梳理了一遍。
3.源码梳理
首先大家知道,Spring Security 中的权限控制有两种不同的方式:
- 通过 URL 请求地址进行控制。
- 通过方法进行控制。
如果通过 URL 请求地址进行控制,负责控制类配置的是 AbstractInterceptUrlConfigurer,我们来看下它的子类:
可以看到它有两个子类:
- ExpressionUrlAuthorizationConfigurer
- UrlAuthorizationConfigurer
两个都可以处理基于 URL 请求地址的权限控制。不同的是,第一个 ExpressionUrlAuthorizationConfigurer
支持权限表达式,第二个不支持。
什么是权限表达式?其实大家都有用,只是可能没注意过这些概念,下图就是 Spring Security 中提供的内置通用权限表达式:
ExpressionUrlAuthorizationConfigurer 支持权限表达式的原因是因为它使用的投票器是 WebExpressionVoter,这个投票器就是用来处理权限表达式的。
而 UrlAuthorizationConfigurer 不支持权限表达式,是因为它使用的投票器是 RoleVoter 和 AuthenticatedVoter,这两者可以用来处理角色或者权限,但是没法处理权限表达式。
关于投票器,小伙伴们可以参考松哥之前的文章:Spring Security 权限管理的投票器与表决机制。
上面说的都是默认行为,我们也可以通过修改配置,让 UrlAuthorizationConfigurer 支持权限表达式,不过一般来说没必要这样做,如果需要支持权限表达式,直接用 ExpressionUrlAuthorizationConfigurer 即可。
当我们调用如下这行代码时:
1 | http.authorizeRequests() |
实际上就是通过 ExpressionUrlAuthorizationConfigurer 去配置基于 URL 请求地址的权限控制,所以它是支持权限表达式的。例如下面这段大家再熟悉不过的代码:
1 | http.authorizeRequests() |
在 ExpressionUrlAuthorizationConfigurer 中创建 SecurityMetadataSource 时,就会检查映射关系,如果 requestMap 为空就会抛出异常:
1 | @Override |
UrlAuthorizationConfigurer 中也有 createMetadataSource 方法,但是却是另外一套实现方案:
1 | @Override |
UrlAuthorizationConfigurer 并不会检查 requestMap 是否为空,但是它会在 createRequestMap 方法中检查一下映射关系是否完整,例如下面这样:
1 | .antMatchers("/admin/**").access("ROLE_ADMIN") |
最后的 /getinfo 没有指定需要的权限,这种就是不完整,就会抛出异常。
现在大家应该大致明白 ExpressionUrlAuthorizationConfigurer 和 UrlAuthorizationConfigurer 的区别了吧。
4.问题解决
ExpressionUrlAuthorizationConfigurer 会要求至少配置一个映射关系,UrlAuthorizationConfigurer 则无此要求。
当我们想要动态配置权限拦截时,一般来说也不会使用权限表达式,数据库中保存的就是普通的权限或者角色,所以这个时候我们可以选择 UrlAuthorizationConfigurer 而不是 ExpressionUrlAuthorizationConfigurer。
反映到代码上,就是下面这样:
1 | @Override |
这样看起来就更合理一些,不用额外配置一条映射关系。
今天这篇文章可能对没了解过动态权限控制的小伙伴来说略难,大家可以参考松哥的 vhr(https://github.com/lenve/vhr)项目去了解下动态权限控制。当然,也可以参考松哥的[Spring Boot+Vue+微人事视频教程](https://mp.weixin.qq.com/s/EEmEL6tpuBT0qLfPwhj2OQ),里边也有讲动态权限控制问题。
好啦,今天就和小伙伴们分享这么多,要不是这次遇到问题,我一直都懒得去研究 ExpressionUrlAuthorizationConfigurer 和 UrlAuthorizationConfigurer 有什么区别。感兴趣的小伙伴可以自己 DEBUG 验证下本文内容哦~