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

实现OAuth资源服务器时遇Jwt解码错误:Malformed Jwk set

OAuth资源服务器JWT解析错误:Malformed Jwk set

问题现象

向测试用OAuth资源服务器的受保护API发送携带access token的请求时,触发以下错误:

An error occurred while attempting to decode the Jwt: Malformed Jwk set.

认证服务器配置

@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
        throws Exception {
    
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
        .oidc(Customizer.withDefaults());   // 启用OpenID Connect 1.0
    
    http
        // 未认证时从授权端点重定向到登录页
        .exceptionHandling((exceptions) -> exceptions
            .defaultAuthenticationEntryPointFor(
                new LoginUrlAuthenticationEntryPoint("/login"),
                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
            ))
        
        // 接受用于用户信息和/或客户端注册的access token
        .oauth2ResourceServer((resourceServer) -> resourceServer
            .jwt(Customizer.withDefaults()));

    return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
        throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .requestMatchers("/jkws").permitAll()
            .anyRequest().authenticated()
        );
        
    // 表单登录处理来自授权服务器过滤器链的登录页重定向
    http.formLogin(Customizer.withDefaults());
    
    http.csrf(csrf -> csrf.disable());
    http.cors(cors -> cors.disable());
    
    return http.build();
}

@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("oidc-client")
            //.clientSecret("{noop}secret")
            .clientSecret("$2a$04$R.Nl31CwJtKOtCDcE69e8OO0O8ryb8Rx2vhcUS5wde6Zpj2750kt2")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .redirectUri("https://oauth.pstmn.io/v1/callback")
            .postLogoutRedirectUri("http://127.0.0.1:8080/")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            .build();

    return new InMemoryRegisteredClientRepository(oidcClient);
}

@Bean
public JWKSource<SecurityContext> jwkSource() {
    KeyPair keyPair = generateRsaKey();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
    JWKSet jwkSet = new JWKSet(rsaKey);
    return new ImmutableJWKSet<>(jwkSet);
}

private static KeyPair generateRsaKey() {
    KeyPair keyPair;
    try {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        keyPair = keyPairGenerator.generateKeyPair();
    }
    catch (Exception ex) {
        throw new IllegalStateException(ex);
    }
    return keyPair;
}

@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
    return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

@Bean
public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder().jwkSetEndpoint("/jwks").build();
}

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() { 
    return (context) -> {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { 
            context.getClaims().claims((claims) -> { 
                Set<String> roles = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities())
                        .stream()
                        .map(c -> c.replaceFirst("^ROLE_", ""))
                        .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); 
                claims.put("roles", roles); 
            });
        }
    };
}

资源服务器配置

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer((oauth2) -> oauth2
            .jwt(Customizer.withDefaults())
        );
    return http.build();
}

/*
 * http://localhost:8080/.well-known/openid-configuration
 */
@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder
            .withJwkSetUri(jwkSetUri)
            .jwsAlgorithm(SignatureAlgorithm.RS256).build();
}

资源服务器属性

spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/jkws

问题根源与修复方案

核心问题:端点路径拼写不一致

  • 认证服务器通过AuthorizationServerSettings配置的JWK集端点是/jwks,但defaultSecurityFilterChain中开放匿名访问的路径写成了/jkws(字母顺序错误)。
  • 资源服务器配置的jwk-set-uri同样错误地指向了/jkws,导致请求到不存在的端点,返回的内容并非合法的JWK集,最终触发解析错误。

修复步骤

  1. 修正认证服务器的权限配置
    将defaultSecurityFilterChain中的requestMatchers("/jkws").permitAll()修改为:

    .requestMatchers("/jwks").permitAll()
    
  2. 修正资源服务器的配置属性
    将资源服务器的配置文件中的属性改为:

    spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/jwks
    

验证修复

完成修改后,直接访问http://localhost:8080/jwks,确认返回的是标准的JWK集JSON格式,示例如下:

{
  "keys": [
    {
      "kty": "RSA",
      "e": "AQAB",
      "use": "sig",
      "kid": "xxxxxx",
      "alg": "RS256",
      "n": "xxxxxx"
    }
  ]
}

内容的提问来源于stack exchange,提问作者Kapil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 03:27:33