一 动态权限
Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查;后两者可以用来对集合类型的参数或者返回值进行过滤,Spring Security将移除使对应表达式的结果为false的元素。
- @PreAuthorize :在方法调用之前,基于表达式的计算结果来限制对方法的访问
- @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
- @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
- @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
这些注解的参数都可接受一个SPEL 表达式,表达式可以是任意合法的SPEL表达式.
如果表达式的计算结果为true,那么安全规则通过,否则就会失败。安全规则通过或失败的结果会因为所使用注解的差异而有所不同。
除了上述几个注解外,通常还可以采用@Secured 和@RolesAllowed 来对方法进行权限控制。
需要注意的是@EnableGlobalMethodSecurity需要几个参数,如下所示:
- prePostEnabled : 确定 Spring Security 前置注释 [@PreAuthorize,@PostAuthorize,..] 是否应该启用;
- secureEnabled : 确定 Spring Security 安全注释 [@Secured] 是否应该启用;
- jsr250Enabled : 确定 JSR-250注释 [@RolesAllowed..] 是否应该启用;
可以在同一应用程序启动一个以上的类型的注释,但只有一种类型用于接口或类的行为(在类的行为没有明确定义时)。如果找到两个注解适用于特定的方法,那么只有其中的一个被应用。
1.1 @PreAuthorize注解使用
如果想要@PreAuthorize
生效,还需要设置开启prePostEnable=true
。配置代码如下:
1 | @EnableGlobalMethodSecurity(prePostEnabled=true) |
通常在项目里面会实现UserDetails
写user
类,关键在于getAuthoritie()
方法里面生成的role有没有前缀ROLE_
。
使用@PreAuthorize
是可以随意设置的
1 | // 都可以,只要和实现的getAuthoritie里面的role对上就可以 |
简单的使用方法如下:
1 | public interface UserService { |
由于@PreAuthorize可以使用Spring表达式语言,任何条件可以很容易地使用EL来表示。只有拥有角色ADMIN
的用户才能访问updateUser
方法,deleteUser
方法只能被同时拥有ADMIN
和DBA
角色的用户调用。
使用表达式时我们还可以在表达式中使用方法参数。
1 | public class UserServiceImpl implements UserService { |
在上面代码中我们定义了调用find(int id)
方法时,只允许参数id小于10的调用;调用find(String username)
时只允许username
为当前用户的用户名;定义了调用add()
方法时只有当参数user
的name
为abc
时才可以调用。
表达式中 #userEntity
直接使用了方法中的同名参数,这使得Spring Security 能够检查传入方法的参数.并将这些参数用于认证决策的指定.
1.2 @PostAuthorize注解使用
有时候可能你会想在方法调用完之后进行权限检查,这种情况比较少,但是如果你有的话,Spring Security也为我们提供了支持,通过@PostAuthorize可以达到这一效果。使用@PostAuthorize时我们可以使用内置的表达式returnObject
表示方法的返回值。
当@EnableGlobalMethodSecurity(prePostEnabled=true)
的时候,@PostAuthorize可以使用:
使用示例如下
1 | @GetMapping("/helloUser") |
另一个使用示例如下:
1 | @PostAuthorize("returnObject.id%2==0") |
上面这一段代码表示将在方法find()
调用完成后进行权限检查,如果返回值的id
是偶数则表示校验通过,否则表示校验失败,将抛出AccessDeniedException
。需要注意的是@PostAuthorize是在方法调用完成后进行权限检查,它不能控制方法是否能被调用,只能在方法调用完成后检查权限决定是否要抛出AccessDeniedException
。
第二个示例
1 | @RequestMapping("getAll") |
我们使用了 @PreAuthorize("hasRole('ROLE_USER')")
和 @PostFilter("filterObject.enabled == true")
这两个注解,表明我们希望,用户必须拥有 ROLE_USER
权限,并且返回用户属性 enabled
为true的所有用户.表达式中的 filterObject
引用的是方法返回值List
中的某一个元素,在这里是 UserEntity
,并且过滤出 enabled
为true
的UserEntity
1.3 @PreFilter注解使用
当@PreFilter标注的方法拥有多个集合类型的参数时,需要通过@PreFilter的filterTarget
属性指定当前@PreFilter是针对哪个参数进行过滤的。
如下面代码就通过filterTarget
指定了当前@PreFilter是用来过滤参数ids
的。filterObject
是使用@PreFilter和@PostFilter时的一个内置表达式,表示集合中的当前对象。
1 | @PreFilter(filterTarget="ids", value="filterObject%2==0") |
1.4 @PostFilter注解使用
使用@PreFilter和@PostFilter可以对集合类型的参数或返回值进行过滤。使用@PreFilter和@PostFilter时,Spring Security将移除使对应表达式的结果为false的元素。
1 | @PostFilter("filterObject.id%2==0") |
上述代码表示将对返回结果中id不为偶数的user进行移除。filterObject
是使用@PreFilter和@PostFilter时的一个内置表达式,表示集合中的当前对象。
1.5 @Secured注解使用
@Secured是从之前Spring版本中引入进来的。它有一个缺点(限制)就是不支持Spring EL表达式
使用@Secured
注解,先要配置@EnableGlobalMethodSecurity(securedEnabled = true)
使用示例如下
1 | public interface BankService { |
@Secured
和@PreAuthorize
用法基本一样,但是里面有个大坑,@Secured
对应的角色必须要有ROLE_
前缀。
在上面的例子中,updateUser
方法可以由具有 ADMIN
角色的人调用,而deleteUser
可以由DBA
或ADMIN
角色的人被调用。如果不拥有所需的角色而试图调用一个方法,那么将一个访问拒绝并将引发异常。
如果你想要指定AND
条件,即想调用deleteUser
方法同时拥有ADMIN
和DBA
角色的用户。这是不可能绕过 @Secured 注释的。
这可以使用 Spring 新的 @PreAuthorize/@PostAuthorize 注解来支持 Spring EL 。
1.6 @RolesAllowed注解使用
@RolesAllowed注解和@Secured注解在各个方面基本上都是一致的。唯一显著的区别在于@RolesAllowed是JSR-250定义的Java标准注解.
在使用@RolesAllowed
之前,需要配置:@EnableGlobalMethodSecurity(jsr250Enabled=true)
JSR250注解还有 @DenyAll
和 @PermitAll
。
1 | @RequestMapping("/test1") |
尽管我们这里只是启用了jsr250Enabled
,但需要说明的一点是这与securedEnabled
并不冲突。这两种注解风格可以同时启用.
这两个注解都有一个不足之处,它们只能根据用户有没有授予特定的权限来限制方法的调用,在判断方式是否执行方面,无法使用其他的因素.
二 权限注解中的问题
若采用@EnableGlobalMethodSecurity(securedEnabled = true)
注解,对函数访问进行控制,那么,就会有一些问题(不加ROLE
),因为,这个时候AccessDecissionManager
会选择RoleVoter
进行vote
,但是RoleVoter
默认的rolePrefix
是 ROLE 。
对于下面的示例
1 | @Secured({"ROLE_ROOT"}) |
而RoleVoter
选举时,会检测是否支持。
1 | public Boolean supports(ConfigAttribute attribute) { |
上面的函数会返回true
,因为传递进去的attribute
是来自于@Secured({"ROLE_ROOT"})
注解。不幸的时,当进入RoleVoter
的vote
函数时,就失败了:
1 | public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { |
原因在于,authority.getAuthority()
返回的将是ROOT
,而并不是ROLE_ROOT
。然而,即使将@Secured({"ROLE_ROOT"})
改为@Secured({"ROOT"})
也没有用, 所以,即使当前用户是ROOT
权限用户,也没有办法操作,会放回403 Access Denied Exception.
解决的办法:有两个。
第一个: 就是将前面提到的UserDetails
的接口函数getAuthorities()
的实现中,添加前缀,如上面提到的”ROLE_”+role.name()`
第二个: 就是不用@Secured()
注解,采用@PreAuthorize()
1 |
|
上面的修改,将会实现AccessDecissionManager
列表中AccessDecisionVoter
,多出一个voter
,即PreInvocationAuthorizationAdviceVoter
并且修改函数上的注解:
1 | @PreAuthorize("hasRole('ROOT')") //或则@PreAuthorize("hasAuthority('ROOT')") |
这样的话,就可以正常实现函数级别的权限控制了。
三 动态权限表达式
Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,此外还简单地使用了之前已经看到的配置属性和访问决定投票者。 基于表达式的访问控制基于相同的体系结构,但允许将复杂的布尔逻辑封装在单个表达式中。
Spring Security使用Spring EL来支持表达,如果您想更深入地理解该主题,则应该查看它的工作方式。 使用“根对象”评估表达式作为评估上下文的一部分。 Spring Security使用特定的类将Web和方法安全性用作根对象,以提供内置表达式和对诸如当前主体的值的访问。
3.1 常见的内置表达式
表达式根对象的基类是SecurityExpressionRoot
。 这提供了一些在Web和方法安全性中都可用的通用表达式。
Expression | Description |
---|---|
hasRole(String role) |
如果当前主体具有指定角色,则返回true。例如hasRole('admin') 默认情况下,如果提供的角色不是以“ ROLE_”开头,则会添加该角色。 可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。 |
hasAnyRole(String… roles) |
如果当前主体具有提供的任何角色(以逗号分隔的字符串列表形式),则返回true。例如hasAnyRole('admin', 'user') 默认情况下,如果提供的角色不是以“ ROLE_”开头,则会添加该角色。 可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。 |
hasAuthority(String authority) |
如果当前主体具有指定的权限,则返回true。例如, hasAuthority('read') |
hasAnyAuthority(String… authorities) |
如果当前委托人具有任何提供的授权(以逗号分隔的字符串列表形式),则返回true,例如 hasAnyAuthority('read', 'write') |
principal |
允许直接访问代表当前用户的主体对象 |
authentication |
允许直接访问从SecurityContext获得的当前Authentication对象 |
permitAll |
Always evaluates to true |
denyAll |
Always evaluates to false |
isAnonymous() |
如果当前主体是匿名用户,则返回true |
isRememberMe() |
如果当前主体是“记住我”用户,则返回true |
isAuthenticated() |
如果用户不是匿名的,则返回true |
isFullyAuthenticated() |
如果用户不是匿名用户或“记住我”用户,则返回true |
hasPermission(Object target, Object permission) |
如果用户可以访问给定权限的给定目标,则返回true, 例如hasPermission(domainObject, 'read') |
hasPermission(Object targetId, String targetType, Object permission) |
如果用户可以访问给定权限的给定目标,则返回true,例如 hasPermission(1, 'com.example.domain.Message', 'read') |
3.2 在Web安全表达式中引用Bean
如果您希望扩展可用的表达式,则可以轻松地引用您公开的任何Spring Bean。 例如,假设您有一个名称为webSecurity的Bean,其中包含以下方法签名:
1 | public class WebSecurity { |
在Java里配置
1 | http |
3.3 Web安全表达式中的路径变量
有时能够引用URL中的路径变量是很好的。 例如,考虑一个RESTful应用程序,该应用程序通过URL路径/ id / {userId}
的ID通过ID查找用户。
您可以通过将路径变量放在模式中来轻松引用它。 例如,如果您有一个名称为webSecurity
的Bean,其中包含以下方法签名:
1 | public class WebSecurity { |
需要在Java里配置
1 | http |
在这两种配置中,匹配的URL会将路径变量传递(并将其转换)为checkUserId
方法。 例如,如果URL是/ user / 123 / resource
,则传入的ID为123。
3.4 内置表达式
有一些特定于方法安全性的内置表达式,我们已经在上面使用过。 filterTarget
和returnValue
值很简单,但是使用hasPermission()
表达式需要仔细观察。
3.4.1 PermissionEvaluator 接口
hasPermission()
表达式委托给PermissionEvaluator
的实例。 它旨在在表达式系统和Spring Security的ACL系统之间架起桥梁,使您可以基于抽象权限在域对象上指定授权约束。 它对ACL模块没有明确的依赖关系,因此如果需要,您可以将其换成其他实现。 该接口有两种方法:
1 | boolean hasPermission(Authentication authentication, Object targetDomainObject, |
它直接映射到表达式的可用版本,但不提供第一个参数(Authentication
对象)。 第一种方法用于已经控制访问的域对象已经加载的情况。 如果当前用户对该对象具有给定的权限,则expression
将返回true。 第二种版本用于未加载对象但已知其标识符的情况。 还需要域对象的抽象“类型”说明符,以允许加载正确的ACL权限。 传统上,它是对象的Java类,但是不必与对象的权限加载方式一致。
要使用hasPermission()
表达式,必须在应用程序上下文中显式配置PermissionEvaluator
。 看起来像这样:
1 |
|
其中myPermissionEvaluator
是实现PermissionEvaluator
的bean。 通常,这将是来自ACL模块(称为AclPermissionEvaluator
)的实现。
3.4.2 方法安全性元注释
您可以使用元注释来确保方法的安全性,以使代码更具可读性。 如果发现在整个代码库中重复相同的复杂表达式,这将特别方便。 例如,
1 |
|
除了在所有地方重复此操作之外,我们还可以创建一个可以使用的元注释。
1 | @Retention(RetentionPolicy.RUNTIME) |
元注释可以用于任何Spring Security方法安全注释。 为了保持符合规范,JSR-250注释不支持元注释。