Spring Boot 3集成GitHub OAuth2登录时邮箱属性始终返回Null的问题求助
Spring Boot 3集成GitHub OAuth2登录时邮箱属性始终返回Null的问题求助
Hey 兄弟,我之前在集成GitHub OAuth2的时候也踩过一模一样的坑!咱们先把问题的根因理清楚,再给你具体的解决方案:
为什么邮箱始终返回Null?
这其实是GitHub OAuth2的一个容易被误解的细节,结合你的情况,主要有这几个原因:
- 你配置的
user:emailscope,并不是让GitHub默认的用户信息端点(https://api.github.com/user)直接返回邮箱,而是允许你的应用调用GitHub的/user/emailsAPI来获取用户的邮箱列表(包括公开和授权的私密邮箱)。 - 即使你的邮箱设为公开,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
相关产品推荐
相关产品推荐

