实现方法级别的安全
尽管在 Web 请求层面考虑安全问题很容易,但这一层面不一定是进行安全限制的最佳场所。有时候,最好在执行受保护的操作时再去校验一下用户是否通过了验证并被授予了足够的权限。
例如,假设出于管理的需要,我们有一个服务类,其中包括一个从数据库中清理所有订单的方法。该方法会使用注入的 OrderRepository,大致如下所示:
public void deleteAllOrders() {
orderRepository.deleteAll();
}
现在,我们有一个控制器,它在处理一个 POST 请求时,将会调用这个 deleteAllOrders() 方法:
@Controller
@RequestMapping("/admin")
public class AdminController {
private OrderAdminService adminService;
public AdminController(OrderAdminService adminService) {
this.adminService = adminService;
}
@PostMapping("/deleteOrders")
public String deleteAllOrders() {
adminService.deleteAllOrders();
return "redirect:/admin";
}
}
我们可以很容易地调整 SecurityConfig,确保只有经过认证的用户才能执行这个 POST 请求,如下所示:
.authorizeRequests()
...
.antMatchers(HttpMethod.POST, "/admin/**")
.access("hasRole('ADMIN')")
....
这样能够很好地防止未经授权的用户发送 POST 请求至 “/admin/deleteOrders”,从而避免所有的订单从数据库中消失。
但是,假设其他的控制器也调用了 deleteAllOrders(),那么我们就需要添加更多的匹配器来保护其他控制器的请求。
作为替代方案,我们可以直接在 deleteAllOrders() 方法上启用安全防护:
@PreAuthorize("hasRole('ADMIN')")
public void deleteAllOrders() {
orderRepository.deleteAll();
}
@PreAuthorize 注解会接受一个 SpEL 表达式,如果表达式的计算结果为 false,这个方法将不会被调用;如果表达式的计算结果为 true,方法就允许调用。在本例中,@PreAuthorize 会检查用户是否具有 ROLE_ADMIN 的权限:如果具有,方法将会被调用,所有的订单会被删除;否则,它会将调用中止。
如果 @PreAuthorize 阻止调用,那么 Spring Security 将会抛出 AccessDeniedException。这是一个非检查型异常,所以我们不需要捕获它,除非想要在异常处理中添加一些自定义的行为。如果我们不捕获它,它将会往上传递,最终被 Spring Security 的过滤器捕获并进行相应的处理——要么返回 HTTP 403 页面,要么在用户没有认证的情况下重定向到登录页面。
要使 @PreAuthorize 发挥作用,需要启用全局的方法安全功能。为了实现这一点,需要使用 @EnableGlobalMethodSecurity 注解标注安全配置类,如下所示:
@Configuration
@EnableGlobalMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
对于大多数方法级别的安全需求,@PreAuthorize 都是一个非常有用的注解。但是,我们也需要知道,还有一个与之对应的 @PostAuthorize 注解,它用在方法调用之后,通常来讲用处不是特别大。@PostAuthorize 注解的运行机制和 @PreAuthorize 注解基本相同,只不过它的表达式是在目标方法调用完成并返回之后执行的。这样一来,在决定是否允许方法调用的时候,我们就能让表达式使用方法的返回值了。
假设我们有一个能够根据 ID 来获取订单的方法。我们如果想限制这个方法,使其只能被管理员或订单所属的用户使用,就可以像这样使用 @PostAuthorize 注解:
@PostAuthorize("hasRole('ADMIN') || " +
"returnObject.user.username == authentication.name")
public TacoOrder getOrder(long id) {
...
}
在本例中,returnObject 就是该方法返回的 TacoOrder。如果其 user 属性中的 username 与 authentication 中的 name 属性一致,那么方法调用就是允许的。但是,要知道两者是否一致,需要先执行这个方法,来获得要进行对比的 TacoOrder 对象。
但是,稍等一下!如果判定安全的条件依赖于方法调用的返回值,那么该如何保证方法不被调用呢?这又是一个 “先有鸡还是先有蛋” 的问题,我们可以先允许方法调用,并在表达式返回值为 false 时抛出一个 AccessDeniedException,从而解决这个难题。