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

如何编写仅应用可访问的Firebase云函数及无认证访问计数安全方案

我来帮你拆解这两个Firebase Cloud Functions的常见安全问题,都是实际开发中经常遇到的场景:

1. 编写仅允许自己应用访问的Firebase Cloud Function

要实现这个,核心是验证请求确实来自你的合法应用,下面是几种靠谱的方案,按推荐程度排序:

  • Firebase App Check(最推荐)
    App Check是Firebase官方提供的验证请求来源的工具,能确保请求来自你注册的iOS/Android/Web应用,甚至是Cloud Functions本身。你需要先在客户端集成App Check SDK,然后在Cloud Function里验证令牌:

    const { getAppCheck } = require("firebase-admin/app-check");
    
    exports.yourRestrictedFunction = functions.https.onRequest(async (req, res) => {
      // 从请求头获取App Check令牌
      const appCheckToken = req.headers["x-firebase-appcheck"];
      if (!appCheckToken) {
        res.status(403).send("未提供合法的应用验证令牌");
        return;
      }
    
      try {
        // 验证令牌有效性
        await getAppCheck().verifyToken(appCheckToken);
      } catch (err) {
        res.status(403).send("应用验证失败,请求来源不合法");
        return;
      }
    
      // 这里写你的函数核心逻辑
      res.status(200).send("请求已通过验证,执行成功");
    });
    

    这种方式能有效拦截第三方恶意调用,Firebase会帮你处理大部分验证细节,省心又安全。

  • Firebase Auth用户验证(适合需要登录的应用)
    如果你的应用所有功能都要求用户登录,那可以在函数里验证用户的ID令牌,确保请求来自你的应用的认证用户:

    const { getAuth } = require("firebase-admin/auth");
    
    exports.yourAuthProtectedFunction = functions.https.onRequest(async (req, res) => {
      // 从请求头获取Bearer令牌
      const idToken = req.headers.authorization?.split("Bearer ")[1];
      if (!idToken) {
        res.status(401).send("未提供身份验证令牌");
        return;
      }
    
      try {
        const decodedToken = await getAuth().verifyIdToken(idToken);
        // 可选:额外验证用户是否属于你的应用(比如检查email域名)
        if (!decodedToken.email?.endsWith("@yourdomain.com")) {
          res.status(403).send("用户无访问权限");
          return;
        }
      } catch (err) {
        res.status(403).send("身份验证失败");
        return;
      }
    
      // 函数核心逻辑
      res.status(200).send("认证通过,执行成功");
    });
    
  • 自定义API密钥(谨慎使用)
    你可以在客户端请求时带上自定义API密钥,在函数里验证密钥是否匹配。但注意:Web应用的API密钥容易被逆向获取,所以这种方式只适合非Web客户端,或作为辅助验证手段。记得把密钥存在Cloud Functions环境变量里,不要硬编码:

    exports.yourApiKeyProtectedFunction = functions.https.onRequest((req, res) => {
      const apiKey = req.headers["x-custom-api-key"];
      if (apiKey !== process.env.YOUR_SECRET_API_KEY) {
        res.status(403).send("无效的API密钥");
        return;
      }
    
      // 函数核心逻辑
      res.status(200).send("密钥验证通过,执行成功");
    });
    
2. 未认证用户点击外链时安全递增访问计数

这个场景的核心挑战是防止恶意刷量,同时因为用户未认证,不能依赖Auth令牌。以下是几个可行的方案:

方案一:中间跳转页+App Check验证(最推荐)

不要直接让用户点击外部链接,而是先跳转到你的Cloud Function(或网站中间页),完成计数后再重定向到目标网站。这样可以:

  • 用App Check验证请求来自你的合法网站,阻止第三方脚本恶意调用;
  • 避免直接暴露计数接口。

函数代码示例:

const { getAppCheck } = require("firebase-admin/app-check");
const { getFirestore } = require("firebase-admin/firestore");

exports.trackVisitAndRedirect = functions.https.onRequest(async (req, res) => {
  // 1. 验证App Check令牌
  const appCheckToken = req.headers["x-firebase-appcheck"];
  if (!appCheckToken) {
    res.status(403).send("请求来源不合法");
    return;
  }
  try {
    await getAppCheck().verifyToken(appCheckToken);
  } catch (err) {
    res.status(403).send("请求来源不合法");
    return;
  }

  // 2. 获取目标资源ID(从查询参数中获取)
  const resourceId = req.query.resourceId;
  if (!resourceId) {
    res.status(400).send("缺少资源ID");
    return;
  }

  // 3. 原子递增访问计数(避免并发更新丢失)
  const db = getFirestore();
  await db.collection("resources").doc(resourceId).update({
    visits: admin.firestore.FieldValue.increment(1)
  });

  // 4. 重定向到目标网站
  const resourceDoc = await db.collection("resources").doc(resourceId).get();
  if (resourceDoc.exists) {
    res.redirect(resourceDoc.data().url);
  } else {
    res.status(404).send("资源不存在");
  }
});

然后在你的网站上,把外部链接改成指向这个函数的地址,比如:/track-visit?resourceId=xxx

方案二:IP限流+原子计数

如果不想用中间跳转页,可以直接提供计数接口,但要加上IP限流,防止同一IP短时间内多次调用:

const rateLimit = require("express-rate-limit");
const express = require("express");
const app = express();
app.use(express.json());

// 配置限流:每个IP每分钟最多调用10次
const limiter = rateLimit({
  windowMs: 60 * 1000, // 1分钟窗口
  max: 10,
  message: "请求过于频繁,请稍后再试"
});
app.use(limiter);

app.post("/increment-visits", async (req, res) => {
  const resourceId = req.body.resourceId;
  if (!resourceId) {
    res.status(400).send("缺少资源ID");
    return;
  }

  const db = getFirestore();
  try {
    await db.collection("resources").doc(resourceId).update({
      visits: admin.firestore.FieldValue.increment(1)
    });
    res.status(200).send("计数更新成功");
  } catch (err) {
    res.status(500).send("更新失败,请重试");
  }
});

exports.incrementVisits = functions.https.onRequest(app);

这种方式的缺点是可能误杀共享IP的正常用户,但对于一般场景已经足够。

额外注意事项

  • 必须使用原子操作:不管哪种方案,都要用FieldValue.increment(1)更新计数,不要先读取再写入,否则并发请求会导致计数丢失;
  • 保持数据库只读权限:继续让/resources端点保持公开只读,所有计数更新都通过Cloud Function完成;
  • 可选:会话验证:如果你的网站有匿名会话,可以在请求时带上会话ID,在函数里验证会话有效性,进一步防止刷量。

内容的提问来源于stack exchange,提问作者Geoffrey H

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 10:31:02