Spring Security 方法级安全控制——@PreAuthorize 的正确使用与扩展
Spring Security 方法级安全控制——@PreAuthorize 的正确使用与扩展
适读人群:使用Spring Security做权限控制的Java后端开发者 | 阅读时长:约16分钟 | 核心价值:掌握@PreAuthorize及其姐妹注解的完整用法,以及如何正确扩展SpEL表达式
被滥用的 hasRole,和我的觉醒
2021年,我参与一个内容管理系统的代码Review,看到了满屏幕的:
@PreAuthorize("hasRole('ADMIN')")
public void publishArticle(Long articleId) { ... }
@PreAuthorize("hasRole('ADMIN')")
public void deleteArticle(Long articleId) { ... }
@PreAuthorize("hasRole('ADMIN')")
public void manageUsers() { ... }所有权限都映射到角色,角色只有ADMIN和USER两种。结果就是:要么啥都能做,要么啥都不能做。
产品说想让"编辑"角色能发文章但不能删文章,想让"审核员"能审核但不能发布……这些需求全部实现不了,只能改代码。
那次之后,我花时间把Spring Security的方法安全这部分完整研究了一遍。今天把能用的都说清楚。
Spring Security 方法安全注解全家桶
Spring Security提供了三种方法安全注解体系:
1. Spring Security 原生注解(推荐)
@EnableMethodSecurity // Spring Security 6.x,替换了旧的 @EnableGlobalMethodSecurity@PreAuthorize:方法执行前鉴权,失败则抛异常(不执行方法)@PostAuthorize:方法执行后鉴权,可以用returnObject访问返回值@PreFilter:方法执行前,过滤集合类型的参数@PostFilter:方法执行后,过滤集合类型的返回值
2. JSR-250 注解
@EnableMethodSecurity(jsr250Enabled = true)@RolesAllowed({"ADMIN", "USER"}):等同于hasAnyRole@PermitAll:允许所有人@DenyAll:拒绝所有人
3. Spring 旧版注解(不推荐,仍支持)
@EnableMethodSecurity(securedEnabled = true)@Secured({"ROLE_ADMIN"}):简单角色检查,不支持SpEL
@PreAuthorize 的完整 SpEL 表达式
SpEL(Spring Expression Language)是@PreAuthorize的核心,内置了以下安全相关方法:
// ===== 角色和权限检查 =====
@PreAuthorize("hasRole('ADMIN')") // 有ROLE_ADMIN角色
@PreAuthorize("hasAuthority('user:create')") // 有user:create权限(不加ROLE_前缀)
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
@PreAuthorize("hasAnyAuthority('user:create', 'user:update')")
// ===== 认证状态 =====
@PreAuthorize("isAuthenticated()") // 已认证
@PreAuthorize("isAnonymous()") // 匿名用户
@PreAuthorize("isFullyAuthenticated()") // 完全认证(非记住我)
// ===== 访问当前认证信息 =====
@PreAuthorize("authentication.name == 'admin'") // 当前用户名
@PreAuthorize("principal.username == 'admin'") // principal对象
@PreAuthorize("authentication.authorities.![authority].contains('user:delete')")
// ===== 访问方法参数 =====
@PreAuthorize("authentication.name == #username") // #参数名 访问入参
@PreAuthorize("#user.id == authentication.principal.userId")
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.userId")
// ===== 逻辑组合 =====
@PreAuthorize("hasRole('ADMIN') and hasAuthority('user:create')")
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
@PreAuthorize("!hasRole('GUEST')") // 非访客
// ===== 自定义Bean =====
@PreAuthorize("@perm.has('user:create')") // 调用Bean的方法
@PreAuthorize("@perm.has('user:create') and @perm.canAccessTenant(#tenantId)")@PostAuthorize:基于返回值的权限控制
/**
* 查询用户详情:只有管理员或者本人才能看到完整信息
* 方法执行后,检查返回的用户是否是当前用户(或当前用户是管理员)
*/
@GetMapping("/{userId}")
@PostAuthorize("hasRole('ADMIN') or returnObject.userId == authentication.principal.userId")
public UserDetailVO getUserDetail(@PathVariable Long userId) {
// 先执行方法,再检查返回值
// 如果checkfail,方法已经执行了,但结果不会返回给调用方
return userService.getUserDetail(userId);
}注意: @PostAuthorize只拦截返回,不阻止方法执行。如果方法有副作用(写DB),不适合用PostAuthorize来保护。
@PreFilter 和 @PostFilter:集合过滤
/**
* 批量创建用户:过滤掉当前用户没有权限创建的类型的用户
* filterObject 是集合中每个元素的占位符
*/
@PreFilter("hasRole('ADMIN') or filterObject.role != 'ADMIN'")
public List<UserVO> batchCreateUsers(List<CreateUserDTO> users) {
// 非管理员只能创建普通用户,ADMIN类型的用户会被过滤掉
return userService.batchCreate(users);
}
/**
* 查询所有订单:只返回属于当前用户的订单
*/
@PostFilter("filterObject.userId == authentication.principal.userId")
public List<OrderVO> getAllOrders() {
// 方法可能返回所有订单,但@PostFilter会过滤掉不属于当前用户的
return orderService.findAll();
}完整代码:自定义安全表达式扩展
/**
* 自定义安全表达式根对象
* 扩展SpEL,添加项目特定的安全方法
*/
public class CustomMethodSecurityExpressionRoot
extends SecurityExpressionRoot
implements MethodSecurityExpressionOperations {
private Object filterObject;
private Object returnObject;
private Object target;
// 注入需要的Service
private final PermissionService permissionService;
private final DataScopeService dataScopeService;
public CustomMethodSecurityExpressionRoot(
Authentication authentication,
PermissionService permissionService,
DataScopeService dataScopeService) {
super(authentication);
this.permissionService = permissionService;
this.dataScopeService = dataScopeService;
}
/**
* 自定义方法:检查权限码
* 用法:@PreAuthorize("hasPermission('user:create')")
*/
public boolean hasPermission(String permCode) {
String username = getAuthentication().getName();
return permissionService.hasPermission(username, permCode);
}
/**
* 数据权限:只能访问自己部门的数据
* 用法:@PreAuthorize("inDept(#deptId)")
*/
public boolean inDept(Long deptId) {
Authentication auth = getAuthentication();
if (!(auth.getPrincipal() instanceof LoginUser user)) return false;
if (hasRole("ADMIN")) return true;
return dataScopeService.isUserInDept(user.getUserId(), deptId);
}
/**
* 自定义方法:检查是否是数据的创建者
* 用法:@PreAuthorize("isCreator(#articleId)")
*/
public boolean isCreator(Long articleId) {
Authentication auth = getAuthentication();
if (!(auth.getPrincipal() instanceof LoginUser user)) return false;
return dataScopeService.isCreator(user.getUserId(), articleId);
}
// 实现 MethodSecurityExpressionOperations 接口方法
@Override public void setFilterObject(Object filterObject) { this.filterObject = filterObject; }
@Override public Object getFilterObject() { return filterObject; }
@Override public void setReturnObject(Object returnObject) { this.returnObject = returnObject; }
@Override public Object getReturnObject() { return returnObject; }
@Override public Object getThis() { return target; }
public void setThis(Object target) { this.target = target; }
}
/**
* 注册自定义表达式根对象
*/
@Component
public class CustomMethodSecurityExpressionHandler
extends DefaultMethodSecurityExpressionHandler {
private final PermissionService permissionService;
private final DataScopeService dataScopeService;
public CustomMethodSecurityExpressionHandler(
PermissionService permissionService,
DataScopeService dataScopeService) {
this.permissionService = permissionService;
this.dataScopeService = dataScopeService;
}
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(
authentication, permissionService, dataScopeService
);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(getDefaultRolePrefix());
return root;
}
}
/**
* 注册到Spring Security
*/
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
@ConditionalOnMissingBean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
PermissionService permissionService,
DataScopeService dataScopeService) {
return new CustomMethodSecurityExpressionHandler(permissionService, dataScopeService);
}
}使用自定义表达式:
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
// 使用自定义的 hasPermission 方法
@PostMapping
@PreAuthorize("hasPermission('article:publish')")
public ArticleVO publishArticle(@RequestBody ArticleDTO dto) {
return articleService.publish(dto);
}
// 只有本部门的文章才能审核
@PutMapping("/{articleId}/review")
@PreAuthorize("hasPermission('article:review') and inDept(#deptId)")
public void reviewArticle(@PathVariable Long articleId, @RequestParam Long deptId) {
articleService.review(articleId);
}
// 只有文章作者或管理员才能删除
@DeleteMapping("/{articleId}")
@PreAuthorize("hasRole('ADMIN') or isCreator(#articleId)")
public void deleteArticle(@PathVariable Long articleId) {
articleService.delete(articleId);
}
}三个踩坑实录
坑一:参数名在编译后丢失,#参数名 取不到值
现象: @PreAuthorize("#userId == authentication.principal.userId"),#userId始终是null,权限检查失败。
原因: Java编译时默认不保留参数名,编译后参数名变成了arg0、arg1等。
解法: 两种方案:
- 编译时保留参数名:在pom.xml或build.gradle中配置
-parameters编译参数 - 使用
@P注解显式指定:
@PreAuthorize("#uid == authentication.principal.userId")
public UserVO getUserDetail(@P("uid") @PathVariable Long userId) { ... }Spring Boot 3.x默认配置了-parameters,一般不需要额外处理。
坑二:@PreAuthorize 在异步方法里拿不到 SecurityContext
现象: @Async方法上加了@PreAuthorize,检查总是失败,因为authentication是null。
原因: 异步方法在新线程里执行,新线程的SecurityContext是空的,没有认证信息。
解法: 配置Spring Security的SecurityContextHolder策略为MODE_INHERITABLETHREADLOCAL,让子线程继承父线程的SecurityContext:
@Bean
public MethodInvokingFactoryBean securityContextHolderStrategy() {
MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();
factoryBean.setTargetClass(SecurityContextHolder.class);
factoryBean.setTargetMethod("setStrategyName");
factoryBean.setArguments(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
return factoryBean;
}但在线程池场景(@Async用的是线程池,不是直接创建子线程),还需要用DelegatingSecurityContextExecutor包装线程池。
坑三:@PreAuthorize 抛出的异常被全局异常处理器吞掉
现象: 权限不足应该返回403,但实际返回了500,且没有友好的错误信息。
原因: @PreAuthorize失败会抛AccessDeniedException,如果全局@ExceptionHandler里只处理了Exception基类,AccessDeniedException的信息会被包装成500错误。
// 全局异常处理器中明确处理 AccessDeniedException
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ApiResult<Void> handleAccessDenied(AccessDeniedException e) {
// 注意:不要暴露具体的权限信息给前端,可能被利用做探测
return ApiResult.fail(403, "权限不足,请联系管理员");
}
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ApiResult<Void> handleAuthenticationException(AuthenticationException e) {
return ApiResult.fail(401, "请先登录");
}
}小结
Spring Security的方法级安全是细粒度权限控制的关键工具:
@PreAuthorize:绝大多数场景的首选,执行前检查@PostAuthorize:少数场景,基于返回值的访问控制@PreFilter/@PostFilter:集合数据的按条件过滤- 自定义SpEL表达式:通过
@Bean注入的方式扩展,实现任意复杂度的权限逻辑
核心原则:权限逻辑要细到操作级别(user:create而不只是ADMIN),配合自定义SpEL Bean(@perm.has('xxx')),既灵活又易读。
