You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

  1. 确保CustomUserDetails实现UserDetails接口,且在JwtAutentificationFilter中正确设置到Authentication对象:
// 在JwtAutentificationFilter的doFilterInternal方法中
CustomUserDetails customUser = // 从JWT解析出的自定义用户信息
UsernamePasswordAuthenticationToken authToken = 
    new UsernamePasswordAuthenticationToken(customUser, null, customUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
  1. 在SpEL表达式中显式转换Principal类型,避免类型匹配问题:
@PreAuthorize("#r.id == T(com.yourpackage.CustomUserDetails).cast(principal).getId()")
public ResponseEntity<?> update(UpdateRequest r) {
    return ResponseEntity.ok().build();
}

方法2:修复自定义授权Bean的问题

如果坚持用自定义Bean的方式,调整以下两点:

  1. 将校验方法改为实例方法,并将类注册为Spring Bean:
@Component("decider")
public class AuthorizationDecider {
    public boolean mayUpdate(Object principal, Long id){
        CustomUserDetails user = (CustomUserDetails) principal;
        return user.getId().equals(id);
    }
}
  1. SpEL表达式中请求体参数需加#前缀(之前的错误大概率是缺少#):
@PreAuthorize("@decider.mayUpdate(principal, #r.id)")
public ResponseEntity<?> update(UpdateRequest r) {
    return ResponseEntity.ok().build();
}

方法3:自定义通用校验注解(推荐大量方法使用)

通过AOP实现通用的权限校验逻辑,避免重复代码:

  1. 创建自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckOwnId {
    String fieldName() default "id"; // 指定请求体中id字段的名称
}
  1. 编写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();
    }
}
  1. 方法上直接使用注解即可:
@CheckOwnId
public ResponseEntity<?> update(UpdateRequest r) {
    return ResponseEntity.ok().build();
}

内容的提问来源于stack exchange,提问作者Almer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 12:44:51