本文基于《Spring实战(第4版)》所写。
通过使用Spring Security保护bean方法,能声明安全规则,保证如果用户没有执行方法的权限,就不会执行相应的方法。
使用注解保护方法
在Spring Security中实现方法级安全性的最常见办法是使用特定的注解,将这些注解应用到需要保护的方法上。
Spring Security提供了三种不同的安全注解:
- Spring Security自动的@Security注解;
- JSR-250的@RolesAllowed注解;
- 表达式驱动的注解,包括@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。
@Secured和@RolesAllowed方案非常类似,能够基于用户所授予的权限限制对方法的访问。当我们需要在方法上定义更灵活的安全规则时,Spring Security提供了@PreAuthorize和@PostAuthorize,而@PreFilter/@PostFilter能够过滤方法返回的以及传入方法的集合。
使用@Secured注解限制方法调用
在Spring中,如果要启用基于注解的方法安全性,关键之处在于要在配置类上使用@EnableGlobalMethodSecurity,如下所示:
package spittr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{
}
除了使用@EnableGlobalMethodSecurity注解,我们可能也注意配置类扩展了GlobalMethodSecurityConfiguration。在之前的篇章中,Web安全的配置类扩展了WebSecurityConfigurerAdapter,与之类似,这个类能够为方法级别的安全性提供更精细的配置。
例如,如果我们在Web层的安全配置中设置认证,那么可以通过重载GlobalMethodSecurityConfiguration的configure()方法实现该功能:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
让我们回到@EnableGlobalMethodSecurity注解,注意它的securedEnabled属性设置成了true。如果securedEnabled属性的值为true的话,将会创建一个切点,这样的话Spring Security切面就会包装带有@Secured注解的方法。例如,考虑如下这个带有@Secured注解的addSpittle() 方法:
@Secured("ROLE_SPITTER")
public void addSpittle(Spittle spittle) {
// ...
}
@Secured注解会使用一个String数组作为参数。每个String值是一个权限,调用这个方法至少需要具备其中的一个权限。通过传递进来ROLE_SPITTER,我们告诉Spring Security只允许具有ROLE_SPITTER权限的认证用户才能调用addSpittle() 方法。
如果传递给@Secured多个权限值,认证用户必须至少具备其中的一个才能进行方法的调用。例如,下面使用@Secured的方式表明用户必须具备ROLE_SPITTER或ROLE_ADMIN权限才能触发这个方法:
@Secured({"ROLE_SPITTER", "ROLE_ADMIN"})
public void addSpittle(Spittle spittle) {
// ...
}
如果方法被没有认证的用户或没有所需权限的用户调用,保护这个方法的切面将抛出一个Spring Security异常(可能是AuthenticationException或AccessDeniedException的子类)。它们是非检查型异常,但这个异常最终必须要被捕获和处理。如果被保护的方法是在Web请求,这个异常会被Spring Security的过滤器自动处理。否则的话,就需要代码来处理这个异常。
在Spring Security中使用JSR-250的@RolesAllowed注解
@RolesAllowed注解和@Secured注解在各个方面基本上都是一致的。唯一显著的区别在于@RolesAllowed是JSR-250定义的Java标准注解。
如果选择使用@RolesAllowed的话,需要将@EnableGlobalMethodSecurity的jsr250Enabled属性设置为true,以开启此功能:
@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{
}
尽管这里只启用了jsr250Enabled,但需要说明的一点是这与securedEnabled并不冲突。这两种注解风格可以同时启用。
在将jsr250Enabled设置为true之后,将会启用一个切点,这样带有@RolesAllowed注解的方法都会被Spring Security的切面包装起来。因此,在方法上使用@RolesAllowed的方式与使用@Secured类似。例如,如下的addSpittle() 方法使用了@RolesAllowed注解来代替@Secured:
@RolesAllowed("ROLE_SPITTER")
public void addSpittle(Spittle spittle) {
// ...
}
尽管@RolesAllowed比@Secured有些优势,它是实现方法安全的标准注解,但是这两个注解有一个共同的不足。它们只能根据用户有没有授予特定的权限来限制方法的调用。在判断方式是否执行方面,无法使用其他的因素。在之前的篇章中曾经看到过,在保护URL方面,能够使用SpEL表达式克服这一限制。接下来,我们看一下如何组合使用SpEL与Spring Security所提供的方法调用前后注解,实现基于表达式的方法安全性。
使用表达式实现方法级别的安全性
Spring Security 3.0 引入了几个新注解,它们使用SpEL能够在方法调用上实现更有意思的安全性约束。这些注解的值参数中都可以接受一个SpEL表达式。表达式可以是任意合法的SpEL表达式,可能会包含之前篇章所列的Spring Security 对 SpEL的扩展。如果表达式的计算结果为true,那么安全规则通过,否则就会失败。安全规则通过或失败的结果会因为所使用注解的差异而有所不同。下表描述了这些新的注解。
注解 | 描述 |
---|---|
@PreAuthorize | 在方法调用之前,基于表达式的计算结果来限制对方法的访问 |
@PostAuthorize | 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常 |
@PostFilter | 允许方法调用,但必须按照表达式来过滤方法的结果 |
@PreFilter | 允许方法调用,但必须在进入方法之前过滤输入值 |
首先,我们需要将@EnableGlobalMethodSecurity注解的prePostEnabled属性设置为true,从而启用它们:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{
}
表述方法访问规则
Spring Security提供了两个注解,@PreAuthorize和@PostAuthorize,他们能够基于表达式的计算结果来限制方法的访问。在定义安全限制方面,表达式带来了极大的灵活性。
@PreAuthorize和@PostAuthorize之间的关键区别在与表达式执行的时机。@PreAuthorize的表达式会在方法调用之前执行,如果表达式的计算结果不为true的话,将会阻止方法执行。与之相反,@PostAuthorize的表达式直到方法返回才会执行,然后决定是否抛出安全性的异常。
在方法调用前验证权限
@PreAuthorize不只是添加了SpEL支持的@Secured和@RolesAllowed,还可以基于用户所授予的角色,使用@PreAuthorize来限制访问:
@PreAuthorize("hasRole('ROLE_SPITTER')")
public void addSpittle(Spittle spittle){
...
}
@PreAuthorize的功能并不限于这个简单的例子所展现的。@PreAuthorize的String类型参数是一个SpEL表达式。借助于SpEL表达式来实现访问决策,我们能够编写出更高级的安全性约束。例如,Spittr 应用程序的一般用户只能写140个字以内的Spittle,而付费用户不限制字数。
@PreAuthorize(
"(hasRole('ROLE_SPITTER') and #spittle.text.length() <= 140)"
+ " or hasRole('ROLE_PREMIUM')")
public void addSpittle(Spittle spittle){
...
}
表达式中的#spittle部分直接引用了方法中的同名参数。这使得Spring Security能够检查传入方法的参数,并将这些参数用于认证决策的制定。
在方法调用之后验证权限
在方法调用之后验证权限并不是比较常见的方法。事后验证一般需要基于安全保护方法的返回值来进行安全性决策。这种情况意味着方法必须被调用执行并且得到返回值。
例如,假设我们想对getSpittleById()方法进行保护,确保返回的Spittle对象属于当前的认证用户。我们只有得到Spittle对象之后,才能判断它是否属性当前用户。因此,getSpittleById()方法必须要执行。在得到Spittle之后,如果它不属于当前用户的话,将会抛出安全性异常。
@PostAuthorize("returnObject.spitter.username == principal.username")
public Spittle getSpittleById(long id){
...
}
为了便利地访问受保护方法的返回对象,Spring Security在SpEL中提供了名为returnObject的变量。在这里,我们知道返回对象是一个Spittle对象,所以这个表达式可以直接访问其spittle属性中的username属性。
在对比表达式双等号的另一侧,表达式到内置的principal对象中取出其username属性。principal是另一个Spring Security内置的特殊名称,它代表了当前认证用户的主要信息(通常是用户名)。
在Spittle对象所包含Spitter中,如果username属性与principal的username属性相同,这个Spittle将返回给调用。否则,会抛出一个AccessDeniedException异常,而调用者也不会得到Spittle对象。
过滤方法的输入和输出
有时候限制方法调用太严格了。有时,需要保护的并不是对方法的调用,需要保护的是传入方法的数据和方法返回的数据。
例如,我们有一个名为getOffensiveSpittles() 的方法,这个方法会返回标记为具有攻击性的Spittle列表。这个方法主要会给管理员使用,以保证Spittr应用中内容的和谐。但是,普通用户也可以使用这个方法,用来查看他们所发布的Spittle有没有被标记为具有攻击性。这个方法的签名大致如下所示:
public List<Spittle> getOffensiveSpittles() { ... }
这个方法与具体的用户并没有关联。它只会返回攻击性Spittle的一个列表,并不关心它们属于哪个用户。对于管理员使用来说,这是一个很好的方法,但是它无法限制列表中Spittle都属于当前用户。
我们需要一种方式过滤这个方法返回的Spittle集合,将结果限制为允许当前用户看到的内容,而这就是Spring Security的@PostFilter所能做的事情。
事后对方法的返回值进行过滤
@PostFilter也使用了一个SpEL作为值参数。但是,这个表达式不是用来限制方法访问的,@PostFilter会使用这个表达式计算该方法所返回集合的每个成员,将计算结果为false的成员移除掉。
为了阐述该功能,我们将@PostFilter应用在getOffensiveSpittles()方法上:
@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PostFilter("hasRole('ROLE_ADMIN') || "
+ "filterObject.spitter.username == principal.name" )
public List<Spittle> getOffensiveSpittles(){
...
}
在这里,@PreAuthorize限制只有具备ROLE_SPITTER或ROLE_ADMIN权限的用户才能访问该方法。然后,@PostFilter注解将会过滤这个列表,确保用户只能看到允许的Spittle。具体来讲,管理员能够看到所有攻击性的Spittle,非管理员只能看到属于自己的Spittle。
表达式中 filterObject对象应用的是这个方法所返回的List中的某一个元素。在这个Spittle对象中,如果Spitter的用户名于认证用户相同或者用户具有ROLE_ADMIN角色,那这个元素将会最终包含在过滤的列表中。否则,它将被过滤掉。
事先对方法的参数进行过滤
除了事后过滤方法的返回值,我们可以预先过滤传入到方法中值。
例如,假设我们需要以批处理的方式删除Spittle组成的列表。为了完成该功能,我们可能会编写一个方法,其签名大致如下所示:
public void deleteSpittles(List<spittle> spittles) { ... }
如果我们想在它上面应用一些安全规则的话,比如Spittle只能由其所有者或管理员删除。如果将逻辑放在方法中,意味着我们需要将安全逻辑直接嵌入到方法之中。相对于删除Spittle来讲,安全逻辑是独立的关注点。如果列表中只包含实际要删除Spittle,这样会帮助方法中的逻辑更加简单。
Spring Security的@PreFilter注解能够很好地解决这个问题。@PreFilter也使用SpEL来过滤集合,只有满足SpEL表达式的元素才会留在集合中。但是它所过滤的不是方法的返回值,而是要进入方法中的集合成员。
@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PreFilter("hasRole('ROLE_ADMIN') || "
+ "targetObject.spitter.username == principal.name" )
public void deleteSpittles(List<spittle> spittles){
...
}
与前面一样,对于没有ROLE_SPITTER或ROLE_ADMIN权限的用户,@PreAuthorize注解会阻止对这个方法的调用。但同时@PreFilter注解能够保证传递给deleteSpittles()方法的列表中,只包含当前用户有权限删除的Spittle。这个表达式会针对集合中每个元素进行计算,只有表达式计算结果为true的元素才会保留在列表中。targetObject是Spring Security提供的另外一个值,它代表了要进行计算的当前列表元素。
如果安全表达式很复杂且难以控制了,那么就应该看以下如何编写自定义的许可计算器(premission evaluator),以简化你的SpEL表达式。
定义许可计算器
我们能够将表达式替换为更加简单的版本,如下所示:
@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PreFilter("hasPermission(targetObject, 'delete')" )
public void deleteSpittles(List<spittle> spittles){
...
}
现在,设置给@PreFilter的表达式更加紧凑。它实际上只是在问一个问题“用户有权限删除目标对象吗?”。如果有的话,表达式的计算结果为true,Spittle会保存在列表中,并传递给deleteSpittles()方法。如果没有权限的话,它将会被移除掉。
hasPermission() 函数是Spring Security为SpEL提供的扩展。它为开发者提供了一个时机,能够在执行计算的事后插入任意的逻辑。我们所需要做的就是编写并注册一个自定义的许可计算器。下面程序展现了SpittlePermissionEvaluator类,它就是一个自定义的许可计算器,包含了表达式逻辑。
package spittr.web;
import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import spittr.model.Spittle;
import java.io.Serializable;
public class SpittlePermissonEvaluator implements PermissionEvaluator {
private static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("ROLE_ADMIN");
public boolean hasPermission(Authentication authentication, Object target, Object permission) {
if (target instanceof Spittle){
Spittle spittle = (Spittle) target;
String username = spittle.getSpitter().getUsername();
if ("delete".equals(permission)){
return isAdmin(authentication) ||
username.equals(authentication.getName());
}
}
throw new UnsupportedOperationException("hasPermission not supported for object <" + target
+ "> and permission <" + permission + ">");
}
public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
throw new UnsupportedOperationException();
}
public boolean isAdmin(Authentication authentication){
return authentication.getAuthorities().contains(ADMIN_AUTHORITY);
}
}
SpittlePermissonEvaluator实现了Spring Security的PermissionEvaluator接口,它需要实现两个不通的hasPermission()方法。其中的一个hasPermission()方法把要评估的对象作为第二个参数。第二个hasPermission()方法在只有目标对象的ID可以得到的时候才有用,并将ID作为Serializable传入第二个参数。
为了满足我们的需要,我们假设使用Spittle对象来评估权限,所以第二个方法只是简单地抛出异常。
对于第一个hasPermission()方法,要检查所评估的对象是否为一个Spittle,并判断所检查的是否为删除权限。如果是这样,它将对比Spitter的用户名是否与认证用户的名称相等,或者当前用户是否具有ROLE_ADMIN权限。
许可计算器已经准备就绪,接下来需要将其注册到Spring Security中,以便在@PreFilte表达式的时候支持hasPermission()操作。为了实现该功能,我们需要替换原有的表达式处理器,换成使用自定义许可计算器的处理器。
默认情况下,Spring Security会配置为使用DefaultMethodSecurityExpressionHandler,它会使用一个DenyAllPermissionEvaluator实例。顾名思义,DenyAllPermissionEvaluator将会在hasPermission()方法中始终返回false,拒绝所有的方法访问。但是,我们可以为Spring Security提供另一个DefaultMethodSecurityExpressionHandler,让它使用我们自定义的SpitterPermissionEvaluator,这需要重载GlobalMethodSecurityConfiguration的createExpressionHandler方法:
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new SpittlePermissonEvaluator());
return expressionHandler;
}
现在,不管在任何地方的表达式中使用hasPermission()来保护方法,都会调用SpittlePermissonEvaluator来决定用户是否有权限调用方法。