SpringBoot OAuth2集成Okta场景下如何为Jwt添加可校验自定义声明
结论:可以实现,但首先要明确核心限制
JWT的签名由授权服务器(Okta)的私钥生成,公钥公开用于校验,你拿到已签发的JWT后如果直接修改声明内容,无法用Okta的私钥重新签名,修改后的令牌会直接验签失败,这也是TokenEnhancer、TokenConverter这类组件不适用的原因——它们都是授权服务器签发令牌时用于增强声明的组件,无法处理已签发完成的令牌。
下面是两种可行实现方案:
方案1:业务侧自行签发嵌套令牌(推荐,侵入性最低)
思路是不修改原Okta签发的JWT,将原令牌和自定义声明封装后用业务侧自己持有的密钥签发新的JWT返回给前端,资源服务器侧先校验外层业务令牌,再取出原Okta令牌走原有校验逻辑即可。
1. 新增JJWT依赖(用于自签发JWT)
如果项目未引入JWT工具包,先添加依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
2. 实现添加自定义声明的接口
@GetMapping("/addIdUser") public ResponseEntity<String> addUser(@AuthenticationPrincipal Jwt originalJwt) { // 定义需要添加的自定义声明 Map<String, Object> extraClaims = new HashMap<>(); extraClaims.put("custom_user_id", "你的业务自定义值"); extraClaims.put("biz_scene", "当前业务场景标识"); // 存储原Okta令牌的原始值 extraClaims.put("original_okta_token", originalJwt.getTokenValue()); // 用业务侧自己的密钥签发新JWT,过期时间和原令牌保持一致即可 Key secretKey = Keys.hmacShaKeyFor("你自己设置的至少32位安全密钥".getBytes(StandardCharsets.UTF_8)); String newToken = Jwts.builder() .setClaims(extraClaims) .setExpiration(originalJwt.getExpiresAt()) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); return new ResponseEntity<>(newToken, HttpStatus.OK); }
3. 资源服务器新增自定义过滤器处理校验逻辑
在原有OAuth2校验过滤器之前新增一层过滤器,先解析外层自签发令牌,再将原Okta令牌透传给后续的Spring Security校验流程:
@Configuration @EnableWebSecurity public class ResourceServerConfig { @Value("${biz.jwt.secret}") private String selfJwtSecret; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) // 自定义过滤器放在Bearer令牌校验过滤器之前 .addFilterBefore(new CustomJwtExtractFilter(selfJwtSecret), BearerTokenAuthenticationFilter.class) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } public static class CustomJwtExtractFilter extends OncePerRequestFilter { private final String secret; public CustomJwtExtractFilter(String secret) { this.secret = secret; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String bearerToken = resolveToken(request); if (bearerToken == null) { filterChain.doFilter(request, response); return; } try { // 校验外层自签发JWT的合法性 Jws<Claims> jws = Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(bearerToken); // 取出自定义声明存入请求属性,业务代码可直接读取 request.setAttribute("custom_claims", jws.getBody()); // 替换请求头的Authorization为原Okta令牌,供后续OAuth2校验 HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { @Override public String getHeader(String name) { if (HttpHeaders.AUTHORIZATION.equals(name)) { return "Bearer " + jws.getBody().get("original_okta_token"); } return super.getHeader(name); } }; filterChain.doFilter(wrapper, response); } catch (JwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("非法令牌"); } } private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } }
4. 业务代码读取自定义声明
@GetMapping("/biz/test") public String testBiz(HttpServletRequest request) { Claims customClaims = (Claims) request.getAttribute("custom_claims"); String customUserId = (String) customClaims.get("custom_user_id"); return "自定义用户ID:" + customUserId; }
方案2:调用Okta令牌交换接口
如果不希望自行维护令牌签名逻辑,可以调用Okta的令牌交换接口,请求Okta为你签发包含自定义声明的新令牌,所有令牌均由Okta签名,无需修改原有资源服务器的校验逻辑。
操作步骤为:
- 在Okta后台配置令牌交换策略,开放自定义声明映射规则
- 业务代码中调用Okta的
/token接口,grant_type指定为urn:ietf:params:oauth:grant-type:token-exchange,传入原有令牌和需要添加的自定义声明参数 - 拿到Okta返回的新令牌直接返回给前端即可
内容的提问来源于stack exchange,提问作者CoderJammer
相关产品推荐
相关产品推荐

