基于Node与Google OAuth2的API认证方案及令牌刷新咨询
Google OAuth2 API认证:受保护路由授权与令牌刷新问题
概述
我的API需要代表用户访问Google服务,已设置access_type: 'offline'以获取refresh token,使用googleapis库中的oAuth2Client实现认证流程。
现有认证流程
客户端请求授权URL
API端生成授权URL的函数:getAuthUrl() { try { const scopes = [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', 'openid', 'https://www.googleapis.com/auth/youtube.readonly', ]; const authUrl = oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes, include_granted_scopes: true, prompt: 'consent', }); return authUrl; } catch (e) { throw new AuthServiceError((e as Error)?.message); } }客户端换取令牌
客户端获取authorizationCode后,调用API换取令牌:async getTokens(authorizationCode: string) { try { const { tokens } = await oAuth2Client.getToken(authorizationCode); oAuth2Client.setCredentials(tokens); const { credentials } = oAuth2Client; const idToken = credentials.id_token; const refreshToken = credentials.refresh_token; if (!idToken) { throw new Error(ErrorMessage.TOKENS_MISSING_ID_TOKEN); } if (!refreshToken) { throw new Error(ErrorMessage.TOKENS_MISSING_REFRESH_TOKEN); } return { idToken, refreshToken, }; } catch (e) { throw new AuthServiceError((e as Error)?.message); } },API端处理用户创建与令牌返回
API控制器通过令牌获取用户信息,创建/查找用户后返回Google的idToken:async googleTokens(req, res, next) { try { const { authorizationCode } = req.body; const { idToken, refreshToken } = await googleAuthService.getTokens(authorizationCode); const { userId } = await googleAuthService.getUserInfo(refreshToken); // 在数据库中查找或创建该userId对应的用户,返回用户数据 const userData = await db.user.findOrCreate(userId); res.send({ data: { user: { ...userData, idToken, }, }, }); } catch (e) { next(e); } },客户端访问受保护路由
客户端将Google的idToken放在请求头Authorization: Bearer <idToken>中,访问受保护内容。服务端验证令牌
服务端中间件使用google.oauth.OAuth2的verifyIdToken方法验证请求头中的idToken有效性,通过则允许访问。
问题
- 步骤2中换取的Google idToken会在1小时后过期,推荐的刷新方式是什么?应该在服务端还是客户端处理?
- 当前策略是否能正确实现基于Google OAuth2的API认证?如果不能,结合以下需求如何调整:
- 代表用户离线访问Google服务
- 在数据库中创建用户账户,并授权API请求访问受保护内容
解决方案与调整建议
1. 令牌刷新方案:服务端处理,禁止客户端接触refresh token
- 核心原则:Google的refresh token是长期有效且高风险的凭证,必须仅在服务端存储,绝对不能暴露给客户端。
- 具体流程:
- 首次获取refresh token后,立即将其与用户记录绑定,存入数据库(比如在用户表新增
google_refresh_token字段)。 - 当客户端的idToken过期时,客户端向服务端发起专门的刷新请求(如
POST /refresh-google-token),请求中携带当前的用户标识(或服务端自行签发的会话凭证)。 - 服务端验证请求合法性后,从数据库取出对应用户的refresh token,调用
oAuth2Client.refreshAccessToken()获取新的idToken和accessToken。 - 服务端将新的idToken返回给客户端,客户端更新本地存储即可。
- 首次获取refresh token后,立即将其与用户记录绑定,存入数据库(比如在用户表新增
2. 现有认证策略的问题与调整方案
当前策略存在两个核心问题:一是直接返回Google idToken给客户端用于接口授权,缺乏自主会话管理;二是未持久化存储refresh token,无法支持离线访问Google服务。结合需求调整如下:
(1)重构授权凭证:使用服务端自定义JWT替代Google idToken
- 服务端在完成用户创建后,自行签发一个自定义JWT(而非返回Google的idToken)给客户端。自定义JWT包含用户ID等必要信息,用服务端私钥签名,设置较短的过期时间(如15分钟)。
- 客户端访问受保护路由时,携带该自定义JWT,服务端中间件验证JWT的签名与有效性即可,无需再调用Google的验证接口,提升性能。
(2)持久化存储Google refresh token,支持离线访问
- 在步骤3的
googleTokens控制器中,将refresh token存入用户记录:// 替换原findOrCreate逻辑,新增/更新用户的Google refresh token const [user, created] = await db.user.findOrCreate({ where: { userId }, defaults: { googleRefreshToken: refreshToken } }); if (!created) { await db.user.update({ googleRefreshToken: refreshToken }, { where: { userId } }); } - 当需要代表用户调用Google API时,从数据库取出refresh token,初始化
oAuth2Client并刷新accessToken:async callGoogleApi(userId) { const user = await db.user.findByPk(userId); const oAuthClient = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); oAuthClient.setCredentials({ refresh_token: user.googleRefreshToken }); // 自动刷新过期的accessToken const { credentials } = await oAuthClient.refreshAccessToken(); oAuthClient.setCredentials(credentials); // 调用Google API,比如YouTube const youtube = google.youtube({ version: 'v3', auth: oAuthClient }); const response = await youtube.channels.list({ mine: true, part: 'snippet' }); return response.data; }
(3)优化令牌刷新流程
- 客户端的自定义JWT过期后,调用服务端的
POST /refresh-session接口,携带过期的JWT。 - 服务端验证JWT的合法性(即使过期,只要签名有效即可),重新签发新的自定义JWT返回给客户端,维持会话有效性。
内容的提问来源于stack exchange,提问作者Bruno Kawka
相关产品推荐
相关产品推荐

