如何为Google Cloud Tasks的HTTP POST请求添加安全防护?
解决Google Cloud Tasks触发HTTP函数的安全验证问题
我在应用中用Google Cloud Tasks做事件调度,到点触发任务后会更新UI状态,但发现一个严重的安全问题:只要有人知道HTTP请求的格式,就能随便调用我的云函数schedulerCallback,进而删除Firestore里的events文档——完全没有任何验证机制。
原创建任务代码
const task = { name: `${taskName}/myTask-${snapshot.id}-${expireSeconds}`, httpRequest: { httpMethod: 'POST', url: `https://${location}-${project}.cloudfunctions.net/schedulerCallback?eventId=${snapshot.id}`, body: Buffer.from(JSON.stringify(payload)).toString('base64'), headers: { 'Content-Type': 'application/json' } }, scheduleTime: { seconds: expirationAtSeconds } }; await tasksClient.createTask({ parent: queuePath, task });
原HTTP触发器代码
export const schedulerCallback = functions.https.onRequest(async (req, res) => { const eventId = req.query.eventId as string; try { await admin.firestore().collection("events").doc(eventId).delete(); } catch(e) { console.log(e); res.status(500).send(e); } });
找了很久都没找到针对性的方案,Stack Overflow上的相关问题也不沾边,最后研究出了几个可行的解决办法,分享给大家:
推荐方案:用OIDC令牌验证请求身份
这是Google官方推荐的方式,让Cloud Tasks在发起请求时携带OIDC ID令牌,云函数验证令牌的合法性,确保请求确实来自Cloud Tasks。
步骤1:修改任务创建代码,添加OIDC令牌配置
创建任务时,在httpRequest里加上oidcToken字段,指定用来签名令牌的服务账号(需要有调用云函数的权限):
const task = { name: `${taskName}/myTask-${snapshot.id}-${expireSeconds}`, httpRequest: { httpMethod: 'POST', url: `https://${location}-${project}.cloudfunctions.net/schedulerCallback?eventId=${snapshot.id}`, body: Buffer.from(JSON.stringify(payload)).toString('base64'), headers: { 'Content-Type': 'application/json' }, // 添加OIDC令牌配置 oidcToken: { serviceAccountEmail: 'your-service-account@your-project.iam.gserviceaccount.com', audience: `https://${location}-${project}.cloudfunctions.net/schedulerCallback` } }, scheduleTime: { seconds: expirationAtSeconds } }; await tasksClient.createTask({ parent: queuePath, task });
注意:audience必须和云函数的URL完全一致,不能带查询参数。
步骤2:在云函数中验证OIDC令牌
在schedulerCallback函数里,先提取请求头里的Authorization令牌,然后验证其合法性:
import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; import { OAuth2Client } from 'google-auth-library'; const authClient = new OAuth2Client(); export const schedulerCallback = functions.https.onRequest(async (req, res) => { // 1. 提取Authorization令牌 const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).send('Unauthorized: Missing or invalid token'); } const idToken = authHeader.split(' ')[1]; try { // 2. 验证令牌 const ticket = await authClient.verifyIdToken({ idToken, audience: `https://${process.env.LOCATION}-${process.env.PROJECT_ID}.cloudfunctions.net/schedulerCallback` }); const payload = ticket.getPayload(); // 3. 验证令牌的签发者是否是指定的服务账号 if (!payload || !payload.email_verified || !payload.email?.endsWith('@your-project.iam.gserviceaccount.com')) { return res.status(403).send('Forbidden: Invalid service account'); } // 4. 执行原有业务逻辑 const eventId = req.query.eventId as string; await admin.firestore().collection("events").doc(eventId).delete(); res.status(200).send('Event deleted successfully'); } catch (e) { console.error('Token verification failed:', e); res.status(403).send('Forbidden: Invalid token'); } });
备选方案:自定义签名验证
如果不想用OIDC,也可以在创建任务时给payload添加一个签名,函数里验证签名是否正确:
步骤1:创建任务时添加签名
import * as crypto from 'crypto'; // 密钥存在环境变量中,绝对不能硬编码 const SIGNING_SECRET = process.env.SIGNING_SECRET; // 生成签名,用payload、eventId、过期时间作为签名依据 const signature = crypto .createHmac('sha256', SIGNING_SECRET) .update(JSON.stringify(payload) + snapshot.id + expireSeconds) .digest('hex'); const task = { name: `${taskName}/myTask-${snapshot.id}-${expireSeconds}`, httpRequest: { httpMethod: 'POST', url: `https://${location}-${project}.cloudfunctions.net/schedulerCallback?eventId=${snapshot.id}&signature=${signature}`, body: Buffer.from(JSON.stringify(payload)).toString('base64'), headers: { 'Content-Type': 'application/json' } }, scheduleTime: { seconds: expirationAtSeconds } }; await tasksClient.createTask({ parent: queuePath, task });
步骤2:函数里验证签名
import * as crypto from 'crypto'; const SIGNING_SECRET = process.env.SIGNING_SECRET; export const schedulerCallback = functions.https.onRequest(async (req, res) => { const eventId = req.query.eventId as string; const receivedSignature = req.query.signature as string; const payload = JSON.parse(Buffer.from(req.body, 'base64').toString()); // 重新计算签名,确保和创建时的参数完全一致 const expectedSignature = crypto .createHmac('sha256', SIGNING_SECRET) .update(JSON.stringify(payload) + eventId + expireSeconds) .digest('hex'); if (receivedSignature !== expectedSignature) { return res.status(403).send('Forbidden: Invalid signature'); } // 执行业务逻辑 try { await admin.firestore().collection("events").doc(eventId).delete(); res.status(200).send('Event deleted successfully'); } catch(e) { console.log(e); res.status(500).send(e); } });
注意事项
- OIDC方案是最安全且维护成本最低的,推荐优先使用。
- 自定义签名的密钥一定要存在环境变量里,绝对不能硬编码到代码中。
- 确保服务账号有足够的权限:创建任务的账号需要
cloudtasks.tasks.create权限,OIDC里的服务账号需要cloudfunctions.invoker权限。
内容的提问来源于stack exchange,提问作者Muhammad Adnan
相关产品推荐
相关产品推荐

