自定义认证注解如何正确传入用户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),需要手动通过切面来解析参数表达式。
- 改造自定义注解,移除
@PreAuthorize,保留基础元注解:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface CurrentUserAccessLevel { String userId(); }
- 编写切面类处理权限校验逻辑:
@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
相关产品推荐
相关产品推荐

