Spring Security 中的权限注解很神奇吗?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security 中的权限注解很神奇吗?相关的知识,希望对你有一定的参考价值。
参考技术A最近有个小伙伴在微信群里问 Spring Security 权限注解的问题:
很多时候事情就是这么巧,松哥最近在做的 tienchin 也是基于注解来处理权限问题的,所以既然大家有这个问题,咱们就一块来聊聊这个话题。
先来看看 Spring Security 权限注解的具体用法,如下:
类似于上面这样,意思就是说,当前用户需要具备 tienchin:channel:query 权限,才能执行当前的接口方法。
那么要搞明白 @PreAuthorize 注解的原理,我觉得得从两个方面入手:
我们一个一个来看。
Spring Expression Language(简称 SpEL)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言。它的语法类似于传统 EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。
SpEL 给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性基于 Spring 产品的需求而设计,这是它出现的一大特色。
在我们离不开 Spring 框架的同时,其实我们也已经离不开 SpEL 了,因为它太好用、太强大了,SpEL 在整个 Spring 家族中也处于一个非常重要的位置。但是很多时候,我们对它的只了解一个大概,其实如果你系统的学习过 SpEL,那么上面 Spring Security 那个注解其实很好理解。
我先通过一个简单的例子来和大家捋一捋 SpEL。
为了省事,我就创建一个 Spring Boot 工程来和大家演示,创建的时候不用加任何额外的依赖,就最最基础的依赖即可。
代码如下:
expressionStr 是我们自定义的一个表达式字符串,这个字符串通过一个 ExpressionParser 对象将之解析为一个 Expression,接下来就可以执行这个 exp 了。
执行的时候有两种方式,对于我们上面这种不带任何额外变量的,我们可以直接执行,直接执行的方式如下:
这个打印结果为 3。
我记得之前有个小伙伴在群里问想执行一个字符串表达式,但是不知道怎么办,js 中有 eval 函数很方便,我们 Java 中也有 SpEL,一样也很方便。
不过很多时候,我们要执行的表达式可能比较复杂,这时候上面这种调用方式就不太够用了。
此时我们可以为要调用的表达式设置一个上下文环境,这个时候就会用到 EvaluationContext 或者它的子类,如下:
当然上面这个表达式不需要设置上下文环境,我举一个需要设置上下文环境的例子。
例如我现在有一个 User 类,如下:
现在我的表达式是这样:
这个表达式就表示获取 user 对象的 username 属性。将来创建一个 user 对象,放到 StandardEvaluationContext 中,并基于此对象执行表达式,就可以打印出来想要的结果。
如果我们将 user 对象设置为 rootObject,那么表达式中就不需要 user 了,如下:
表达式就一个 username 字符串,将来执行的时候,会自动从 user 中找到 username 的值并返回。
当然表达式也可以是方法,例如我在 User 类中添加如下两个方法:
我们就可以通过表达式调用这两个方法,如下:
调用有参的 sayHello:
就直接写方法名然后执行就行了。
调用无参的 sayHello:
这些就都好懂了。
甚至,我们的表达式也可以涉及到 Spring 中的一个 Bean,例如我们向 Spring 中注册如下 Bean:
然后通过 SpEL 表达式来调用这个名为 us 的 bean 中的 sayHello 方法,如下:
给配置的上下文环境设置一个 bean 解析器,这个 bean 解析器会自动跟进名字从 Spring 容器中找打响应的 bean 并执行对应的方法。
当然,关于 SpEL 的玩法还有很多,我就不一一列举了。这里主要是想让小伙伴们知道,有这么个技术,方便大家理解 @PreAuthorize 注解的原理。
接下来我们就回到 Spring Security 中来看 @PreAuthorize 注解。
权限的实现方式千千万,又有各种不同的权限模型,然而归结到代码上,无非两种:
松哥之前的 vhr 使用的是前者。
@PreAuthorize 注解当然对应的是后者。这次做的 tienchin 项目就是后者,我们来看一个例子:
注解好说,里边的 @ss.hasPermi(\'tienchin:channel:query\') 是啥意思呢?
这个 hasPermi 方法的逻辑其实很简单:
这个判断逻辑很简单,就是获取到当前登录的用户,判断当前登录用户的权限集合中是否具备当前请求所需要的权限。具体的判断逻辑没啥好说的,就是看集合中是否存在某个字符串。
那么这个方法是在哪里调用的呢?
大家知道,Spring Security 中处理权限的过滤器是 FilterSecurityInterceptor,所有的权限处理最终都会来到这个过滤器中。在这个过滤器中,将会用到各种投票器、表决器之类的工具,这里我就不细说了,之前的 Spring Security 系列教程都有详细介绍。
在投票器中,我们可以看到专门处理 @PreAuthorize 注解的类 PreInvocationAuthorizationAdviceVoter,我们来看下他里边的核心方法:
框架的源码写的就是好,你一看名字就知道他想干嘛了!这里就进入到最后一句,调用了一个 Advice 中到前置通知,来判断权限是否满足:
现在,当你看到这个 before 方法的时候,应该会觉得比较熟悉了吧。
就这样,是不是很简单?
好啦,今天就和小伙伴们分享这么多,在松哥近期推出的 tienchin 项目视频中,也会通过视频的形式跟大家细聊这个知识点。
spring security注解
Chapter 15. 基于表达式的权限控制
Spring Security 3.0介绍了使用Spring EL表达式的能力,作为一种验证机制 添加简单的配置属性的使用和访问决策投票,就像以前一样。 基于表达式的安全控制是建立在相同架构下的,但是允许使用复杂的布尔逻辑 包含在单独的表达式中。
Spring Security使用Spring El来支持表达式,你应该看一下如何工作的 如果你对深入了解这个话题感兴趣的话。表达式是执行在一个“根对象” 上的,作为一部分执行上下文。Spring Security使用特定的类对应web和方法安全,作为根对象, 为了提供内建的表达式,访问值,比如当前的认证主体。
表达式根对象的基本类是SecurityExpressionRoot
。 这个提供了一些常用的表达式,都可以在web和method权限控制中使用。
Table 15.1. 常用内建表达式
表达式 | 说明 |
---|---|
hasRole([role]) |
返回 true 如果当前主体拥有特定角色。 |
hasAnyRole([role1,role2]) |
返回 true 如果当前主体拥有任何一个提供的角色 (使用逗号分隔的字符串队列) |
principal |
允许直接访问主体对象,表示当前用户 |
authentication |
允许直接访问当前 Authentication 对象 从SecurityContext 中获得 |
permitAll |
一直返回true |
denyAll |
一直返回false |
isAnonymous() |
如果用户是一个匿名登录的用户 就会返回 true |
isRememberMe() |
如果用户是通过remember-me 登录的用户 就会返回 true |
isAuthenticated() |
如果用户不是匿名用户就会返回true |
isFullyAuthenticated() |
如果用户不是通过匿名也不是通过remember-me登录的用户时, 就会返回true 。 |
为了保护单独的URL,你需要首先把<http>
的 use-expressions
属性设置为true。Spring Security会把 <intercept-url>
的access
当做包含了Spring EL表达式。 表达式应该返回boolean,定义访问是否应该被允许,比如:
<http use-expressions="true"> <intercept-url pattern="/admin*" access="hasRole(‘admin‘) and hasIpAddress(‘192.168.1.0/24‘)"/> ... </http>
这里我们已经定义了应用的“admin”范围(通过URL模式定义的) 应该只对拥有“admin”权限的用户有效,并且用户要在本地子网的IP地址下。 我们已经看到了内建的hasRole
表达式,在上一章节。 表达式hasIpAddress
是一个附加的内建表达式,是由web安全提供的。 它是由WebSecurityExpressionRoot
定义的,它的实例作为表达式根对象 当执行web权限表达式时。这个对象也直接暴露了HttpServletRequest
对象 使用request
这个名字,所以你可以直接调用request在一个表达式里。
如果使用了表达式,一个WebExpressionVoter
会被添加到 AccessDecisionManager
中,这是被命名空间创建的。 所以如果你没有使用命名空间还希望使用表达式,你必须把这些添加到你的配置中。
方法安全有一点儿复杂,比单独允许和拒绝规则来说。Spring Security 3.0介绍了一些新注解, 为了对复杂的表达式进行支持。
这里有四个注解,支持表达式属性允许进行前置和后置调用验证检测,也支持过滤提交的 集合参数或返回值。它们是@PreAuthorize
, @PreFilter
, @PostAuthorize
和 @PostFilter
。它们可以通过 global-method-security
命名空间元素启用:
<global-method-security pre-post-annotations="enabled"/>
使用最广泛的注解是@PreAuthorize
,它可以决定一个方法是否可以被调用。 比如(来自“Contacts”实例应用)
@PreAuthorize("hasRole(‘ROLE_USER‘)") public void create(Contact contact);
这意味着只允许拥有"ROLE_USER"角色的用户访问。 很明显,相同的事情可以简单实用传统的配置方法,简单的配置属性来要求角色。 但是如果是这样呢:
@PreAuthorize("hasPermission(#contact, ‘admin‘)") public void deletePermission(Contact contact, Sid recipient, Permission permission);
这里我们其实使用了方法参数作为表达式的一部分来决定是否当前的用户拥有“admin” 表达式对给定的contract。内建的 hasPermission()
表达式链接到 Spring Security ACL模块,通过applicaton context。你可以访问任何方法参数,通过名称 就像表达式的变量,在编译的时候使用debug信息。任何Spring EL功能都可以在表达式里使用, 所以你可以访问参数的属性。比如,如果你希望特定的方法只允许访问一个用户名和contract匹配, 你可以写成
@PreAuthorize("#contact.name == authentication.name)") public void doSomething(Contact contact);
这里我们访问了另一个内建的表达式,authentication
, 它是保存在安全上下文里的Authentication
实例。 你也可以直接访问它的“principal”属性, 使用principal
表达式。这个值一般是一个 UserDetails
实例,所以你可能使用一个 像是principal.username
或是 principal.enabled
的表达式。
不太常见的是,你可能希望之星一个访问控制检测,在方法调用之后。这个可以使用 @PostAuthorize
注解。为了访问一个方法的返回值, 使用表达式中的内建名称returnObject
。
你可能已经知道了,Spring Security支持对集合和数据的过滤, 现在也可以使用表达式实现了。通常是用在一个方法的返回值中。比如:
@PreAuthorize("hasRole(‘ROLE_USER‘)") @PostFilter("hasPermission(filterObject, ‘read‘) or hasPermission(filterObject, ‘admin‘)") public List<Contact> getAll();
当使用@PostFilter
注解时,Spring Security遍历返回的集合 删除任何表达式返回false的元素。名字filterObject
引用 集合中的当前对象。你也可以在方法调用之前进行过滤,使用@PreFilter
虽然很少需要这样做。这个语法是一样的,但是如果这里有更多参数,都是集合类型 然后你必须使用注解的filterTarget
属性选择其中一个。
注意过滤显然不是一个让你读取数据查询的解决方法。如果你过滤大型集合,删除很多实体, 然后这就会是非常没有效率的。
以上是关于Spring Security 中的权限注解很神奇吗?的主要内容,如果未能解决你的问题,请参考以下文章
spring-security 开启注解权限控制为什么没有效果
Spring Security当使用注解标记权限时修改ROLE_前缀