Spring中@PreAuthorize无法识别自定义UserDetails的问题咨询
问题描述
在Spring Service中尝试通过@PreAuthorize注解实现方法安全,需求是校验JWT令牌中的id声明与请求体的id字段是否相等,但遇到了以下问题:
尝试方案1:直接使用SpEL表达式
编写代码如下:
@PreAuthorize("r.id == principal.getId()") public ResponseEntity<?> update(UpdateRequest r) { return ResponseEntity.ok().build(); }
问题:注解识别的是Spring内置的UserDetails,而非自定义的CustomUserDetails。
尝试方案2:自定义授权判断Bean
参考方案编写核心校验代码:
public static boolean mayUpdate(Object principal, Long id){ CustomUserDetails user = (CustomUserDetails) principal; return user.getId().equals(id); }
使用注解@PreAuthorize("@decider.mayUpdate(principal,r.id)")时,出现错误:
Failed to evaluate expression @decider.mayUpdate(principal,r.id)'
可行但冗余的方案:硬编码校验
可以在每个方法中手动从安全上下文获取用户信息判断:
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal(); if(!userDetails.getId().equals(r.getId())){ // 抛出权限错误 }
问题:大量方法需要处理时,重复代码过多,不够优雅。
解决方案
方法1:让@PreAuthorize识别自定义Principal
- 确保
CustomUserDetails实现UserDetails接口,且在JwtAutentificationFilter中正确设置到Authentication对象:
// 在JwtAutentificationFilter的doFilterInternal方法中 CustomUserDetails customUser = // 从JWT解析出的自定义用户信息 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUser, null, customUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authToken);
- 在SpEL表达式中显式转换Principal类型,避免类型匹配问题:
@PreAuthorize("#r.id == T(com.yourpackage.CustomUserDetails).cast(principal).getId()") public ResponseEntity<?> update(UpdateRequest r) { return ResponseEntity.ok().build(); }
方法2:修复自定义授权Bean的问题
如果坚持用自定义Bean的方式,调整以下两点:
- 将校验方法改为实例方法,并将类注册为Spring Bean:
@Component("decider") public class AuthorizationDecider { public boolean mayUpdate(Object principal, Long id){ CustomUserDetails user = (CustomUserDetails) principal; return user.getId().equals(id); } }
- SpEL表达式中请求体参数需加
#前缀(之前的错误大概率是缺少#):
@PreAuthorize("@decider.mayUpdate(principal, #r.id)") public ResponseEntity<?> update(UpdateRequest r) { return ResponseEntity.ok().build(); }
方法3:自定义通用校验注解(推荐大量方法使用)
通过AOP实现通用的权限校验逻辑,避免重复代码:
- 创建自定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckOwnId { String fieldName() default "id"; // 指定请求体中id字段的名称 }
- 编写AOP切面:
@Aspect @Component public class OwnIdCheckAspect { @Around("@annotation(checkOwnId)") public Object checkOwnId(ProceedingJoinPoint joinPoint, CheckOwnId checkOwnId) throws Throwable { // 获取请求体参数(假设请求体是方法第一个参数) Object request = joinPoint.getArgs()[0]; // 反射获取请求体中的id值 Field field = request.getClass().getDeclaredField(checkOwnId.fieldName()); field.setAccessible(true); Long requestId = (Long) field.get(request); // 获取当前用户信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); if (!user.getId().equals(requestId)) { throw new AccessDeniedException("无权限操作该资源"); } return joinPoint.proceed(); } }
- 方法上直接使用注解即可:
@CheckOwnId public ResponseEntity<?> update(UpdateRequest r) { return ResponseEntity.ok().build(); }
内容的提问来源于stack exchange,提问作者Almer
相关产品推荐
相关产品推荐

