最近有个小伙伴在微信群里问 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 注解的原理,我觉得得从两个方面入手:

  1. 首先明白 Spring 中提供的 SpEL。
  2. 其次搞明白 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 方法的时候,应该会觉得比较熟悉了吧。

  1. 首先获取到 preAttr 对象,这个对象里边其实就保存着你 @PreAuthorize 注解中的内容。
  2. 接下来跟进当前登录用户信息 authentication 创建一个上下文对象,此时创建出来的上下文对象中就包含了当前用户具备哪些权限。
  3. 获取过滤器(我们这个项目中无)。
  4. 获取到权限注解。
  5. 最后执行表达式,去查看当前用户权限中是否包含请求所需要的权限。

就这样,是不是很简单?

好啦,今天就和小伙伴们分享这么多,在松哥近期推出的 tienchin 项目视频中,也会通过视频的形式跟大家细聊这个知识点。

Spring Security 中的权限注解很神奇吗?相关推荐

  1. java按钮权限控制_详解Spring Security 中的四种权限控制方式

    Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Se ...

  2. Spring Security 中,想在权限中使用通配符,怎么做?

    小伙伴们知道,在 Shiro 中,默认是支持权限通配符的,例如系统用户有如下一些权限: system:user:add system:user:delete system:user:select sy ...

  3. Spring Security 中最流行的权限管理模型!

    前面和大家说了 ACL,讲了理论,也给了一个完整的案例,相信小伙伴们对于 ACL 权限控制模型都已经比较了解了. 本文我要和大家聊一聊另外一个非常流行的权限管理模型,那就是 RBAC. 1.RBAC ...

  4. 一起搞清楚 Spring Security 中的 UserDetails

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 170元买400元书的机会又来啦! 1. 前言 前一篇介绍了 Spring Security ...

  5. Spring Security入门到实践(一)HTTP Basic在Spring Security中的应用原理浅析

    一.Spring Security简介 打开Spring Security的官网,从其首页的预览上就可以看见如下文字: Spring Security is a powerful and highly ...

  6. Spring Security中文文档

    Spring Security中文文档 来源:https://www.springcloud.cc/spring-security.html#overall-architecture 作者 Ben A ...

  7. 基于 Spring Security 搭建用户权限系统(二) - 自定义配置

    说明 本文的目的是如何基于 Spring Security 去扩展实现一个基本的用户权限模块, 内容会覆盖到 Spring Security 常用的配置. 文中涉及到的业务代码是不完善的, 甚至会存在 ...

  8. Spring Security实现登录权限控制,记住我等功能

    文章目录 1.Spring Security简介 2.登录权限控制 2.1.基本功能 2.2.登录和未登录导航栏显示 2.3.根据权限显示不同页面功能 3.定制登录页和记住我功能 3.1.定制登录页 ...

  9. springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...

    今日干货 刚刚发表查看:66666回复:666 公众号后台回复 ssm,免费获取松哥纯手敲的 SSM 框架学习干货. 上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御.主要 ...

最新文章

  1. Android 追加写入文件的三种方法
  2. CF A. DZY Loves Hash
  3. 由Effiproz DataBase来看.NET开源数据库发展
  4. python3.5安装教程linux_linux安装python3.5
  5. [css] 有哪些方式可以对一个DOM设置它的CSS样式?
  6. python 描述性分析_描述性分析-1对被解释变量进行描述
  7. 格式化字符串漏洞利用 四、利用的变体
  8. Matlab二进制文件的读写
  9. 将您的SQL Server工作负载迁移到PostgreSQL –第1部分
  10. c语言字符串去除第一个和最后一个_387. 字符串中的第一个唯一字符
  11. 让刺猬和狐狸结婚:资本巨鳄BlackRock的金融科技野心
  12. 使用C#的后端Web API:循序渐进教程
  13. 感受MapXtreme2004之二
  14. 调查上网行为管理软件(或设备)
  15. BIGEMAP手机离线地图——基于OruxMaps离线高清卫星地图制作发布
  16. BIS新增手机测试认证标准 IS16333 (Part 3)2017.6.30 印度语
  17. 简单入侵ftp服务器
  18. Matlab中绘制颜色渐变曲线
  19. 科学计算机的圆周率,科学家用超级计算机计算圆周率,到底有什么意义?真能算出来吗?...
  20. BlockingQueue(阻塞队列)

热门文章

  1. PHP将图片验证码转换成base64格式
  2. PCI-7856运动控制卡
  3. 规划 程序员的35岁危机
  4. 自我反省,自我批评、解剖,对近期行为的反思
  5. 红黑树系列1——红黑树的建立
  6. EarlyStopping早停法的实现原理
  7. Win10为什么电脑在有密码的情况下,睡眠后不用输密码打开?
  8. anyview+4.0java版_anyview4.0手机阅读器
  9. c语言中箭头的作用,为什么C中的箭头(-)运算符存在?
  10. 知云文献翻译跨页内容选中翻译操作