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

如何刷新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_token
  • accessType="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;
                        });
            });
}

关键注意事项

  1. 谷歌控制台配置:确保你的OAuth客户端ID是"Web应用"类型,且重定向URI包含后端的/api/auth/google/exchange-code端点。
  2. refresh_token返回规则:谷歌仅在用户首次授权、或授权时触发prompt=consent时返回refresh_token;如果用户已授权过,需让用户重新授权才能获取新的refresh_token。
  3. 敏感信息存储:refresh_token属于敏感数据,存储时需加密,避免明文存储。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 07:10:24