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

非本地主机下如何获取Google API refresh_token以实现文件上传

问题描述

本地测试(http://localhost:3000/)时,已成功配置refresh_token和access_token生成,表单附件可正常上传至Google Drive,相关代码如下:

import formidable from 'formidable';
import fs from 'fs';
import { google } from 'googleapis'
const stream = require('stream');

const CLIENT_ID = process.env.CLIENT_ID
const CLIENT_SECRET = process.env.CLIENT_SECRET
const REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
let REFRESH_TOKEN = process.env.REFRESH_TOKEN
let ACCESS_TOKEN = process.env.ACCESS_TOKEN

let bodyContent = `client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}`;

const getTokens = () => {
    fetch("https://oauth2.googleapis.com/token", {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: bodyContent,
    })
        .then(response => {
            if (response.ok) {
                console.log(response)
                return response.json();
            } else {
                throw new Error('Ошибка при выполнении запроса');
            }
        })
        .then(data => {
            console.log(data.access_token)
            ACCESS_TOKEN = data.access_token;
        })
        .catch(error => {
            console.error('Произошла ошибка:', error);
        });
}

getTokens()

const auth = new google.auth.OAuth2({
    CLIENT_ID,
    CLIENT_SECRET,
    REDIRECT_URI,
})

auth.setCredentials({ refresh_token: REFRESH_TOKEN, access_token: ACCESS_TOKEN })

const drive = google.drive({ version: 'v3', auth });

async function uploadFiles(file) {
    const fileContents = fs.readFileSync(`public/${file.name}`)
    const bufferStream = new stream.PassThrough()
    bufferStream.end(fileContents)

    try {
        const response = await drive.files.create({
            media: {
                mimeType: 'application/pdf',
                body: bufferStream
            },
            requestBody: {
                name: file.name,
                parents: ["1WZj-6PV6sschMMBUFZax-O8ImCqTE7uC"]
            },
            fields: 'id,name'

        })
        console.log(response.data)
        return response.data.id;

    } catch (error) {
        console.error(error);
    }
}

async function sendDataToGoogleSheets(body, url) {
    const data = JSON.parse(body.formValues);

    const sheets = google.sheets({
        version: 'v4', auth
    })

    const urlLink = url ? `https://drive.google.com/file/d/${url}/view` : "Нет приложенного файла"
    try {
        const response = await sheets.spreadsheets.values.append({
            spreadsheetId: "1AR32ZlUSU03QHPwRV64LyPmwhIlH5vGmLg60Ouq5mhI",
            range: "A1:F1",
            valueInputOption: "USER_ENTERED",
            requestBody: {
                values: [
                    [data.name, data.email, data.phoneNumber, data.comment, data.resumeLink, urlLink]
                ]
            }
        })

        return response.data

    } catch (error) {
        console.log(error.message)
    }
}

const post = async (req, res) => {
    const form = new formidable.IncomingForm();

    form.parse(req, async function (err, fields, files) {
        let file = files ? files.file : null;
        try {
            if (file) {
                await saveFile(file).catch(console.error);
                const url = await uploadFiles(file).catch(console.error);
                await sendDataToGoogleSheets(fields, url).catch(console.error);

                await removeFile(file.name).catch(console.error);
            } else {
                await sendDataToGoogleSheets(fields).catch(console.error);
            }
        } catch (error) {
            console.log('Failed to do shit', error.message);
            res.status(500).end('Unexpected error');
        }
        if (err) {
            res.status(err.statusCode || 500).send(err.message);
        } else {
            res.status(201).send('');
        }
    });
};

const saveFile = async (file) => {
    const data = fs.readFileSync(file.path);
    fs.writeFileSync(`./public/${file.name}`, data, console.log('file saved'));
    await fs.unlinkSync(file.path);
    return;
};

const removeFile = async (fileName) => {
    await fs.unlink(`./public/${fileName}`, () => {
        console.log(`file ${fileName} removed`);
    });
    return;
};

const reqRes = (req, res) => {
    req.method === 'POST'
        ? post(req, res)
        : req.method === 'PUT'
            ? console.log('PUT')
            : req.method === 'DELETE'
                ? console.log('DELETE')
                : req.method === 'GET'
                    ? console.log('GET')
                    : res.status(404).send('');
};
export default reqRes;
export { config };

部署到Vercel后,文件无法上传至Google Drive,Google表格可正常填充但无文件链接。尝试为Vercel域名生成新的refresh_token和access_token,但仅能获取到access_token:

{ "access_token": "ХХХХХ", "expires_in": 3599, "scope": "https://www.googleapis.com/auth/drive", "token_type": "Bearer" }

代码中需要同时设置refresh_token和access_token才能上传文件(auth.setCredentials({ refresh_token: REFRESH_TOKEN, access_token: ACCESS_TOKEN })),但Vercel环境下无法获取refresh_token,请问如何正确获取所需令牌以支持非本地主机使用?


解决方法

1. 核心:授权请求必须带access_type=offline参数

Google OAuth只有在授权阶段指定access_type=offline时,才会返回长期有效的refresh_token。如果之前的授权没加这个参数,只会返回短期access_token,不会生成refresh_token。

2. 重新生成包含refresh_token的令牌

  • 构造授权URL,确保包含以下关键参数(替换为你的实际信息):
    https://accounts.google.com/o/oauth2/v2/auth?
    client_id=你的CLIENT_ID&
    redirect_uri=你的Vercel回调地址&
    response_type=code&
    scope=https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/spreadsheets&
    access_type=offline&
    prompt=consent
    
    其中prompt=consent会强制用户重新授权,确保即使之前授权过也会返回refresh_token。
  • 打开该URL完成授权,用返回的授权码交换令牌:
    发送POST请求到https://oauth2.googleapis.com/token,表单参数包括:
    client_id=你的CLIENT_ID
    client_secret=你的CLIENT_SECRET
    code=授权后得到的CODE
    redirect_uri=你的Vercel回调地址
    grant_type=authorization_code
    
    这次响应里就会包含refresh_token。

3. 简化代码中的令牌管理

Google官方的google-auth-library会自动处理access_token的刷新,不需要手动维护:

  • 只需传入refresh_token即可,删除手动刷新的getTokens函数:
    auth.setCredentials({ refresh_token: REFRESH_TOKEN });
    
    当access_token过期时,库会自动用refresh_token获取新的access_token。

4. 检查Vercel环境变量配置

确保Vercel后台的环境变量REFRESH_TOKEN、CLIENT_ID、CLIENT_SECRET完全正确,无拼写错误或空格。

5. 确认OAuth应用的redirect_uri配置

  • 如果是Web应用类型,必须在Google Cloud控制台中添加Vercel域名作为授权的redirect_uri(比如https://你的Vercel应用域名.vercel.app/callback);
  • 如果是桌面应用类型,可继续使用urn:ietf:wg:oauth:2.0:oob,但需确保应用类型设置正确。

内容的提问来源于stack exchange,提问作者Артем

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 19:44:56