Spring Security 中的权限注解很神奇吗?
最近有个小伙伴在微信群里问 Spring Security 权限注解的问题:
很多时候事情就是这么巧,松哥最近在做的 tienchin 也是基于注解来处理权限问题的,所以既然大家有这个问题,咱们就一块来聊聊这个话题。
当然一些基础的知识我就不讲了,对于 Spring Security 基本用法尚不熟悉的小伙伴,可在公众号后台回复 ss,有原创的系列教程。
1. 具体用法
先来看看 Spring Security 权限注解的具体用法,如下:
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping("/list")
public TableDataInfo getChannelList() {startPage();List<Channel> list = channelService.list();return getDataTable(list);
}
类似于上面这样,意思就是说,当前用户需要具备 tienchin:channel:query
权限,才能执行当前的接口方法。
那么要搞明白 @PreAuthorize 注解的原理,我觉得得从两个方面入手:
- 首先明白 Spring 中提供的 SpEL。
- 其次搞明白 Spring Security 中对方法注解的处理规则。
我们一个一个来看。
2. 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 注解的原理。
3. @PreAuthorize
接下来我们就回到 Spring Security 中来看 @PreAuthorize 注解。
权限的实现方式千千万,又有各种不同的权限模型,然而归结到代码上,无非两种:
- 基于 URL 地址的权限处理
- 基于方法注解的权限处理
松哥之前的 vhr 使用的是前者。
@PreAuthorize 注解当然对应的是后者。这次做的 tienchin 项目就是后者,我们来看一个例子:
@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));
}
这个判断逻辑很简单,就是获取到当前登录的用户,判断当前登录用户的权限集合中是否具备当前请求所需要的权限。具体的判断逻辑没啥好说的,就是看集合中是否存在某个字符串。
那么这个方法是在哪里调用的呢?
大家知道,Spring Security 中处理权限的过滤器是 FilterSecurityInterceptor,所有的权限处理最终都会来到这个过滤器中。在这个过滤器中,将会用到各种投票器、表决器之类的工具,这里我就不细说了,之前的 Spring Security 系列教程都有详细介绍。
在投票器中,我们可以看到专门处理 @PreAuthorize 注解的类 PreInvocationAuthorizationAdviceVoter,我们来看下他里边的核心方法:
@Override
public int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) {PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);if (preAttr == null) {return ACCESS_ABSTAIN;}return this.preAdvice.before(authentication, method, preAttr) ? ACCESS_GRANTED : ACCESS_DENIED;
}
框架的源码写的就是好,你一看名字就知道他想干嘛了!这里就进入到最后一句,调用了一个 Advice 中到前置通知,来判断权限是否满足:
public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) {PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr;EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);Expression preFilter = preAttr.getFilterExpression();Expression preAuthorize = preAttr.getAuthorizeExpression();if (preFilter != null) {Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi);this.expressionHandler.filter(filterTarget, preFilter, ctx);}return (preAuthorize != null) ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true;
}
现在,当你看到这个 before 方法的时候,应该会觉得比较熟悉了吧。
- 首先获取到 preAttr 对象,这个对象里边其实就保存着你 @PreAuthorize 注解中的内容。
- 接下来跟进当前登录用户信息 authentication 创建一个上下文对象,此时创建出来的上下文对象中就包含了当前用户具备哪些权限。
- 获取过滤器(我们这个项目中无)。
- 获取到权限注解。
- 最后执行表达式,去查看当前用户权限中是否包含请求所需要的权限。
就这样,是不是很简单?
好啦,今天就和小伙伴们分享这么多,在松哥近期推出的 tienchin 项目视频中,也会通过视频的形式跟大家细聊这个知识点。
Spring Security 中的权限注解很神奇吗?相关推荐
- java按钮权限控制_详解Spring Security 中的四种权限控制方式
Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Se ...
- Spring Security 中,想在权限中使用通配符,怎么做?
小伙伴们知道,在 Shiro 中,默认是支持权限通配符的,例如系统用户有如下一些权限: system:user:add system:user:delete system:user:select sy ...
- Spring Security 中最流行的权限管理模型!
前面和大家说了 ACL,讲了理论,也给了一个完整的案例,相信小伙伴们对于 ACL 权限控制模型都已经比较了解了. 本文我要和大家聊一聊另外一个非常流行的权限管理模型,那就是 RBAC. 1.RBAC ...
- 一起搞清楚 Spring Security 中的 UserDetails
点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 170元买400元书的机会又来啦! 1. 前言 前一篇介绍了 Spring Security ...
- Spring Security入门到实践(一)HTTP Basic在Spring Security中的应用原理浅析
一.Spring Security简介 打开Spring Security的官网,从其首页的预览上就可以看见如下文字: Spring Security is a powerful and highly ...
- Spring Security中文文档
Spring Security中文文档 来源:https://www.springcloud.cc/spring-security.html#overall-architecture 作者 Ben A ...
- 基于 Spring Security 搭建用户权限系统(二) - 自定义配置
说明 本文的目的是如何基于 Spring Security 去扩展实现一个基本的用户权限模块, 内容会覆盖到 Spring Security 常用的配置. 文中涉及到的业务代码是不完善的, 甚至会存在 ...
- Spring Security实现登录权限控制,记住我等功能
文章目录 1.Spring Security简介 2.登录权限控制 2.1.基本功能 2.2.登录和未登录导航栏显示 2.3.根据权限显示不同页面功能 3.定制登录页和记住我功能 3.1.定制登录页 ...
- springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...
今日干货 刚刚发表查看:66666回复:666 公众号后台回复 ssm,免费获取松哥纯手敲的 SSM 框架学习干货. 上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御.主要 ...
最新文章
- Android 追加写入文件的三种方法
- CF A. DZY Loves Hash
- 由Effiproz DataBase来看.NET开源数据库发展
- python3.5安装教程linux_linux安装python3.5
- [css] 有哪些方式可以对一个DOM设置它的CSS样式?
- python 描述性分析_描述性分析-1对被解释变量进行描述
- 格式化字符串漏洞利用 四、利用的变体
- Matlab二进制文件的读写
- 将您的SQL Server工作负载迁移到PostgreSQL –第1部分
- c语言字符串去除第一个和最后一个_387. 字符串中的第一个唯一字符
- 让刺猬和狐狸结婚:资本巨鳄BlackRock的金融科技野心
- 使用C#的后端Web API:循序渐进教程
- 感受MapXtreme2004之二
- 调查上网行为管理软件(或设备)
- BIGEMAP手机离线地图——基于OruxMaps离线高清卫星地图制作发布
- BIS新增手机测试认证标准 IS16333 (Part 3)2017.6.30 印度语
- 简单入侵ftp服务器
- Matlab中绘制颜色渐变曲线
- 科学计算机的圆周率,科学家用超级计算机计算圆周率,到底有什么意义?真能算出来吗?...
- BlockingQueue(阻塞队列)