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

求SpringBoot集成MockPass(SingPass v2-NDI OIDC)示例及JWT解码方案

SpringBoot集成MockPass(SingPass v2 - NDI OIDC)问题解决与示例说明

问题原因

你遇到的invalid_id_token错误,核心原因是MockPass返回的ID Token采用JWE加密格式,而Spring Security OAuth2默认仅处理JWS签名格式的Token,因此会触发JWEHeader转JWSHeader的类型转换异常。

自定义Token解码方案

要解决这个问题,你需要自定义JWT解码器,让它同时支持JWE解密和JWS验证,同时可以在Token流转过程中拦截并自行解码。

1. 配置支持JWE的JwtDecoder

以下是完整的Spring Security配置类,包含自定义解码器和Token拦截逻辑:

@Configuration
public class MockPassSecurityConfig {

    @Value("${spring.security.oauth2.client.registration.mockpass.client-id}")
    private String clientId;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .oauth2Login(oauth2 -> oauth2
                        .tokenEndpoint(token -> token.accessTokenResponseClient(customTokenResponseClient()))
                        .userInfoEndpoint(userInfo -> userInfo.oidcUserService(customOidcUserService()))
                );
        return http.build();
    }

    // 自定义Token响应客户端,用于拦截原始ID Token并自行解码
    private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customTokenResponseClient() {
        DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
        client.setAccessTokenResponseConverter(tokenParams -> {
            // 获取原始ID Token
            String rawIdToken = (String) tokenParams.get("id_token");
            
            // 自行解码JWE格式的Token
            try {
                JWT jwt = JWTParser.parse(rawIdToken);
                if (jwt instanceof EncryptedJWT encryptedJWT) {
                    // 从JWK Set加载解密密钥
                    JWKSource<SecurityContext> jwkSource = JWKSet.load(new URL(jwkSetUri)).toJWKSource();
                    JWEKeySelector<SecurityContext> keySelector = new JWEKeySelector<>(jwkSource);
                    JWK decryptionKey = keySelector.selectJWEKeys(encryptedJWT.getHeader(), null).get(0);
                    
                    // 解密Token
                    encryptedJWT.decrypt(new DirectDecrypter(decryptionKey.toSecretKey()));
                    System.out.println("解密后的Claims: " + encryptedJWT.getJWTClaimsSet());
                }
            } catch (ParseException | JOSEException | MalformedURLException e) {
                throw new OAuth2AuthenticationException(new OAuth2Error("invalid_id_token", "解码ID Token失败", null));
            }

            return DefaultAuthorizationCodeTokenResponseClient.DEFAULT_CONVERTER.convert(tokenParams);
        });
        return client;
    }

    // 自定义OIDC用户服务,基于解密后的Token构建用户信息
    private OidcUserService customOidcUserService() {
        OidcUserService delegate = new OidcUserService();
        delegate.setOidcUserMapper(userInfo -> {
            // 这里可以直接使用解密后的ID Token Claims
            OidcIdToken idToken = userInfo.getIdToken();
            return new DefaultOidcUser(
                    AuthorityUtils.createAuthorityList("ROLE_USER"),
                    idToken,
                    "sub" // 用sub作为用户名标识
            );
        });
        return delegate;
    }

    // 配置支持JWE的JwtDecoder
    @Bean
    public JwtDecoder jwtDecoder() throws MalformedURLException {
        JWKSource<SecurityContext> jwkSource = JWKSet.load(new URL(jwkSetUri)).toJWKSource();
        ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        
        // 设置JWE解密密钥选择器
        jwtProcessor.setJWEKeySelector(new JWEKeySelector<>(jwkSource));
        // 设置JWS验证密钥选择器
        jwtProcessor.setJWSKeySelector(JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(jwkSource));
        
        return new NimbusJwtDecoder(jwtProcessor);
    }
}

2. 配置文件示例(application.yml)

spring:
  security:
    oauth2:
      client:
        registration:
          mockpass:
            client-id: YOUR_MOCKPASS_CLIENT_ID
            client-secret: YOUR_MOCKPASS_CLIENT_SECRET
            scope: openid,profile
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/mockpass"
        provider:
          mockpass:
            authorization-uri: https://mockpass.singpass.gov.sg/oauth/v2/authorize
            token-uri: https://mockpass.singpass.gov.sg/oauth/v2/token
            user-info-uri: https://mockpass.singpass.gov.sg/oauth/v2/userinfo
            jwk-set-uri: https://mockpass.singpass.gov.sg/oauth/v2/jwks
            user-name-attribute: sub

示例项目结构

src/
├── main/
│   ├── java/com/yourcompany/mockpassdemo/
│   │   ├── MockpassDemoApplication.java
│   │   └── config/MockPassSecurityConfig.java
│   └── resources/
│       └── application.yml

关键注意事项

  • 确保你的Spring Boot版本在2.7.x及以上,Spring Security 5.7.x及以上,这些版本对JWE的支持更成熟
  • 确认MockPass后台配置的redirect-uri与Spring Boot应用配置的完全一致
  • 调试时可以先将原始ID Token复制到JWT调试工具,但JWE格式需要密钥才能解密,建议用代码中的日志打印解密后的Claims

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 14:47:34