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

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:email scope已正确配置(当前配置已满足),这是Twitch返回邮箱信息的前置条件。
  • 避免手动编写URL编码后的JSON字符串,容易出现拼写或编码错误,推荐使用序列化工具生成。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 22:50:56