前面Spring Security是保护应用的Web层,限制URL级别访问,此处是方法级别的保护;
一、使用注解保护方法
Spring Security提供了三种不同的安全注解:
- Spring Security自带的@Secured注解:能够基于用户所授予的权限限制对方法的访问
- JSR-250的@RolesAllowed注解:能够基于用户所授予的权限限制对方法的访问
- 表达式驱动的注解,包括@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter:@PreAuthorize和@PostAuthorize可以在方法上定义更灵活的安全规则;@PreFilter和@PostFilter能够过滤方法返回的以及传入方法的集合;
1、使用@Secured注解限制方法调用
// GlobalMethodSecurityConfiguration:能够为方法级别的安全性提供更精细的配置
// 和第九章中WebSecurityConfigurerAdapter类似
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true) // 启用基于注解的方法安全性
// 如果securedEnabled属性的值为true的话,将会创建一个切点,
// 这样的话Spring Security切面就会包装带有@Secured注解的方法。
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{
// 在Web层的安全配置中设置认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
// @Secured注解会使用一个String数组作为参数。每个String值是一个权限,调用这个方法至少需要具备其中的一个权限。
// 只允许具有ROLE_SPITTER权限的认证用户才能调用addSpittle() 方法
@Secured("ROLE_SPITTER")
public void addSpittle(Spittle spittle) {
// ...
}
// 必须具备ROLE_SPITTER或ROLE_ADMIN权限才能触发这个方法
@Secured({"ROLE_SPITTER", "ROLE_ADMIN"})
public void addSpittle(Spittle spittle) {
// ...
}
如果方法被没有认证的用户或没有所需权限的用户调用,保护这个方法的切面将抛出一个Spring Security异常(可能是AuthenticationException或AccessDeniedException的子类)。它们是非检查型异常,但这个异常最终必须要被捕获和处理。如果被保护的方法是在Web请求,这个异常会被Spring Security的过滤器自动处理。否则的话,就需要代码来处理这个异常。
2、在Spring Security中使用JSR-250的@RolesAllowed注解
@RolesAllowed注解和@Secured注解区别:@RolesAllowed是JSR-250定义的Java标准注解;
两者共同不足:只能根据用户有没有授予特定的权限来限制方法的调用。在判断方式是否执行方面,无法使用其他的因素。
@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled = true) // 使用@RolesAllowed,此处需为true
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{
}
二、使用表达式实现方法级别的安全性
SqEL能够在方法调用上实现更加灵活的安全性约束,如果表达式的计算结果为true,那么安全规则通过,否则就会失败。下表描述了这些新的注解:
注解 | 描述 |
---|---|
@PreAuthorize | 在方法调用之前,基于表达式的计算结果来限制对方法的访问 |
@PostAuthorize | 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常 |
@PostFilter | 允许方法调用,但必须按照表达式来过滤方法的结果 |
@PreFilter | 允许方法调用,但必须在进入方法之前过滤输入值 |
1、启用注解
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法前后注解
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{
}
2、表述方法访问规则
@PreAuthorize和@PostAuthorize:
优点:基于表达式的计算结果来限制方法的访问,更加灵活;
区别:表达式执行的时机
- @PreAuthorize的表达式会在方法调用之前执行,如果表达式的计算结果为false,会阻止方法执行。
- @PostAuthorize的表达式直到方法返回才会执行,然后决定是否抛出安全性的异常。
(1)在方法调用前验证权限
// 同@RolesAllowed和@Secured
@PreAuthorize("hasRole('ROLE_SPITTER')")
public void addSpittle(Spittle spittle){
...
}
// #spittle直接引用了方法中的同名参数
@PreAuthorize(
"(hasRole('ROLE_SPITTER') and #spittle.text.length() <= 140)"
+ " or hasRole('ROLE_PREMIUM')")
public void addSpittle(Spittle spittle){
...
}
(2)在方法调用之后验证权限-需要方法有返回值;如果注解判断结果不符合预期,将抛出异常
// returnObject是SqEl提供的变量;principal也是另一个内置特殊名称,代表当前认证用户的主要信息
// 此处如果不匹配,将会排抛出AccessDeniedException
@PostAuthorize("returnObject.spitter.username == principal.username")
public Spittle getSpittleById(long id){
...
}
3、过滤方法的输入和输出
@PreAuthorize和@PostAuthorize限制方法调用,但有时需要限制传入方法和方法返回的数据;
(1)事后对方法的返回值进行过滤
@PostFilter会使用SqEl表达式计算该方法所返回集合的每个成员,将计算结果为false的成员移除掉。
/*
@PreAuthorize限制只有具备ROLE_SPITTER或ROLE_ADMIN权限的用户才能访问该方法;
@PostFilter会对返回的List进行过滤,确保用户只能看到允许的Spittle;
filterObject引用的是方法返回List中的某一个元素;
在这个Spittle对象中,如果Spitter的用户名于认证用户相同或者用户具有ROLE_ADMIN角色,
那这个元素将会最终包含在过滤的列表中。否则,它将被过滤掉。
*/
@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PostFilter("hasRole('ROLE_ADMIN') || filterObject.spitter.username == principal.name" )
public List<Spittle> getOffensiveSpittles(){
...
}
(2)事先对方法的参数进行过滤-不常用
如以下以批处理的方式删除Spittle组成的列表(只能由其所有者或管理员删除):
/* 尽管可以在方法内部过滤删除部分spittle,但这样会将安全逻辑直接嵌入到方法中;
相对于删除Spittle,安全逻辑是独立的关注点;如果列表中能够只包含实际要删除的Spittle会更好
@PreFilter可以过滤要进入方法中集合成员
targetObject表示要进行计算的当前列表元素
*/
@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PreFilter("hasRole('ROLE_ADMIN') || targetObject.spitter.username == principal.name" )
public void deleteSpittles(List<spittle> spittles){
...
}
(3)定义许可计算器-针对表达式比较复杂的场景
@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PreFilter("hasPermission(targetObject, 'delete')" )
public void deleteSpittles(List<spittle> spittles){
...
}
hasPermission() 函数是Spring Security为SpEL提供的扩展。它为开发者提供了一个时机,能够在执行计算的事后插入任意的逻辑。我们所需要做的就是编写并注册一个自定义的许可计算器。
a、编写许可器:
// 假设使用Spittle对象来评估权限,所以第二个hasPermission方法只是简单地抛出异常
public class SpittlePermissonEvaluator implements PermissionEvaluator {
private static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("ROLE_ADMIN");
// Object target要评估的对象
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 + ">");
}
// 只有目标对象的ID可以得到时此方法才有用,并将ID作为Serializable传入第二个参数
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);
}
}
b、注册许可器:
默认情况下,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;
}