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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 05:00:36