使用Firebase数据库触发器添加Google Calendar事件的OAuth2问题排查
看起来你踩了Google OAuth2和Calendar API集成的常见坑——用错了Token类型,还没处理Token过期的问题。我来一步步帮你理清:
错误根源
你日志里的No access, refresh token or API key is set或者No refresh token is set,本质是两个问题:
- 你传给云函数的Token不是Google Calendar API认可的OAuth2访问令牌(access token)
- 就算拿到了access token,它有效期只有1小时,你没有存储刷新令牌(refresh token)来自动续期
你需要哪种Token?
你需要的是带有Calendar权限的Google OAuth2 access token + refresh token:
- access token是直接调用Calendar API的临时凭证,有效期1小时
- refresh token是长期凭证,用来在access token过期时自动获取新的access token(只要用户没撤销权限就一直有效)
Android端正确获取授权凭证
你之前用的serverAuthCode/idToken/Firebase ID Token都不对,正确步骤如下:
1. 配置GoogleSignInOptions
必须请求Calendar权限、离线访问(才能拿到refresh token),并且要用Web应用的Client ID(不是Android的Client ID):
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) // 请求Calendar读写权限 .requestScopes(Scope("https://www.googleapis.com/auth/calendar")) // 填你在Google Cloud Console里创建的Web应用Client ID .requestServerAuthCode("YOUR_WEB_CLIENT_ID") // 关键:请求离线访问,才能拿到refresh token .requestOfflineAccess() .build() val googleSignInClient = GoogleSignIn.getClient(this, gso)
2. 登录后获取Server Auth Code
登录成功后拿到serverAuthCode,把这个Code传到Firebase云函数(不要在客户端直接换token,因为需要Web Client Secret,这个不能暴露在客户端):
startActivityForResult(googleSignInClient.signInIntent, RC_SIGN_IN) // 在onActivityResult里处理登录结果 val account = GoogleSignIn.getSignedInAccountFromIntent(data).result val serverAuthCode = account.serverAuthCode // 将这个code传到云函数
云函数端处理Token并调用Calendar API
1. 用Server Auth Code换Access/Refresh Token
云函数里用收到的serverAuthCode,加上你的Web Client ID和Secret,去Google的OAuth2服务器换token:
async function exchangeAuthCodeForTokens(authCode) { const oauth2Client = new google.google.auth.OAuth2( // 从Firebase环境变量读取,不要硬编码敏感信息! functions.config().google.client_id, functions.config().google.client_secret ); const { tokens } = await oauth2Client.getToken(authCode); // tokens里包含access_token、refresh_token、expiry_date等字段 return tokens; }
注意:用
firebase functions:config:set google.client_id="你的Web Client ID" google.client_secret="你的Web Client Secret"设置环境变量,避免硬编码敏感信息。
2. 存储Refresh Token
把换回来的refresh_token存在Firebase数据库的用户节点下(比如users/{userId}/calendarTokens/refreshToken),之后每次调用Calendar API都用它获取有效access token。
3. 改进云函数的Auth Client
修改你的getOauthClient函数,用refresh token自动获取并刷新access token:
async function getOauthClient(refreshToken) { const oauth2Client = new google.google.auth.OAuth2( functions.config().google.client_id, functions.config().google.client_secret ); // 设置refresh token oauth2Client.setCredentials({ refresh_token: refreshToken }); try { // 自动刷新access token(如果已过期) const { credentials } = await oauth2Client.refreshAccessToken(); oauth2Client.setCredentials(credentials); // 可选:把新的access token存回数据库,减少下次刷新操作 } catch (err) { console.error("刷新Access Token失败:", err); throw err; } return oauth2Client; }
4. 修改addEvent函数逻辑
现在云函数应该先从数据库取用户的refresh token,再创建Auth Client:
export const addEvent = functions.database .ref('users/{user}/calendarEvents/{eventId}') .onCreate(async (snapshot, context) => { const data = snapshot.val(); const userId = context.params.user; // 从数据库获取用户的refresh token const tokenSnapshot = await admin.database() .ref(`users/${userId}/calendarTokens/refreshToken`) .once('value'); const refreshToken = tokenSnapshot.val(); if (!refreshToken) { throw new Error("用户未授权Calendar访问"); } const authClient = await getOauthClient(refreshToken); const calendar = google.google.calendar('v3'); try { await calendar.events.insert({ auth: authClient, calendarId: 'primary', resource: getCalendarEvent(data), }); // 成功后删除触发节点 await snapshot.ref.remove(); return; } catch (err) { console.error("添加Calendar事件失败:", err); throw err; } });
为什么之前的Token没用?
serverAuthCode:这是授权码,不是直接能用的access token,必须用它去换真正的tokenidToken:这是Google身份验证Token,用来验证用户身份,不能访问Calendar APIFirebaseAuth.getIdToken():这是Firebase的身份Token,只适用于Firebase相关服务,和Google Calendar API无关
额外注意事项
- 确保在Google Cloud Console里启用了Google Calendar API
- 配置OAuth同意屏幕,添加
https://www.googleapis.com/auth/calendar权限,测试阶段可以添加测试用户 - 第一次授权才能拿到refresh token,之后用户重新登录不会返回,所以一定要保存好
- 如果用户撤销了权限,refresh token会失效,需要引导用户重新授权
内容的提问来源于stack exchange,提问作者evgen shein

