如何在Client Credentials Flow下将用户Security Principal传递给后端REST API?
解决方案步骤
1. BFF端:传递已认证用户信息至REST API
BFF的Session中存储着用户从Auth0获取的令牌,先从中解析出用户唯一标识(比如Auth0令牌里的sub字段),再在调用REST API时通过自定义请求头传递该标识。
代码示例(BFF侧RestTemplate调用)
// 从Session取出用户的Auth0令牌 String userAccessToken = (String) session.getAttribute("userAccessToken"); // 解析JWT获取用户ID(sub) Jwt jwt = Jwts.parser().setSigningKey(auth0PublicKey).parseClaimsJws(userAccessToken).getBody(); String userId = jwt.getSubject(); // 构造请求头,同时携带Client Credentials令牌和用户ID HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(clientCredentialsToken); // 原有的服务间调用令牌 headers.set("X-Authenticated-User-Id", userId); HttpEntity<Void> requestEntity = new HttpEntity<>(headers); restTemplate.exchange("http://rest-api-server/entities/{id}", HttpMethod.PUT, requestEntity, Entity.class, id);
2. REST API端:拦截请求并实例化Security Principal
通过Spring Security过滤器拦截请求,从自定义请求头提取用户ID,生成Authentication对象并存入SecurityContext,为@PreAuthorize注解提供可用的Principal。
2.1 自定义Authentication令牌
public class UserIdAuthenticationToken extends AbstractAuthenticationToken { private final String userId; public UserIdAuthenticationToken(String userId) { super(Collections.emptyList()); this.userId = userId; setAuthenticated(true); // 标记为合法认证用户 } @Override public Object getCredentials() { return null; // 无需凭据,仅传递用户标识 } @Override public Object getPrincipal() { return userId; // 将用户ID作为Principal } }
2.2 自定义请求头解析过滤器
public class UserIdHeaderAuthenticationFilter extends OncePerRequestFilter { private static final String USER_ID_HEADER = "X-Authenticated-User-Id"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String userId = request.getHeader(USER_ID_HEADER); // 仅在未设置Authentication时注入 if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) { Authentication authentication = new UserIdAuthenticationToken(userId); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } }
2.3 Spring Security配置
同时保留Client Credentials令牌的合法性验证,确保请求来自合法BFF,再添加自定义过滤器注入用户Principal:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用@PreAuthorize注解 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .anyRequest().authenticated() // 先验证BFF的Client Credentials令牌合法性 .and().oauth2ResourceServer().jwt() // 再注入用户Principal .and().addFilterBefore(new UserIdHeaderAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
3. 用@PreAuthorize实现实体归属验证
现在SecurityContext中已存在用户ID作为Principal,直接通过SpEL表达式或自定义方法验证实体的ownerid是否匹配当前用户。
方式一:直接使用SpEL表达式
@RestController @RequestMapping("/entities") public class EntityController { @Autowired private EntityRepository entityRepository; @PutMapping("/{id}") @PreAuthorize("@entityRepository.findById(#id).orElseThrow().ownerid == authentication.principal") public ResponseEntity<Entity> updateEntity(@PathVariable Long id, @RequestBody Entity entity) { Entity existing = entityRepository.findById(id).orElseThrow(() -> new RuntimeException("实体不存在")); // 复制更新字段(保留原ownerid) BeanUtils.copyProperties(entity, existing, "id", "ownerid"); return ResponseEntity.ok(entityRepository.save(existing)); } }
方式二:自定义权限验证方法
如果逻辑复杂,可封装为独立方法:
@Component("securityChecker") public class SecurityChecker { @Autowired private EntityRepository entityRepository; public boolean isEntityOwner(Long entityId, String userId) { Entity entity = entityRepository.findById(entityId).orElse(null); return entity != null && userId.equals(entity.getOwnerid()); } }
控制器中调用:
@PutMapping("/{id}") @PreAuthorize("@securityChecker.isEntityOwner(#id, authentication.principal)") public ResponseEntity<Entity> updateEntity(@PathVariable Long id, @RequestBody Entity entity) { // 业务逻辑 }
额外注意事项
- 确保BFF与REST API间通信使用HTTPS,防止自定义请求头被篡改;
- BFF需先验证用户令牌的有效性,再解析用户ID,避免传递伪造信息;
- 若频繁查询实体owner信息,可添加缓存优化性能;
- 如需更多用户属性(如角色),可在BFF传递额外字段,或REST API根据用户ID从数据库查询并封装为UserDetails作为Principal。
内容的提问来源于stack exchange,提问作者user193116
相关产品推荐
相关产品推荐

