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

基于Google APIs的JS环境下通过google.accounts.oauth2刷新AccessToken

Google OAuth2令牌使用相关疑问

依据Google OAuth2 Web令牌模型开发指南所述:

令牌过期
设计上,access token的生命周期较短。若在用户会话结束前access token过期,需通过用户驱动事件(如按钮点击)调用requestAccessToken()获取新令牌。

这一指引倾向于避免使用refresh token,由此引出问题0:何时应使用refresh token?

我的Web应用中用户登录时长远超access token的1小时有效期,为按需持续获取新access token,编写了相应代码,调用方式如下:

const provider = new GoogleAccessTokenProvider(clientId)
const scopes = ["https://www.googleapis.com/auth/drive.metadata.readonly"]
provider.getTokenWithScopes(scopes)
    .then(token => {
        console.log(token)
    }).catch(e => {
        console.log(e)
    })

代码中包含不少繁琐逻辑,因此提出以下技术疑问:
问题1:将google.accounts.oauth2返回的令牌响应存储在localStorage中,是否符合Google的预期规范?
问题2:代码以10秒缓冲期管理令牌过期,这是否需要开发者自行处理?是否有自动处理该逻辑的库?
问题3:当access token已过期但包含所需权限范围时,调用requestAccessToken()并设置prompt为"none",会短暂弹出Google授权对话框后消失,体验不佳,这是否符合Google的预期操作?此时使用refresh token是否更合适?
问题4:因权限范围(scopes)与调用绑定,每次调用requestAccessToken()时都需调用initTokenClient,这是否符合Google的设计意图?

代码实现如下:

class LocalStorage {
    getItem(key: string): string | null {
        return window.localStorage.getItem(key)
    }

    setItem(key: string, value: string): void {
        return window.localStorage.setItem(key, value)
    }
}

class TokenResponseStorage {
    constructor(private readonly localStorage = new LocalStorage()) {
    }

    getItem(key: string): WrappedGoogleAccessTokenResponse | null {
        const value = this.localStorage.getItem(key)
        if (value === null) {
            return null
        }
        return JSON.parse(value)
    }

    setItem(key: string, value: WrappedGoogleAccessTokenResponse): void {
        this.localStorage.setItem(key, JSON.stringify(value))
    }
}

interface WrappedGoogleAccessTokenResponse {
    _type: "wrapped.google.access.token.response"
    expiresAtMillis: number
    tokenResponse: google.accounts.oauth2.TokenResponse
}

export class GoogleAccessTokenProvider {
    private readonly expiryMarginMillis = 1000 * 10 // ten seconds before expiry

    constructor(private readonly clientId: string,
                private readonly storage = new TokenResponseStorage(),
                private readonly storageKey = 'docsndata.google.access.token') {
    }

    async getTokenWithScopes(scopes: string[]): Promise<string> {
        return new Promise(resolve => {
            const wrappedToken = this.storage.getItem(this.storageKey)
            if (wrappedToken && scopes.every(s => wrappedToken.tokenResponse.scopes.includes(s))) {
                if (this._hasSufficientExpiry(wrappedToken)) {
                    resolve(wrappedToken.tokenResponse.access_token)
                } else {
                    this._promptForToken(scopes, 'none').then(resolve)
                }
            } else {
                this._promptForToken(scopes, 'consent').then(resolve)
            }
        })
    }

    private _hasSufficientExpiry(token: WrappedGoogleAccessTokenResponse): boolean {
        return token.expiresAtMillis - this.expiryMarginMillis > Date.now()
    }

    private async _promptForToken(scopes: string[], prompt: "none" | "consent"): Promise<string> {
        const that = this
        return new Promise(resolve => {
            const tokenClient = google.accounts.oauth2.initTokenClient({
                client_id: this.clientId,
                scope: scopes.join(' '),
                callback: function (tokenResponse) {
                    that._storeTokenResponse(tokenResponse)
                    resolve(tokenResponse.access_token)
                }
            })
            tokenClient.requestAccessToken({prompt})
        })
    }

    private _storeTokenResponse(tokenResponse: google.accounts.oauth2.TokenResponse) {
        const wrappedTokenResponse: WrappedGoogleAccessTokenResponse = {
            _type: "wrapped.google.access.token.response",
            expiresAtMillis: Date.now() + parseInt(tokenResponse.expires_in) * 1000,
            tokenResponse
        }
        this.storage.setItem(this.storageKey, wrappedTokenResponse)
    }
}

内容的提问来源于stack exchange,提问作者Mike Hogan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 07:25:33