PreAuthorize
一句话定义:方法执行前权限校验注解,通过 Spring SpEL 表达式在目标方法调用前评估权限条件,条件不满足时立即抛出
AccessDeniedException,阻止方法执行。
定义与作用
@PreAuthorize 是 Spring Security 方法级别安全中最常用、最灵活的注解。它在方法调用前拦截,使用 Spring Expression Language(SpEL)表达式评估权限,支持引用方法参数、当前认证信息、Spring Bean 方法等。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
String value(); // SpEL 表达式
}
核心表达式变量:
| 变量 | 类型 | 含义 |
|---|---|---|
authentication | Authentication | 当前安全上下文 |
principal | Object | authentication.getPrincipal(),通常是 UserDetails |
#paramName | 方法参数 | 通过参数名引用方法入参 |
@beanName | Spring Bean | 调用 Spring 容器中的 Bean 方法 |
returnObject | 返回值 | ❌ 在 @PreAuthorize 中不可用(方法未执行) |
核心原理
SpEL 表达式评估流程
内置 SpEL 方法
// Spring Security 在 SpEL 中提供的方法(SecurityExpressionRoot)
hasRole(String role) // 是否有 ROLE_xxx
hasAnyRole(String... roles) // 是否有任一角色
hasAuthority(String authority) // 是否有指定权限
hasAnyAuthority(String... authorities) // 是否有任一权限
isAuthenticated() // 是否已认证
isFullyAuthenticated() // 是否完整认证(非 RememberMe)
isAnonymous() // 是否匿名
isRememberMe() // 是否 RememberMe 登录
permitAll() // 总是 true
denyAll() // 总是 false
principal // 当前主体
authentication // 当前认证对象
示例一:基于角色和参数所有权的权限控制
场景说明
用户管理系统,要求:
ADMIN可以修改任何用户- 普通用户只能修改自己的信息
操作前配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN")
.and()
.withUser("alice").password("{noop}alice").roles("USER")
.and()
.withUser("bob").password("{noop}bob").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(auth -> auth.anyRequest().authenticated())
.formLogin(withDefaults());
}
}
@Service
public class UserService {
// 场景:修改用户信息
@PreAuthorize("hasRole('ADMIN') or #userDto.username == authentication.name")
public void updateUser(UserDto userDto) {
userRepository.save(userDto);
}
}
public class UserDto {
private String username;
private String email;
// getters/setters
}
操作后配置(更复杂的 SpEL 表达式)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 修改用户:ADMIN 可以修改任何人;用户只能修改自己
@PreAuthorize("hasRole('ADMIN') or #userDto.username == authentication.name")
public void updateUser(UserDto userDto) {
userRepository.save(userDto);
}
// 删除用户:只有 ADMIN 可以删除,且不能删除自己
@PreAuthorize("hasRole('ADMIN') and #username != authentication.name")
public void deleteUser(String username) {
userRepository.deleteByUsername(username);
}
// 修改密码:必须是自己,且新密码不能和旧密码相同
@PreAuthorize("#username == authentication.name and #newPassword != #oldPassword")
public void changePassword(String username, String oldPassword, String newPassword) {
// 修改密码逻辑
}
}
结果分析
| 操作 | 调用用户 | 参数 | 结果 | 表达式评估 |
|---|---|---|---|---|
updateUser(aliceDto) | admin | username="alice" | ✅ | hasRole('ADMIN') = true |
updateUser(bobDto) | alice | username="bob" | ❌ 403 | hasRole('ADMIN')=false, "bob"=="alice"=false |
updateUser(aliceDto) | alice | username="alice" | ✅ | "alice"=="alice"=true |
deleteUser("admin") | admin | username="admin" | ❌ 403 | hasRole('ADMIN')=true, "admin"!="admin"=false |
deleteUser("alice") | admin | username="alice" | ✅ | hasRole('ADMIN')=true, "alice"!="admin"=true |
changePassword("alice", "123", "123") | alice | 新=旧 | ❌ 403 | "alice"=="alice"=true, "123"!="123"=false |
changePassword("alice", "123", "456") | alice | 新≠旧 | ✅ | 全部满足 |
示例二:引用 Spring Bean 方法进行权限判断
场景说明
权限判断逻辑复杂,需要查询数据库或调用外部服务。将权限判断逻辑封装到 Spring Bean 中,在 @PreAuthorize 中通过 @beanName.method() 调用。
操作前配置
@Component("permissionService")
public class PermissionService {
@Autowired
private UserDepartmentRepository deptRepository;
// 检查用户是否属于指定部门
public boolean belongsToDepartment(String username, String deptCode) {
return deptRepository.existsByUsernameAndDeptCode(username, deptCode);
}
// 检查用户是否有文档的审批权限
public boolean canApproveDocument(String username, Long documentId) {
Document doc = documentRepository.findById(documentId).orElseThrow();
return doc.getApprover().equals(username) ||
deptRepository.isManagerOf(username, doc.getDeptCode());
}
}
@Service
public class DocumentService {
// 引用 Bean 方法:只有所属部门成员才能创建
@PreAuthorize("@permissionService.belongsToDepartment(authentication.name, #document.deptCode)")
public Document createDocument(Document document) {
return documentRepository.save(document);
}
// 引用 Bean 方法:审批权限判断
@PreAuthorize("@permissionService.canApproveDocument(authentication.name, #documentId)")
public void approveDocument(Long documentId) {
Document doc = documentRepository.findById(documentId).orElseThrow();
doc.setStatus("APPROVED");
documentRepository.save(doc);
}
// 组合表达式:ADMIN 或审批人
@PreAuthorize("hasRole('ADMIN') or @permissionService.canApproveDocument(authentication.name, #documentId)")
public void rejectDocument(Long documentId) {
Document doc = documentRepository.findById(documentId).orElseThrow();
doc.setStatus("REJECTED");
documentRepository.save(doc);
}
}
操作后配置
@Component("permissionService")
public class PermissionService {
// 缓存权限结果,避免重复查询
@Cacheable(value = "userDept", key = "#username + '-' + #deptCode")
public boolean belongsToDepartment(String username, String deptCode) {
return deptRepository.existsByUsernameAndDeptCode(username, deptCode);
}
}
结果分析
alice(属于D100部门)创建deptCode="D100"的文档:@permissionService.belongsToDepartment("alice", "D100")→true- 方法执行
alice(属于D100)创建deptCode="D200"的文档:@permissionService.belongsToDepartment("alice", "D200")→false- 抛出
AccessDeniedException
admin(无部门归属)调用rejectDocument:hasRole('ADMIN')→true,短路求值,直接放行- 不会执行
canApproveDocument(SpELor的短路特性)
易错场景:SpEL 表达式参数名编译后丢失
场景:使用 #paramName 引用参数时,运行时抛出 SpelEvaluationException: Variable paramName is not defined。
原因:Java 编译后默认不保留方法参数名。Spring Security 的 SpEL 解析器无法找到参数名对应的变量。
错误代码:
@Service
public class OrderService {
@PreAuthorize("#orderId == 1") // 如果编译未保留参数名,可能报错
public Order getOrder(Long orderId) { ... }
}
解决方案:
// 方案一:编译时保留参数名(推荐)
// Maven 配置:
// <compilerArgs><arg>-parameters</arg></compilerArgs>
// 或 Gradle:compileJava { options.compilerArgs += ['-parameters'] }
// 方案二:使用 @P 或 @Param 注解显式命名
@Service
public class OrderService {
@PreAuthorize("#id == 1")
public Order getOrder(@P("id") Long orderId) { ... }
}
// 方案三:使用 args 数组引用(不需要参数名)
@Service
public class OrderService {
@PreAuthorize("#args[0] == 1")
public Order getOrder(Long orderId) { ... }
}
面试考点:
问:
@PreAuthorize("#userId == authentication.principal.id")运行时抛出变量未定义异常,为什么? 答:Java 编译后默认擦除方法参数名(除非使用-parameters编译选项),Spring Security 的 SpEL 解析器无法将#userId映射到实际参数。解决方案有三种:① 编译时添加-parameters选项保留参数名;② 使用 Spring Security 的@P("userId")注解或 JSR-330 的@Param("userId")显式标注参数名;③ 使用#args[0]按索引引用参数。生产环境推荐方案一,通过 Maven/Gradle 配置全局开启参数名保留。
@PreAuthorize 表达式速查表
| 场景 | 表达式 | 说明 |
|---|---|---|
| 角色检查 | hasRole('ADMIN') | 检查 ROLE_ADMIN |
| 多角色任选 | hasAnyRole('ADMIN', 'MANAGER') | 任一角色满足 |
| 权限检查 | hasAuthority('SCOPE_write') | 检查具体权限字符串 |
| 参数等于用户名 | #id == authentication.name | 校验数据所有权 |
| 参数属性判断 | #userDto.age >= 18 | 参数属性 SpEL 访问 |
| 调用 Bean 方法 | @authService.canAccess(#id) | 外部权限判断服务 |
| 组合条件 | hasRole('ADMIN') and #amount < 10000 | 角色 + 业务规则 |
| 否定条件 | !hasRole('GUEST') | 非访客 |
| 认证状态 | isFullyAuthenticated() | 排除 RememberMe |