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

Spring Boot 3集成GitHub OAuth2登录时邮箱属性始终返回Null的问题求助

Spring Boot 3集成GitHub OAuth2登录时邮箱属性始终返回Null的问题求助

Hey 兄弟,我之前在集成GitHub OAuth2的时候也踩过一模一样的坑!咱们先把问题的根因理清楚,再给你具体的解决方案:

为什么邮箱始终返回Null?

这其实是GitHub OAuth2的一个容易被误解的细节,结合你的情况,主要有这几个原因:

  • 你配置的user:email scope,并不是让GitHub默认的用户信息端点(https://api.github.com/user)直接返回邮箱,而是允许你的应用调用GitHub的/user/emails API来获取用户的邮箱列表(包括公开和授权的私密邮箱)。
  • 即使你的邮箱设为公开,GitHub的/user端点(Spring Security默认调用的用户信息接口)也只有在你的公开主邮箱是唯一的、且是账号的默认主邮箱的情况下才会返回email字段,绝大多数场景下这个字段都会为空——这就是你现在遇到的核心问题!
  • 还有一个容易忽略的点:如果你是后来才修改的scope为user:email,但之前已经授权过应用,GitHub不会自动更新权限,你需要在GitHub的授权应用列表里 revoke 你的应用权限,重新登录授权,否则应用拿不到user:email的权限,调用/user/emails会报403。

怎么解决?

最可靠的方式是在你的CustomOauth2UserService里,用获取到的Access Token主动调用GitHub的/user/emails API来拉取邮箱。下面是具体的代码修改和配置注意事项:

1. 确保Scope配置正确(再确认+重新授权)

你的application.yml配置没问题,但一定要重新授权应用让新scope生效:

security:
  oauth2:
    client:
      registration:
        github:
          client-id: xxx
          client-secret: xxx
          redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
          scope: user:email  # 这个scope必须存在,否则调用/user/emails会被拒绝

👉 操作步骤:打开GitHub的Settings → Applications → Authorized OAuth Apps,找到你的应用点击Revoke access,然后重新登录你的应用完成授权。

2. 修改CustomOauth2UserService,主动拉取邮箱

在loadUser方法中,用Access Token调用GitHub的邮箱API,优先选择主邮箱(is_primary: true的验证过的邮箱):

@Service
@RequiredArgsConstructor
public class CustomOauth2UserService extends DefaultOAuth2UserService {
    private static final Logger log = LoggerFactory.getLogger(CustomOauth2UserService.class);
    private final UserConnectedAccountRepository userConnectedAccountRepository;
    private final UserRepository userRepository;
    private final RestTemplate restTemplate = new RestTemplate(); // 或者用WebClient(推荐,RestTemplate已标记为Deprecated)

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oAuth2User.getName();
        String email = null;

        // 针对GitHub,主动调用邮箱API拉取邮箱
        if ("github".equals(provider)) {
            OAuth2AccessToken accessToken = userRequest.getAccessToken();
            try {
                // 构造请求头,携带Access Token
                HttpHeaders headers = new HttpHeaders();
                headers.setBearerAuth(accessToken.getTokenValue());
                HttpEntity<Void> requestEntity = new HttpEntity<>(headers);

                // 调用GitHub邮箱API,返回邮箱列表
                ResponseEntity<List<Map<String, Object>>> response = restTemplate.exchange(
                        "https://api.github.com/user/emails",
                        HttpMethod.GET,
                        requestEntity,
                        new ParameterizedTypeReference<>() {}
                );

                if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null && !response.getBody().isEmpty()) {
                    // 优先选择已验证的主邮箱
                    for (Map<String, Object> emailInfo : response.getBody()) {
                        Boolean isPrimary = (Boolean) emailInfo.get("primary");
                        Boolean isVerified = (Boolean) emailInfo.get("verified");
                        if (Boolean.TRUE.equals(isPrimary) && Boolean.TRUE.equals(isVerified)) {
                            email = (String) emailInfo.get("email");
                            break;
                        }
                    }
                    // 如果找不到主邮箱,取第一个已验证的邮箱
                    if (email == null) {
                        email = (String) response.getBody().stream()
                                .filter(info -> Boolean.TRUE.equals(info.get("verified")))
                                .findFirst()
                                .map(info -> info.get("email"))
                                .orElse(null);
                    }
                }
            } catch (Exception e) {
                log.error("拉取GitHub邮箱失败: {}", e.getMessage());
            }
        } else {
            // 其他OAuth2提供商(如Google)直接从属性中获取邮箱
            email = oAuth2User.getAttribute("email");
        }

        log.info("最终获取到的用户邮箱: {}", email);

        // 你的原有业务逻辑,现在email已赋值
        Optional<UserConnectedAccount> userConnectedAccount = userConnectedAccountRepository.findByProviderIdAndProvider(providerId, provider);
        if (userConnectedAccount.isEmpty()) {
            userRepository.findUserByEmail(email)
                    .ifPresentOrElse(user -> connectAccount(providerId, provider, user),
                            () -> createUser(providerId, provider, oAuth2User, email) // 注意修改createUser方法接收email参数
                    );
        }

        // 注意:这里主键字段不要传"email"(原属性中没有),用GitHub用户的唯一标识"id",且不要传null给SimpleGrantedAuthority
        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                oAuth2User.getAttributes(),
                "id"
        );
    }

    // 重写createUser方法,传入email给AppUser赋值
    public void createUser(String providerId, String provider, OAuth2User oAuth2User, String email) {
        AppUser appUser = new AppUser();
        appUser.setEmail(email);
        appUser.setName(oAuth2User.getAttribute("name"));
        // 其他必要属性赋值
        appUser = userRepository.save(appUser);
        connectAccount(providerId, provider, appUser);
    }

    // 原有connectAccount方法保持不变
    private void connectAccount(String providerId, String provider, AppUser appUser) {
        UserConnectedAccount newUserConnectedAccount = new UserConnectedAccount(providerId, provider, appUser);
        userConnectedAccountRepository.save(newUserConnectedAccount);
    }
}

3. 额外注意事项

  • 你原来的DefaultOAuth2User构造函数第三个参数传的是"email",但oAuth2User的属性里没有这个字段,会导致认证失败,改成GitHub用户的唯一标识"id"即可。
  • 不要给SimpleGrantedAuthority传null,至少配置一个默认角色(如ROLE_USER),否则后续权限验证会出现异常。
  • 如果项目是Spring WebFlux环境,建议用WebClient替代RestTemplate,代码逻辑类似,只需要把RestTemplate的调用换成WebClient的异步调用+block()即可。

验证效果

重新启动应用后,完成GitHub授权,查看日志里的获取到的用户邮箱输出,就能看到正确的邮箱值了,后续的业务逻辑也能正常使用邮箱进行关联或创建用户操作。

备注:内容来源于stack exchange,提问作者Osteen Omega

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 11:03:08