Spring Boot集成Twitch OIDC无法获取用户邮箱问题求助
Spring Boot集成Twitch OIDC无法获取UserInfo邮箱的解决方案
问题场景
在Spring Boot应用中通过Spring Authorization Server集成Twitch OIDC身份提供商,已按Twitch文档要求在/oauth2/authorize请求中添加claims参数(URL中已可见该参数),但调用UserInfo端点始终无法获取用户邮箱。
核心问题分析
当前代码中claims参数的配置存在遗漏:仅在id_token字段声明了email和email_verified,但UserInfo端点需要的声明未在userinfo字段中指定。原代码的claims解码后为:
{"id_token":{"email":null,"email_verified":null},"userinfo":{"picture":null}}
userinfo部分仅包含picture,未包含email和email_verified,导致Twitch不会在UserInfo响应中返回邮箱信息。
解决方案
1. 修正Claims参数配置
重构自定义授权请求解析器中的claims构建逻辑,确保userinfo字段包含所需的邮箱声明,同时采用可靠的JSON序列化+URL编码方式,避免手动编码错误:
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { private final OAuth2AuthorizationRequestResolver defaultResolver; public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo) { this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization"); } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request); return authRequest != null ? customizeAuthorizationRequest(authRequest) : null; } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request, clientRegistrationId); return authRequest != null ? customizeAuthorizationRequest(authRequest) : null; } private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest authRequest) { Map<String, Object> additionalParameters = new LinkedHashMap<>(authRequest.getAdditionalParameters()); // 构建完整的Claims结构:同时针对ID Token和UserInfo指定邮箱声明 Map<String, Object> claims = new HashMap<>(); // ID Token中需要的声明 Map<String, Object> idTokenClaims = new HashMap<>(); idTokenClaims.put("email", null); idTokenClaims.put("email_verified", null); // UserInfo端点需要的声明 Map<String, Object> userInfoClaims = new HashMap<>(); userInfoClaims.put("email", null); userInfoClaims.put("email_verified", null); userInfoClaims.put("picture", null); claims.put("id_token", idTokenClaims); claims.put("userinfo", userInfoClaims); // 序列化并URL编码Claims参数 try { ObjectMapper objectMapper = new ObjectMapper(); String claimsJson = objectMapper.writeValueAsString(claims); String encodedClaims = URLEncoder.encode(claimsJson, StandardCharsets.UTF_8.toString()); additionalParameters.put("claims", encodedClaims); } catch (JsonProcessingException | UnsupportedEncodingException e) { throw new RuntimeException("Failed to encode claims parameter", e); } return OAuth2AuthorizationRequest.from(authRequest) .additionalParameters(additionalParameters) .build(); } }
2. 确保自定义解析器正确注册
在Spring Security配置中,将自定义解析器绑定到OAuth2登录的授权端点:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .authorizationEndpoint(endpoint -> endpoint .authorizationRequestResolver(new CustomAuthorizationRequestResolver(clientRegistrationRepository())) ) ); return http.build(); } @Bean public ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(twitchClientRegistration()); } private ClientRegistration twitchClientRegistration() { return ClientRegistration.withRegistrationId("twitch") .clientId("your-client-id") .clientSecret("your-client-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .redirectUri("http://localhost:9000/login/oauth2/code/twitch") .scope("openid", "user:read:email") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .clientName("Sign in with Twitch") .authorizationUri("https://id.twitch.tv/oauth2/authorize") .tokenUri("https://id.twitch.tv/oauth2/token") .userInfoUri("https://id.twitch.tv/oauth2/userinfo") .jwkSetUri("https://id.twitch.tv/oauth2/keys") .userNameAttributeName("preferred_username") .build(); } }
3. 验证UserInfo返回内容
添加一个测试接口,打印UserInfo的所有属性,确认邮箱字段是否存在:
import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController public class UserController { @GetMapping("/user") public Map<String, Object> getUserInfo(Authentication authentication) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; OAuth2User oauth2User = oauthToken.getPrincipal(); return oauth2User.getAttributes(); } }
关键注意事项
- 必须确保
user:read:emailscope已正确配置(当前配置已满足),这是Twitch返回邮箱信息的前置条件。 - 避免手动编写URL编码后的JSON字符串,容易出现拼写或编码错误,推荐使用序列化工具生成。
内容的提问来源于stack exchange,提问作者Spartan
相关产品推荐
相关产品推荐

