> 文档中心 > Spring Security 中的权限注解很神奇吗?

Spring Security 中的权限注解很神奇吗?

最近有个小伙伴在微信群里问 Spring Security 权限注解的问题:

很多时候事情就是这么巧,松哥最近在做的 tienchin 也是基于注解来处理权限问题的,所以既然大家有这个问题,咱们就一块来聊聊这个话题。

当然一些基础的知识我就不讲了,对于 Spring Security 基本用法尚不熟悉的小伙伴,可在公众号后台回复 ss,有原创的系列教程。

1. 具体用法

先来看看 Spring Security 权限注解的具体用法,如下:

@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")@GetMapping("/list")public TableDataInfo getChannelList() {    startPage();    List 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 方法,如下:

@AutowiredBeanFactory beanFactory;@Testvoid 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 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 permissions, String permission) {    return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}

这个判断逻辑很简单,就是获取到当前登录的用户,判断当前登录用户的权限集合中是否具备当前请求所需要的权限。具体的判断逻辑没啥好说的,就是看集合中是否存在某个字符串。

那么这个方法是在哪里调用的呢?

大家知道,Spring Security 中处理权限的过滤器是 FilterSecurityInterceptor,所有的权限处理最终都会来到这个过滤器中。在这个过滤器中,将会用到各种投票器、表决器之类的工具,这里我就不细说了,之前的 Spring Security 系列教程都有详细介绍。

在投票器中,我们可以看到专门处理 @PreAuthorize 注解的类 PreInvocationAuthorizationAdviceVoter,我们来看下他里边的核心方法:

@Overridepublic int vote(Authentication authentication, MethodInvocation method, Collection 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 创建一个上下文对象,此时创建出来的上下文对象中就包含了当前用户具备哪些权限。
  • 获取过滤器(我们这个项目中无)。
  • 获取到权限注解。
  • 最后执行表达式,去查看当前用户权限中是否包含请求所需要的权限。

就这样,是不是很简单?

 

前言

作为开发人员,对于Spring全家桶肯定是不陌生的,而来自于Spring大家族的Spring Boot,作为Spring团队提供的流行框架,它的存在解决的Spring框架使用较为繁琐的问题,所以掌握SpringBoot是精通Spring必不可少的一个过程。

在面试过程中,SpringBoot的相关内容都会被面试官给问到,几乎一线互联网对于技术岗的任职要求都对Spring有一定的规定,所以学习Spring刻不容缓。

而今天,我们则主要一起来学习SpringBoot相关内容,这份华为大神珍藏版分享给你——SpringBoot全优笔记,面面俱到太全面了!

PART1:SpringBoot入门之打好扎实基础

1.SpringBoot的来临

  • Spring的历史
  • 注解还是XML
  • SpringBoot的优点
  • 传统Spring MVC和Spring Boot的对比

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

2.聊聊开发环境搭建和基本开发

  • 搭建Spring Boot开发环境
  • Spring Boot的依赖和自动配置
  • 使用自定义配置
  • 开发自己的Spring Boot项目

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

3.全注解下的Spring IOC

  • IOC容器简介
  • 装配你的Bean
  • 依赖注入
  • 生命周期
  • 使用属性文件
  • 条件装配Bean
  • Bean的作用域
  • 使用@Profile
  • 引入XML配置Bean
  • 使用Spring EL

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

4.开始约定编程——SpringAOP

  • 约定编程
  • AOP的概念
  • AOP开发详解
  • 多个切面

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

PART2:SpringBoot提升之修炼秘籍

1.访问数据库

  • 配置数据源
  • 使用JDBC Template操作数据库
  • 使用JPA(Hibernate)操作数据
  • 整合MyBaits框架

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

2.聊聊数据库事务处理

  • JDBC的数据库事务
  • Spring声明式事务的使用
  • 隔离级别
  • 传播行为
  • @Transactional自调用失效问题

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

3.使用性能利器——Redis

  • spring-data-redis项目简介
  • 在Spring Boot中配置和使用Redis
  • Redis的一些特殊用法
  • 使用Spring缓存注解操作Redis

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

4.文档数据库——MongoDB

  • 配置MongoDB
  • 使用MongoTemplate实例
  • 使用JPA

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

PART3:SpingBoot进阶学习之开发实战

1.初识Spring MVC

  • Spring MVC框架的设计
  • SpringMVC流程
  • 定制Spring MVC的初始化
  • Spring MVC实例

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

2.深入Spring MVC开发

  • 处理器映射
  • 获取控制器参数
  • 自定义参数转换规则
  • 数据验证
  • 数据模型
  • 视图和视图解析器
  • 文件上传
  • 拦截器
  • 国际化
  • Spring MVC拾遗

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

3.构建REST风格网站

  • REDT简述
  • 使用Spirng MVC开发REST风格端点
  • 客户端请求RestTemplate

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

4.安全——Spring Security

  • 概述和简单安全认证
  • 使用WebSecurityConfigurerAdapter自定义
  • 自定义用户服务信息
  • 限制请求
  • 用户认证功能

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

5.学点Spring其他的技术

  • 异步线程池
  • 异步消息
  • 定时任务
  • WebSocket应用

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

6.Spring5框架——WebFlux

  • 基础概念
  • 通过Spring MVC方式开发WebFlux服务端
  • 深入WebFlux服务端开发
  • 深入客户端开发
  • 使用路由函数方式开发WebFlux

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

7.实践一下——抢购商品

  • 设计与开发(数据库表设计+使用MyBatis开发持久层+使用Spring开发业务层和控制层+测试和配置)

  • 高并发开发(超发现象+悲观锁+乐观锁+使用Redis处理高并发)

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

8.部署、测试和监控

  • 部署和运行
  • 测试
  • Actuator监控端点
  • HTTP监控
  • JMX监控

华为大神珍藏版:SpringBoot全优笔记,面面俱到太全了

总结

很显然,微服务是未来的发展趋势,项目会从传统架构一点一点转向微服务架构。不论你是互联网人才,还是传统行业从业者,学习流行技术是非常重要的。SpringBoot作为官方大力推荐的一个技术,是每一个开发人员都应该要掌握的重点。

需要资料的小伙伴点赞 +关注下方公众号免费即可获取哦

学习不难,贵在自觉和坚持。

粉色屋