基于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
相关产品推荐
相关产品推荐

