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

自定义认证注解如何正确传入用户ID参数?权限越权规避方案

问题:自定义权限注解参数传递失败,如何优化横向越权校验

我有一个REST API接口,用来获取学生的课程详情:

@CurrentUserAccessLevel(userId = "#studentId")
@GetMapping(value = "/{course-code}/users/{student-id}")
public UserCourseDetailsDto getUserCourseDetails(@PathVariable(value = "course-code") final Long courseCode,
                                                 @PathVariable(value = "student-id") final Long studentId) {
    return userCourseService.getUserCourseDetails(studentId, courseCode);
}

同时定义了一个自定义权限注解@CurrentUserAccessLevel,用来校验当前用户是否有权限访问目标资源:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@PreAuthorize("@applicationUserDetailsService.hasAccessToResourceOwnedBy(#userId)")
public @interface CurrentUserAccessLevel {
    String userId();
}

注解调用的权限校验方法如下:

public boolean hasAccessToResourceOwnedBy(final Long userId) {
    final User currentUser = userService.resolveCurrentUser();
    return isAdmin(currentUser) || Objects.equals(currentUser.getId(), userId);
}

现在遇到的问题是:注解中通过userId = "#studentId"传递的参数始终为null,但如果直接把@PreAuthorize中的#userId改成#studentId,就能正确获取到路径参数里的学生ID并完成校验。

我的核心需求是通过这个自定义注解避免横向越权,想知道有没有更优的解决方案?


优化方案

方案1:用切面实现注解的参数解析(推荐,灵活易扩展)

Spring的@PreAuthorize表达式无法直接解析注解属性里的SpEL字符串(比如#studentId),需要手动通过切面来解析参数表达式。

  1. 改造自定义注解,移除@PreAuthorize,保留基础元注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CurrentUserAccessLevel {
    String userId();
}
  1. 编写切面类处理权限校验逻辑:
@Aspect
@Component
public class CurrentUserAccessLevelAspect {

    private final SpelExpressionParser spelParser = new SpelExpressionParser();
    private final ApplicationUserDetailsService authService;
    private final UserService userService;

    public CurrentUserAccessLevelAspect(ApplicationUserDetailsService authService, UserService userService) {
        this.authService = authService;
        this.userService = userService;
    }

    @Before("@annotation(currentUserAccessLevel)")
    public void checkResourceAccess(JoinPoint joinPoint, CurrentUserAccessLevel currentUserAccessLevel) {
        // 获取方法签名和参数信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = methodSignature.getMethod();
        Object[] methodArgs = joinPoint.getArgs();
        String[] paramNames = methodSignature.getParameterNames();

        // 构建SpEL上下文,绑定方法参数
        StandardEvaluationContext spelContext = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            spelContext.setVariable(paramNames[i], methodArgs[i]);
        }

        // 解析注解中传入的userId表达式,拿到目标用户ID
        String userIdExpr = currentUserAccessLevel.userId();
        Long targetUserId = spelParser.parseExpression(userIdExpr).getValue(spelContext, Long.class);

        // 执行权限校验,不通过则抛出异常
        if (!authService.hasAccessToResourceOwnedBy(targetUserId)) {
            throw new AccessDeniedException("无权访问该资源");
        }
    }
}

这种方式能完美解析注解中传入的#studentId参数,而且后续要扩展权限规则(比如增加角色校验、资源归属校验)也非常方便。

方案2:直接用Spring Security内置表达式简化校验(适合简单场景)

如果你的权限逻辑只是「管理员可访问,或当前用户ID与目标ID匹配」,完全可以不用自定义注解,直接在方法上写@PreAuthorize表达式:

@PreAuthorize("hasRole('ADMIN') || #studentId == authentication.principal.id")
@GetMapping(value = "/{course-code}/users/{student-id}")
public UserCourseDetailsDto getUserCourseDetails(@PathVariable(value = "course-code") final Long courseCode,
                                                 @PathVariable(value = "student-id") final Long studentId) {
    return userCourseService.getUserCourseDetails(studentId, courseCode);
}

这种方式代码量最少,不需要额外的自定义注解和切面,适合逻辑简单的场景。

方案3:动态SpEL表达式(不推荐,可读性差)

如果不想用切面,也可以修改注解的@PreAuthorize表达式,让它动态解析注解属性里的参数名,但这种方式写法繁琐,可读性极差,维护成本高:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@PreAuthorize("@applicationUserDetailsService.hasAccessToResourceOwnedBy(" +
        "@spelExpressionParser.parseExpression(#currentUserAccessLevel.userId()).getValue(" +
        "new org.springframework.context.expression.StandardEvaluationContext(" +
        "T(org.springframework.core.MethodParameter).forExecutable(" +
        "#root.target.getClass().getMethod(#root.method.name, #root.method.parameterTypes), 0))" +
        ")")
public @interface CurrentUserAccessLevel {
    String userId();
}

需要提前把SpelExpressionParser注入到Spring容器中,不建议在生产环境使用这种写法。


方案对比

  • 方案1(切面):最灵活,适合需要复用复杂权限逻辑的场景,扩展成本低。
  • 方案2(内置表达式):最简单,适合逻辑单一的权限校验,代码简洁直观。
  • 方案3(动态SpEL):可读性差,维护困难,不推荐使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 17:13:15