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
相关产品推荐
相关产品推荐

