Spring Boot中验证Cognito OAuth2令牌与传入userId匹配的方法
验证传入userId与Cognito令牌用户一致性的优雅实现
你的Spring Boot应用已经通过OAuth2资源服务器完成了Cognito JWT的验证,要实现传入userId与令牌所属用户的一致性校验,无需调用Cognito的userInfo端点(JWT本身是自包含令牌,已携带用户核心信息,调用外部接口会增加性能开销),以下是两种优雅的实现方式:
方式一:方法层面校验(推荐,灵活可控)
利用Spring Security的方法级安全注解@PreAuthorize,直接在Controller方法中添加校验逻辑,无需额外编写过滤器或注解。
直接使用表达式校验
如果你的Cognito令牌中用户ID存储在sub字段(Cognito默认的用户唯一标识),可以直接在方法上添加:
@GetMapping("/users/{userId}") @PreAuthorize("#userId == authentication.principal.subject") public ResponseEntity<User> getUserDetail(@PathVariable String userId) { // 业务逻辑 return ResponseEntity.ok(userService.getById(userId)); }
如果你的用户ID存储在cognito:username字段(自定义用户名场景),则修改表达式为:
@PreAuthorize("#userId == authentication.principal.attributes['cognito:username']")
自定义权限校验器(适合复杂场景)
如果需要复用校验逻辑,或者表达式过于冗长,可以自定义权限校验器:
- 实现
PermissionEvaluator接口:
public class UserIdMatchPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication authentication, Object targetUserId, Object permission) { if (authentication == null || !(authentication.getPrincipal() instanceof Jwt)) { return false; } Jwt jwt = (Jwt) authentication.getPrincipal(); // 根据实际字段获取令牌中的用户ID,这里用sub示例 String tokenUserId = jwt.getSubject(); return tokenUserId.equals(targetUserId.toString()); } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { return false; } }
- 在
SecurityConfig中配置方法安全表达式处理器:
@Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig { // ... 原有的filterChain配置 @Bean public MethodSecurityExpressionHandler methodSecurityExpressionHandler() { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); handler.setPermissionEvaluator(new UserIdMatchPermissionEvaluator()); return handler; } }
- 在Controller方法中使用:
@GetMapping("/users/{userId}") @PreAuthorize("hasPermission(#userId, 'USER_ID_MATCH')") public ResponseEntity<User> getUserDetail(@PathVariable String userId) { // 业务逻辑 return ResponseEntity.ok(userService.getById(userId)); }
方式二:全局过滤器校验(适合批量接口统一处理)
如果你的大部分接口都需要校验userId与令牌的一致性,可以编写全局过滤器,在认证完成后自动校验:
- 实现
OncePerRequestFilter:
@Component public class JwtUserIdValidationFilter extends OncePerRequestFilter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 定义需要校验的接口路径模板,根据你的实际接口规则调整 String[] protectedPatterns = {"/users/{userId}", "/orders/{userId}/**"}; // 判断当前请求是否匹配需要校验的路径 boolean needValidation = Arrays.stream(protectedPatterns) .anyMatch(pattern -> pathMatcher.match(pattern, request.getRequestURI())); if (needValidation) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) { String tokenUserId = jwt.getSubject(); // 或cognito:username // 从路径中提取传入的userId String pathUserId = extractUserIdFromPath(request, protectedPatterns); if (!tokenUserId.equals(pathUserId)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "传入的userId与令牌所属用户不匹配"); return; } } } filterChain.doFilter(request, response); } private String extractUserIdFromPath(HttpServletRequest request, String[] patterns) { for (String pattern : patterns) { Map<String, String> variables = pathMatcher.extractUriTemplateVariables(pattern, request.getRequestURI()); if (variables.containsKey("userId")) { return variables.get("userId"); } } return null; } }
- 在
SecurityConfig中注册过滤器,确保在OAuth2资源服务器过滤器之前执行:
@Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtUserIdValidationFilter userIdValidationFilter) throws Exception { http .requiresChannel(channel -> channel.anyRequest().requiresSecure()) .cors().and().csrf().disable() .addFilterBefore(userIdValidationFilter, OAuth2ResourceServerFilter.class) // 添加自定义过滤器 .authorizeHttpRequests(auth -> auth .requestMatchers(new AntPathRequestMatcher("/actuator/**")).permitAll() .anyRequest().authenticated()) .oauth2ResourceServer().jwt(); return http.build(); }
关键说明
- Cognito JWT默认包含
sub(用户唯一UUID)、cognito:username(用户登录名)等字段,可通过jwt.getClaims()查看所有字段,选择与业务匹配的用户ID字段。 - 优先使用方法层面校验,因为它更灵活,能针对不同接口做差异化处理;全局过滤器适合批量统一规则的场景。
内容的提问来源于stack exchange,提问作者blastervla
相关产品推荐
相关产品推荐

