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

授权码流程下Google Drive Picker重复请求授权的问题解决

解决Google Drive Picker授权码流程下重复登录授权问题

问题描述

我基于授权码流程实现了Google Drive Picker API,Picker可正常启动并完成文件选择,但存在问题:即使在同一会话内持有有效的Access Token,每次选择新文件时仍会要求用户登录并授权。


客户端实现

function initClient() {
  client = google.accounts.oauth2.initCodeClient({
    client_id: CLIENT_ID,
    scope: SCOPES,
    ux_mode: "popup",
    callback: async (response) => {
      var code_receiver_uri = "http://localhost:5000/oauth2callback";
      try {
        const { code, scope } = response;
        const fetchResponse = await fetch(code_receiver_uri, {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
          body: `code=${code}&scope=${scope}`,
        });
        result = await fetchResponse.json();
        accessToken = result.token;
        refreshToken = result.refresh_token;
        // 将认证信息持久化到localStorage
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', refreshToken);
        localStorage.setItem('tokenExpiry', result.expiry);
        
        document.getElementById("signout_button").style.visibility = "visible";
        document.getElementById("authorize_button").innerText = "Refresh";
        await createPicker();
      } catch (error) {
        console.error("Error making request: ", error);
      }
    },
  });
  gisInited = true;
  // 页面加载时从localStorage恢复认证信息
  accessToken = localStorage.getItem('accessToken');
  refreshToken = localStorage.getItem('refreshToken');
  maybeEnableButtons();
}

function gapiLoaded() {
  gapi.load("client:picker", initializePicker);
}

async function initializePicker() {
  await gapi.client.load(
    "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"
  );
  pickerInited = true;
  maybeEnableButtons();
}

function maybeEnableButtons() {
  if (pickerInited && gisInited) {
    document.getElementById("authorize_button").style.visibility = "visible";
  }
}

function createPicker() {
  const picker = new google.picker.PickerBuilder()
    .enableFeature(google.picker.Feature.NAV_HIDDEN)
    .enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
    .setDeveloperKey(API_KEY)
    .setAppId(APP_ID)
    .setOAuthToken(accessToken)
    .addView(new google.picker.DocsView()
      .setIncludeFolders(true)
      .setMimeTypes('application/pdf')
      .setOwnedByMe(true))
    .setCallback(pickerCallback)
    .build();
  picker.setVisible(true);
}

async function getAccessToken() {
  if (!accessToken || isAccessTokenExpired()) {
    // 使用刷新令牌获取新的访问令牌
    accessToken = await refreshAccessToken();
  }
  return accessToken;
}

// 修正:浏览器环境无法使用Node.js的fs模块,改用localStorage存储的过期时间
function isAccessTokenExpired() {
  const accessTokenExpirationTime = localStorage.getItem('tokenExpiry');
  if (!accessTokenExpirationTime) return true;
  
  const currentTimestamp = Math.floor(Date.now() / 1000);
  return parseInt(accessTokenExpirationTime) <= currentTimestamp;
}

async function refreshAccessToken() {
  const response = await fetch("http://localhost:5000/get-token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: `refresh_token=${refreshToken}`,
  });

  const result = await response.json();
  // 更新localStorage中的令牌信息
  localStorage.setItem('accessToken', result.token);
  localStorage.setItem('tokenExpiry', result.expiry);
  return result.token;
}

async function pickerCallback(data) {
  console.log("Here in picker callback!");
  if (data.action === google.picker.Action.PICKED) {
    const documents = data[google.picker.Response.DOCUMENTS];
    const documentPayload = documents.map((document) => ({
      fileId: document.id,
      fileName: document.name,
    }));

    const download_endpoint = "http://localhost:5000/gdrive/download";
    const response = await fetch(download_endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(documentPayload),
    });

    const result = await response.json();
    console.log(result);
  }
}

function handleAuthClick() {
  // 修正:如果令牌有效,直接打开Picker;无效再触发授权/刷新
  if (accessToken && !isAccessTokenExpired()) {
    createPicker();
  } else {
    client.requestCode();
  }
}
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
<script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>

服务端实现

import os
from flask import Flask, request, render_template
from flask_cors import CORS
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request

app = Flask(__name__)
CORS(app)
app.secret_key = "c8a8fe7b5ace3e7c662c202ce4910834"

CREDENTIALS_FILE = "../credentials.json"
CLIENT_SECRET_FILE = "client_secret.json"

@app.route("/oauth2callback", methods=["POST"])
def oauth_endpoint():
    authorization_code = request.form.get("code")
    scopes = request.form.get("scope").split()

    # 修正:收到新授权码时,必须用code获取凭证,而不是直接加载旧文件
    flow = Flow.from_client_secrets_file(
        CLIENT_SECRET_FILE,
        scopes=scopes,
        redirect_uri="postmessage"
    )
    flow.fetch_token(code=authorization_code)
    credentials = flow.credentials

    # 保存凭证到文件
    with open(CREDENTIALS_FILE, "w") as creds:
        creds.write(credentials.to_json())

    return {
        "token": credentials.token,
        "refresh_token": credentials.refresh_token,
        "token_uri": credentials.token_uri,
        "client_id": credentials.client_id,
        "client_secret": credentials.client_secret,
        "scopes": credentials.scopes,
        "expiry": credentials.expiry.timestamp()  # 返回过期时间戳给前端
    }

def retrieve_refresh_token():
    if os.path.exists(CREDENTIALS_FILE):
        credentials = Credentials.from_authorized_user_file(CREDENTIALS_FILE)
        return credentials.refresh_token
    return None

@app.route("/get-token", methods=['POST'])
def refresh_access_token():
    refresh_token = retrieve_refresh_token()
    if not refresh_token:
        return {"error": "No refresh token found"}, 400
    
    # 修正:正确构建Credentials对象
    credentials = Credentials.from_authorized_user_file(
        CREDENTIALS_FILE,
        refresh_token=refresh_token
    )
    if credentials.expired:
        credentials.refresh(Request())
        # 保存刷新后的凭证
        with open(CREDENTIALS_FILE, "w") as creds:
            creds.write(credentials.to_json())
    
    return {
        "token": credentials.token,
        "expiry": credentials.expiry.timestamp()
    }

def download(file_id, file_name):
    credentials = Credentials.from_authorized_user_file(CREDENTIALS_FILE)
    service = build("drive", "v3", credentials=credentials)
    media_request = service.files().get_media(fileId=file_id)
    download_path = os.path.join("../downloads/", file_name)
    
    with open(download_path, "wb") as file:
        downloader = MediaIoBaseDownload(file, media_request)
        done = False
        while not done:
            status, done = downloader.next_chunk()
            print(f"Download {int(status.progress() * 100)}%.")

@app.route("/gdrive/download", methods=["POST"])
def gdrive_download():
    request_files = request.get_json()
    failed_files = []
    successful_files = []

    for file in request_files:
        file_id = file.get("fileId")
        file_name = file.get("fileName")
        try:
            download(file_id, file_name)
            successful_files.append(file_name)
        except Exception as e:
            print(e)
            failed_files.append(file_name)
    
    return {"successful": successful_files, "failed": failed_files}, 201

@app.route("/")
def home():
    return render_template('implicitindex.html')

if __name__ == "__main__":
    app.run(port=5000, debug=True)

关键修复点说明

  1. 前端持久化认证信息

    • 将accessToken、refreshToken、tokenExpiry存储到localStorage,页面刷新后无需重新授权
    • 移除浏览器环境无法运行的Node.js fs模块调用,改用前端存储的过期时间校验令牌有效性
  2. 优化授权触发逻辑

    • 修改handleAuthClick函数:如果当前令牌有效,直接打开Picker;仅当令牌无效时才触发授权流程或刷新令牌
  3. 修正服务端凭证处理逻辑

    • 收到新授权码时,必须用该码获取新凭证,而非直接加载旧文件,确保最新的认证信息被保存
    • 刷新令牌后返回过期时间戳,让前端能正确校验令牌有效性
  4. 修复令牌刷新逻辑

    • 服务端刷新令牌后重新保存凭证文件,确保后续请求使用最新令牌
    • 前端刷新令牌后同步更新localStorage中的信息

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 14:57:32