如何刷新Google Identity Services返回的Access Token?React全栈场景
解决谷歌登录无刷新令牌及自动刷新Access Token问题
要解决这个问题,核心是切换到授权码流程(Authorization Code Flow with PKCE)——这是谷歌Identity Services中唯一能返回refresh_token的流程,默认的隐式授权/响应式令牌流程不会提供刷新令牌。结合你的技术栈,具体实现步骤如下:
1. 前端调整:切换到授权码流程
修改@react-oauth/google的GoogleLogin组件配置,启用授权码模式并要求离线访问权限:
import { GoogleLogin } from '@react-oauth/google'; function GoogleAuthComponent() { const handleLoginSuccess = async (response) => { // 拿到谷歌返回的授权码code const authCode = response.code; // 将code传给后端,由后端交换令牌 const res = await fetch('/api/auth/google/exchange-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: authCode }) }); const authData = await res.json(); // 存储后端返回的应用自有令牌(比如JWT) localStorage.setItem('app-token', authData.token); }; return ( <GoogleLogin clientId="你的谷歌客户端ID" responseType="code" // 启用授权码流程 accessType="offline" // 要求返回refresh_token prompt="select_account" // 可选:强制用户选择账号,避免静默授权跳过refresh_token返回 onSuccess={handleLoginSuccess} onError={() => console.error('谷歌登录失败')} /> ); }
关键参数说明:
responseType="code":告诉谷歌返回授权码而非直接返回access_tokenaccessType="offline":明确要求返回refresh_token,用于离线刷新令牌
2. 后端处理:交换授权码并存储refresh_token
基于Spring Webflux + Spring Security OAuth2 Client实现授权码交换,同时用Hibernate-Reactive存储refresh_token关联用户:
第一步:配置OAuth2客户端
在application.yml中添加谷歌客户端配置:
spring: security: oauth2: client: registration: google: client-id: 你的谷歌客户端ID client-secret: 你的谷歌客户端密钥 redirect-uri: "{baseUrl}/api/auth/google/exchange-code" # 与前端请求的后端端点一致 scope: openid,email,profile authorization-grant-type: authorization_code provider: google: authorization-uri: https://accounts.google.com/o/oauth2/auth token-uri: https://oauth2.googleapis.com/token user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo user-name-attribute: sub
第二步:编写授权码交换控制器
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import java.util.Map; @RestController public class GoogleAuthController { private final ReactiveOAuth2AuthorizedClientService authorizedClientService; private final ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter; private final UserRepository userRepository; // 你的Hibernate-Reactive用户仓库 public GoogleAuthController(ReactiveClientRegistrationRepository clientRepo, ReactiveOAuth2AuthorizedClientService authorizedClientService, UserRepository userRepository) { this.authorizedClientService = authorizedClientService; this.oauth2Filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRepo, authorizedClientService); this.userRepository = userRepository; } @PostMapping("/api/auth/google/exchange-code") public Mono<Map<String, String>> exchangeCode(@RequestBody Map<String, String> request) { String code = request.get("code"); // 用授权码交换谷歌令牌(包含access_token和refresh_token) return oauth2Filter.getAuthorizedClient("google", null) .flatMap(client -> { String accessToken = client.getAccessToken().getTokenValue(); String refreshToken = client.getRefreshToken().getTokenValue(); String googleUserId = client.getPrincipalName(); // 查找或创建用户,存储refresh_token到数据库 return userRepository.findByGoogleId(googleUserId) .switchIfEmpty(Mono.defer(() -> { User newUser = new User(); newUser.setGoogleId(googleUserId); newUser.setGoogleRefreshToken(refreshToken); return userRepository.save(newUser); })) .map(user -> { // 更新用户的refresh_token(如果有新的) if (!refreshToken.equals(user.getGoogleRefreshToken())) { user.setGoogleRefreshToken(refreshToken); userRepository.save(user).subscribe(); } // 生成应用自有JWT令牌返回给前端 String appToken = generateAppJwt(user); return Map.of("token", appToken); }); }); } // 生成应用自有JWT的工具方法(自行实现) private String generateAppJwt(User user) { // 示例:用JJWT等库生成包含用户信息的JWT return "your-app-jwt-token"; } }
3. 自动刷新Access Token逻辑
后端维护refresh_token后,当谷歌的access_token过期时,自动用refresh_token换取新令牌,对前端完全透明:
// 在需要调用谷歌API的服务类中添加刷新逻辑 private Mono<String> getValidGoogleAccessToken(User user) { String storedRefreshToken = user.getGoogleRefreshToken(); return oauth2Filter.getClientRegistration("google") .flatMap(registration -> { // 调用谷歌令牌刷新接口 return oauth2Filter.getWebClient() .post() .uri(registration.getProviderDetails().getTokenUri()) .bodyValue(Map.of( "grant_type", "refresh_token", "refresh_token", storedRefreshToken, "client_id", registration.getClientId(), "client_secret", registration.getClientSecret() )) .retrieve() .bodyToMono(Map.class) .map(response -> { String newAccessToken = (String) response.get("access_token"); String newRefreshToken = (String) response.get("refresh_token"); // 如果谷歌返回了新的refresh_token,更新数据库 if (newRefreshToken != null) { user.setGoogleRefreshToken(newRefreshToken); userRepository.save(user).subscribe(); } return newAccessToken; }); }); }
关键注意事项
- 谷歌控制台配置:确保你的OAuth客户端ID是"Web应用"类型,且重定向URI包含后端的
/api/auth/google/exchange-code端点。 - refresh_token返回规则:谷歌仅在用户首次授权、或授权时触发
prompt=consent时返回refresh_token;如果用户已授权过,需让用户重新授权才能获取新的refresh_token。 - 敏感信息存储:
refresh_token属于敏感数据,存储时需加密,避免明文存储。
内容的提问来源于stack exchange,提问作者Petar Dimitrov
相关产品推荐
相关产品推荐

