授权码流程下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)
关键修复点说明
前端持久化认证信息
- 将
accessToken、refreshToken、tokenExpiry存储到localStorage,页面刷新后无需重新授权 - 移除浏览器环境无法运行的Node.js
fs模块调用,改用前端存储的过期时间校验令牌有效性
- 将
优化授权触发逻辑
- 修改
handleAuthClick函数:如果当前令牌有效,直接打开Picker;仅当令牌无效时才触发授权流程或刷新令牌
- 修改
修正服务端凭证处理逻辑
- 收到新授权码时,必须用该码获取新凭证,而非直接加载旧文件,确保最新的认证信息被保存
- 刷新令牌后返回过期时间戳,让前端能正确校验令牌有效性
修复令牌刷新逻辑
- 服务端刷新令牌后重新保存凭证文件,确保后续请求使用最新令牌
- 前端刷新令牌后同步更新
localStorage中的信息
内容的提问来源于stack exchange,提问作者Ashok Budha
相关产品推荐
相关产品推荐

