Spring PreAuthorize注解调试及save方法权限校验异常排查
我基于Spring官方教程的Spring Boot应用,已升级至最新Spring版本。在EmployeeRepository中,@PreAuthorize注解在delete方法上能正常生效(无权限请求返回403),但在save方法上完全不生效——即使将delete的注解表达式原封不动复制到save上,无权限请求仍能执行save操作。
相关代码片段:
@Override @PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name")// 不生效 Employee save(@Param("employee") Employee employee); @Override @PreAuthorize("#employee?.manager?.name == authentication?.name") // 生效 void delete(@Param("employee") Employee employee);
日志显示,delete方法会输出授权失败的详情,而save方法直接显示授权通过,两者测试时使用的是同一个无权限对象。
1. 开启SpEL调试日志
将org.springframework.expression包的日志级别设为DEBUG,能看到表达式解析、变量取值和计算的完整过程,直接定位#employee?.manager?.name和authentication?.name的实际值。
在application.yml中配置:
logging: level: org.springframework.expression: DEBUG
2. 自定义表达式上下文调试
继承DefaultMethodSecurityExpressionHandler,重写createEvaluationContext方法,打印或断点查看授权上下文里的变量:
@Component public class DebugMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { @Override public EvaluationContext createEvaluationContext(Authentication authentication, MethodInvocation invocation) { EvaluationContext context = super.createEvaluationContext(authentication, invocation); // 打印当前认证用户信息 System.out.println("Authentication name: " + authentication.getName()); // 打印方法参数(这里假设第一个参数是Employee) Object employee = invocation.getArguments()[0]; if (employee instanceof Employee emp) { System.out.println("Employee manager name: " + Optional.ofNullable(emp.getManager()).map(Employee::getName).orElse("null")); } return context; } }
然后在Security配置中指定使用这个处理器:
@Configuration @EnableMethodSecurity public class SecurityConfig { @Autowired private DebugMethodSecurityExpressionHandler expressionHandler; @Bean public MethodSecurityExpressionHandler methodSecurityExpressionHandler() { return expressionHandler; } }
3. 断点精准调试
在org.springframework.security.access.prepost.PreAuthorizationAdviceVoter.vote()方法设断点,能看到授权决策的完整过程;或者在SpelExpression.getValue(EvaluationContext context)方法断点,查看表达式计算时的变量值。
4. 验证当前认证用户
在save方法的实现(或通过AOP)中直接打印当前认证信息,确认请求对应的用户是否符合预期:
// 临时添加到save方法中 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); System.out.println("Current auth user: " + auth.getName());
1. SpEL参数绑定失效
升级Spring版本后,方法参数名的默认解析规则可能变化,导致@Param("employee")没有正确将参数映射到SpEL的#employee变量。此时#employee为null,表达式#employee?.manager == null结果为true,直接通过授权。
验证方案:修改save的注解表达式为:
@PreAuthorize("#employee != null and (#employee?.manager == null or #employee?.manager?.name == authentication?.name)")
如果修改后无权限请求被拦截,说明确实是参数绑定问题。此时可以尝试:
- 确保编译时保留参数名(Java编译添加
-parameters参数) - 去掉
@Param,直接用参数名(需确保Spring能正确解析参数名)
2. Spring Data REST的方法处理差异
Spring Data REST对save(对应POST/PUT端点)和delete(对应DELETE端点)的请求处理路径不同,可能存在拦截顺序或代理逻辑的差异:
- 检查SecurityFilterChain配置,确认POST/PUT请求是否被纳入方法级权限校验的范围
- 查看Spring Data REST的日志(
org.springframework.data.rest设为DEBUG),确认save方法是否被代理类正确调用
3. 方法重载导致注解未生效
如果EmployeeRepository继承的CrudRepository或JpaRepository中有多个save重载方法,可能你重写的带注解的save方法并没有被实际调用,而是调用了父类的无注解方法。
验证方案:在重写的save方法中添加日志,确认请求是否进入该方法。
4. Spring Security配置变更
升级到最新Spring版本后,@EnableMethodSecurity的默认配置可能变化(比如旧版本的prePostEnabled默认值不同)。确保配置类上添加了@EnableMethodSecurity(替代旧的@EnableGlobalMethodSecurity),且开启了Pre/Post注解支持。
内容的提问来源于stack exchange,提问作者Zlelik

