详解Spring Security如何在权限中使用通配符
作者:_江南一点雨 发布时间:2023-04-17 23:41:54
前言
小伙伴们知道,在 Shiro 中,默认是支持权限通配符的,例如系统用户有如下一些权限:
system:user:add
system:user:delete
system:user:select
system:user:update
…
现在给用户授权的时候,我们可以像上面这样,一个权限一个权限的配置,也可以直接用通配符:
system:user:*
这个通配符就表示拥有针对用户的所有权限。
今天我们来聊聊 Spring Security 中对此如何处理,也顺便来看看 TienChin 项目中,这块该如何改进。
1. SpEL
要搞明白基于注解的权限管理,那么得首先理解 SpEL,不需要了解多深入,我这里就简单介绍下。
Spring Expression Language(简称 SpEL)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言。它的语法类似于传统 EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。
SpEL 给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性基于 Spring 产品的需求而设计,这是它出现的一大特色。
在我们离不开 Spring 框架的同时,其实我们也已经离不开 SpEL 了,因为它太好用、太强大了,SpEL 在整个 Spring 家族中也处于一个非常重要的位置。但是很多时候,我们对它的只了解一个大概,其实如果你系统的学习过 SpEL,那么上面 Spring Security 那个注解其实很好理解。
我先通过一个简单的例子来和大家捋一捋 SpEL。
为了省事,我就创建一个 Spring Boot 工程来和大家演示,创建的时候不用加任何额外的依赖,就最最基础的依赖即可。
代码如下:
String expressionStr = "1 + 2";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expressionStr);
expressionStr 是我们自定义的一个表达式字符串,这个字符串通过一个 ExpressionParser 对象将之解析为一个 Expression,接下来就可以执行这个 exp 了。
执行的时候有两种方式,对于我们上面这种不带任何额外变量的,我们可以直接执行,直接执行的方式如下:
Object value = exp.getValue();
System.out.println(value.toString());
这个打印结果为 3。
我记得之前有个小伙伴在群里问想执行一个字符串表达式,但是不知道怎么办,js 中有 eval 函数很方便,我们 Java 中也有 SpEL,一样也很方便。
不过很多时候,我们要执行的表达式可能比较复杂,这时候上面这种调用方式就不太够用了。
此时我们可以为要调用的表达式设置一个上下文环境,这个时候就会用到 EvaluationContext 或者它的子类,如下:
StandardEvaluationContext context = new StandardEvaluationContext();
System.out.println(exp.getValue(context));
当然上面这个表达式不需要设置上下文环境,我举一个需要设置上下文环境的例子。
例如我现在有一个 User 类,如下:
public class User {
private Integer id;
private String username;
private String address;
//省略 getter/setter
}
现在我的表达式是这样:
String expression = "#user.username";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setVariable("user", user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
这个表达式就表示获取 user 对象的 username 属性。将来创建一个 user 对象,放到 StandardEvaluationContext 中,并基于此对象执行表达式,就可以打印出来想要的结果。
如果我们将 user 对象设置为 rootObject,那么表达式中就不需要 user 了,如下:
String expression = "username";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
表达式就一个 username 字符串,将来执行的时候,会自动从 user 中找到 username 的值并返回。
当然表达式也可以是方法,例如我在 User 类中添加如下两个方法:
public String sayHello(Integer age) {
return "hello " + username + ";age=" + age;
}
public String sayHello() {
return "hello " + username;
}
我们就可以通过表达式调用这两个方法,如下:
调用有参的 sayHello:
String expression = "sayHello(99)";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
就直接写方法名然后执行就行了。
调用无参的 sayHello:
String expression = "sayHello";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("广州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
这些就都好懂了。
甚至,我们的表达式也可以涉及到 Spring 中的一个 Bean,例如我们向 Spring 中注册如下 Bean:
@Service("us")
public class UserService {
public String sayHello(String name) {
return "hello " + name;
}
}
然后通过 SpEL 表达式来调用这个名为 us 的 bean 中的 sayHello 方法,如下:
@Autowired
BeanFactory beanFactory;
@Test
void contextLoads() {
String expression = "@us.sayHello('javaboy')";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
}
给配置的上下文环境设置一个 bean 解析器,这个 bean 解析器会自动跟进名字从 Spring 容器中找打响应的 bean 并执行对应的方法。
当然,关于 SpEL 的玩法还有很多,我就不一一列举了。这里主要是想让小伙伴们知道,有这么个技术,方便大家理解 @PreAuthorize 注解的原理。
总结一下:
1.在使用 SpEL 的时候,如果表达式直接写的就是方法名,那是因为在构建 SpEL 上下文的时候,已经设置了 RootObject 了,我们所调用的方法,实际上就是 RootObject 对象中的方法。
2.在使用 SpEL 对象的时候,如果像调用非 RootObject 对象中的方法,那么表达式需要加上 @对象名 作为前缀,例如前面案例的 @us。
2. 自定义权限该如何写
那么自定义权限到底该如何写呢?首先我们来看下在 Spring Security 中,不涉及到通配符的权限该怎么处理。
松哥举一个简单的例子,我们创建一个 Spring Boot 工程,引入 Web 和 Security 依赖,为了方便,这里的用户我直接创建在内存中,配置如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:add","system:user:delete").build());
return m;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.permitAll();
return http.build();
}
}
都是常规配置,没啥好说的。注意前面的注解,开启基于注解的权限控制。
这里我多啰嗦一句,大家看创建用户的时候,调用的是 authorities 方法去设置权限的,这个跟 roles 方法其实没啥大的区别,调用 roles 方法会自动为你设置的字符串添加一个 ROLE_ 前缀,其他的其实都一样。在 Spring Security 中,role 和 permission 仅仅只是人为划分出来的东西,底层的实现包括判断逻辑基本上都是没有区别的。
接下来我们定义四个测试接口,如下:
@RestController
public class UserController {
@GetMapping("/add")
@PreAuthorize("hasPermission('/add','system:user:add')")
public String addUser() {
return "add";
}
@GetMapping("/delete")
@PreAuthorize("hasPermission('/delete','system:user:delete')")
public String deleteUser() {
return "delete";
}
@GetMapping("/update")
@PreAuthorize("hasPermission('/update','system:user:update')")
public String updateUser() {
return "update";
}
@GetMapping("/select")
@PreAuthorize("hasPermission('/select','system:user:select')")
public String selectUser() {
return "select";
}
}
接口访问都需要不同的权限。
此时如果大家启动项目去此时,系统会提示你四个接口统统都不具备权限,这是啥原因呢?我们来继续分析。
小伙伴们看这里,调用的时候 @PreAuthorize 注解中执行写方法名,不用写对象名,说明调用的方法是 RootObject 中的方法,这里的 RootObject 实际上就是 SecurityExpressionRoot,我们来看看这个对象中的 hasPermission 方法:
@Override
public boolean hasPermission(Object target, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
}
@Override
public boolean hasPermission(Object targetId, String targetType, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, (Serializable) targetId, targetType,
permission);
}
最终的调用又指向了 permissionEvaluator 对象。
在 Spring Security 中,permissionEvaluator 有一个统一的接口就是 PermissionEvaluator,但是这个接口只有一个实现类,就是 DenyAllPermissionEvaluator,看名字就知道,这是拒绝所有。
public class DenyAllPermissionEvaluator implements PermissionEvaluator {
private final Log logger = LogFactory.getLog(getClass());
/**
* @return false always
*/
@Override
public boolean hasPermission(Authentication authentication, Object target, Object permission) {
return false;
}
/**
* @return false always
*/
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission) {
return false;
}
}
这两个方法里啥都没干,直接返回了 false,这下就破案了!
所以,在 Spring Security 中,如果想判断权限,需要自己提供一个 PermissionEvaluator 的实例,我们来看下:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(permission)) {
return true;
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
我这里的判断逻辑比较简单,所以只需要实现第一个方法就行了,这个方法三个参数,第一个参数就是当前登录成功的用户对象,后面两个参数则是我们在 @PreAuthorize("hasPermission('/select','system:user:select')") 注解中的两个参数,现在该有的东西都有了,我们只需要判断需要的权限当前用户是否有就行了。
这个自定义的权限评估器写好之后,注册到 Spring 容器就行了,其他什么事情都不用做。
接下来我们就可以对刚才的四个接口进行测试了,测试过程我就不演示了,小伙伴们自行用 postman 测试就行了。
3. 权限通配符
看明白了上面的逻辑,现在不用我说,大家也知道权限通配符在 Spring Security 中是不支持的(无论你在 @PreAuthorize 注解中写的 SpEL 是哪个,调用的是哪个方法,都是不支持权限通配符的)。
例如我现在这样描述我的用户权限:
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:*").build());
return m;
}
我想用 system:user:* 字符串表示 javaboy 具有针对用户的所有权限。
直接这样写肯定是不行的,最终字符串比较一定是不会通过的。
那么怎么办呢?用正则似乎也不太行,因为 * 在正则中不代表所有字符,如果拆解字符串去比较,功能虽然也行得通,但是比较麻烦。
想来想去,想到一个办法,不知道小伙伴们是否还记得我们之前在 vhr 中用过的 AntPathMatcher,用这个不就行了!
修改后的 CustomPermissionEvaluator 如下:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (antPathMatcher.match(authority.getAuthority(), (String) permission)) {
return true;
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
修改之后,现在只要用户具备 system:user:* 权限,就四个接口都能访问了。
4. TienChin 项目怎么做的
TienChin 项目用的是 RuoYi-Vue 脚手架,我们来看下这个脚手架的实现方式:
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping("/list")
public TableDataInfo getChannelList() {
startPage();
List<Channel> list = channelService.list();
return getDataTable(list);
}
看了前面的讲解,现在 @ss.hasPermi('tienchin:channel:query') 应该很好懂了:
ss 是一个注册在 Spring 容器中的 bean,对应的类位于 org.javaboy.tienchin.framework.web.service.PermissionService 中。
很明显,hasPermi 就是这个类中的方法。
这个 hasPermi 方法的逻辑其实很简单:
public boolean hasPermi(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
private boolean hasPermissions(Set<String> permissions, String permission) {
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
这个判断逻辑很简单,就是获取到当前登录的用户,判断当前登录用户的权限集合中是否具备当前请求所需要的权限。具体的判断逻辑没啥好说的,就是看集合中是否存在某个字符串,从判断的逻辑中我们也可以看出来,这个权限也是不支持通配符的。
来源:https://blog.csdn.net/u012702547/article/details/125500145


猜你喜欢
- 在idea中安装完ActivateJrebel以后,运行时弹出激活页面,输入团队地址: http://jrebel.whrj999.com/
- 一、简介Mybatis-Plus(简称MP)是一个 Mybatis 的一个增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发
- 目录前言演示效果:实现步骤:核心点:总结前言流布局在在项目中还是会时不时地用到的,比如在搜索历史记录,分类,热门词语等可用标签来显示的,都可
- java实现学生通讯录,使用XML文件作为数据存储,实现学生编号,姓名,年龄,住址,专业,生日等信息的添加,删除,修改,查找,备份,恢复,专
- 本文实例为大家分享了Android颜色渐变动画效果的实现代码,供大家参考,具体内容如下前言案例效果的实现比较简单,利用Android自带的颜
- 本文实例讲述了Android图片压缩工具类。分享给大家供大家参考,具体如下:这里共享一个图片压缩工具类:package com.sanwei
- 解析:CLR支持两种类型:值类型和引用类型。用Jeffrey Richter(《CLR via C#》作者)的话来说,“不理解引用类型和值类
- start方法和run方法$start()$方法用来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到$cpu$时间片,就
- 本文实例为大家分享了java实现上传网络图片到微信临时素材的具体代码,供大家参考,具体内容如下package org.afuos.playc
- java带T的时间格式化public static void main(String[] args) { &nb
- final 可以适用的范围:修饰类:使用这种修饰符的类无法被继承修饰函数:被修饰的不能被重写修饰属性:1.final修饰的成员变量是常量,值
- 前言:线程的生命周期指的是线程从创建到销毁的整个过程,通常情况下线程的生命周期有以下 5 种:初始状态可运行状态运行状态休眠状态终止状态它们
- 我们很多时候会碰到这样的问题,使用多线程刷一个表的数据时需要多个线程不能重复提取数据,那么这个时候就需要使用到线程的排他锁了。在c#里面其实
- 本篇实例内容是关于C#读取CAD文件的,直接看代码//在不使用任务插件的情况下读取DWG文件的缩略图,以便在没有安装AutoCAD的计算机上
- 近期,公司推行正版化,本人使用的是JetBrains教育版,是不允许进行商业开发的,因此开启了艰难的备用IDE选型之路。最终,我选定了轻量级
- 本文实例展示了C#基于TimeSpan实现倒计时效果的方法,比较实用的功能,对于初学者来说有一定的学习参考价值。具体实现方法如下:示例代码如
- 目录前言实现思路实测前言需求 导出Excel:本身以为是一个简单得导出,但是每行得记录文件中有一列为图片url,需要下载所有记录行对应得图片
- 下面是我做C#第一个项目的过程:1.首先打开VS,这里我用的是VS2022预览版,鼠标双击打开VS;图一2.打开VS后会出现下面的页面,鼠标
- 什么是枚举类型枚举类型(Enumerated Type) 很早就出现在编程语言中,它被用来将一组类似的值包含到一种类型当中。而这种枚举类型的
- 简介记录一个利用系统分享功能进行图片分享的工具类(代码是用Kotlin写的,都是比较简单的语法,部分可能需要自定义的地方都已经标出)。调用方