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

Spring Security OIDC应用中nonce参数生成与配置问题

解决OpenID Connect Spring Client对接需nonce参数的IDP问题

我之前在基于这个项目做多IDP对接的时候,也碰到过需要给特定IDP添加nonce参数的问题,分享下我当时的解决思路和具体实现:

1. 先理解Spring Security对nonce的默认处理

Spring Security 5.3+版本的OIDC客户端其实默认会自动生成并处理nonce:

  • 默认的DefaultOidcAuthorizationRequestResolver会在授权请求时生成随机nonce,添加到请求参数中
  • 同时会把nonce和授权请求绑定,在后续验证ID Token时自动校验一致性

如果你的项目里没有自定义覆盖授权请求解析器,先检查是不是IDP对nonce有特殊要求(比如Base64编码),或者配置里无意中禁用了这个逻辑。

2. 针对特定IDP自定义nonce生成逻辑

如果项目需要给某一个IDP单独配置nonce(其他IDP不需要),可以通过授权请求增强器来实现:

步骤1:添加nonce到授权请求参数

在Spring Security的配置类中,给oauth2Login配置authorizationRequestCustomizer,针对目标IDP生成nonce并存储到session中:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(auth -> auth.anyRequest().authenticated())
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authEndpoint -> authEndpoint
                    .authorizationRequestCustomizer(customizer -> customizer
                        .additionalParameters(params -> {
                            // 只给需要nonce的IDP添加参数
                            ClientRegistration client = customizer.getClientRegistration();
                            if ("your-target-idp-registration-id".equals(client.getRegistrationId())) {
                                // 生成随机nonce,可根据IDP要求转成Base64
                                String nonce = UUID.randomUUID().toString();
                                // 如果IDP要求Base64编码,用下面这行替换
                                // String nonce = Base64.getUrlEncoder().encodeToString(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
                                
                                params.put("nonce", nonce);
                                // 将nonce存入session,后续验证ID Token用
                                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                                request.getSession().setAttribute("nonce_" + client.getRegistrationId(), nonce);
                            }
                        })
                    )
                )
                .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(customOidcUserService())
                )
            );
    }
}

步骤2:验证ID Token中的nonce

实现自定义的OidcUserService,校验返回的ID Token中的nonce和请求时存储的是否一致:

private OAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService() {
    final OidcUserService delegate = new OidcUserService();
    return userRequest -> {
        // 从session取出存储的nonce
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String storedNonce = (String) request.getSession().getAttribute("nonce_" + userRequest.getClientRegistration().getRegistrationId());
        
        // 校验ID Token中的nonce
        OidcIdToken idToken = userRequest.getIdToken();
        String tokenNonce = idToken.getNonce();
        if (storedNonce == null || !storedNonce.equals(tokenNonce)) {
            throw new OAuth2AuthenticationException(new OAuth2Error("invalid_nonce", "Nonce参数不匹配", null));
        }
        
        // 验证通过后清理session中的nonce
        request.getSession().removeAttribute("nonce_" + userRequest.getClientRegistration().getRegistrationId());
        
        return delegate.loadUser(userRequest);
    };
}

3. 自定义授权请求解析器的情况

如果你的项目里自定义了OidcAuthorizationRequestResolver(比如项目多IDP的扩展逻辑),需要确保集成默认的nonce生成逻辑:

public class CustomOidcAuthorizationRequestResolver implements OidcAuthorizationRequestResolver {

    private final OidcAuthorizationRequestResolver delegate;

    public CustomOidcAuthorizationRequestResolver(OAuth2AuthorizationRequestResolver baseResolver) {
        // 基于默认的OIDC授权请求解析器构建代理
        this.delegate = new DefaultOidcAuthorizationRequestResolver(baseResolver);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest authRequest = this.delegate.resolve(request);
        return customizeAuthRequest(authRequest, request);
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest authRequest = this.delegate.resolve(request, clientRegistrationId);
        return customizeAuthRequest(authRequest, request);
    }

    private OAuth2AuthorizationRequest customizeAuthRequest(OAuth2AuthorizationRequest authRequest, HttpServletRequest request) {
        if (authRequest == null) {
            return null;
        }
        
        ClientRegistration client = authRequest.getClientRegistration();
        if ("your-target-idp-registration-id".equals(client.getRegistrationId())) {
            // 如果默认没生成nonce,手动添加(如果默认已经生成,可以跳过这步)
            Map<String, Object> additionalParams = new LinkedHashMap<>(authRequest.getAdditionalParameters());
            if (!additionalParams.containsKey("nonce")) {
                String nonce = UUID.randomUUID().toString();
                additionalParams.put("nonce", nonce);
                request.getSession().setAttribute("nonce_" + client.getRegistrationId(), nonce);
            }
            
            // 返回更新后的授权请求
            return OAuth2AuthorizationRequest.from(authRequest)
                .additionalParameters(additionalParams)
                .build();
        }
        
        return authRequest;
    }
}

然后在配置类中注册这个解析器:

@Bean
public OidcAuthorizationRequestResolver oidcAuthorizationRequestResolver(ClientRegistrationRepository clientRepo) {
    OAuth2AuthorizationRequestResolver baseResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRepo, "/oauth2/authorization");
    return new CustomOidcAuthorizationRequestResolver(baseResolver);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(auth -> auth.anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .authorizationEndpoint(authEndpoint -> authEndpoint
                .authorizationRequestResolver(oidcAuthorizationRequestResolver(clientRepo))
            )
        );
}

注意事项

  • 有些IDP要求nonce必须是URL安全的Base64编码,记得按要求调整生成逻辑
  • 一定要验证ID Token中的nonce,避免重放攻击
  • 多IDP场景下,要给每个IDP的nonce单独存储(比如用nonce_{registrationId}作为session key),避免不同IDP之间的冲突

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 07:50:49