非Firebase托管NextJS应用调用云函数遇CORS及安全问题
我有一个部署在非Firebase托管服务上的NextJS应用,同时使用Firebase Cloud Functions可调用函数。函数代码如下:
functions/index.js:
// index.js const functions = require('firebase-functions') const admin = require('firebase-admin') admin.initializeApp() const db = admin.firestore() const function1 = require('./src/function1') exports.function1 = functions .region('us-east1') .https.onCall((data, context) => { return function1.handler(data, context, db) })
functions/src/function1.js:
exports.handler = (data, context, db) => { // 业务代码 }
该函数通过context和db操作Firestore增删记录,本地模拟正常,但线上部署后调用出现CORS错误。找到的解决方案都是开放未授权访问,虽能解决CORS但存在安全隐患,希望得到以下两种方案之一:
- 允许未授权调用,但通过
context参数校验用户权限,未授权则终止执行; - 仅允许特定URI(如
https://www.mywebsite.com)调用云函数。
补充信息
- 使用Firebase Auth;
- CORS错误信息:
Access to fetch at 'https://us-east1-(projectid).cloudfunctions.net/function1' from origin 'https://(projectid).vercel.app' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
- 客户端调用代码:
import { functions } from './FirebaseConfig' // ^ 上述代码导入内容: // export const firebase = initializeApp(firebaseConfig) // export const functions = getFunctions(firebase, 'us-east1') const function1 = httpsCallable(functions, 'function1') // 通过按钮点击事件调用 <button onClick={() => { function1({ // 传入数据 }) .then(result => { // 处理结果 }) .catch(error => { // 处理错误 }) }} > 调用函数 </button>
方案1:允许未授权调用,但校验用户权限
Firebase可调用函数的context.auth会自动携带已登录用户的认证信息,只要用官方httpsCallable调用,CORS会被自动处理。你只需要在函数中添加权限校验逻辑:
修改function1.js的handler:
exports.handler = (data, context, db) => { // 校验用户是否已登录 if (!context.auth) { throw new functions.https.HttpsError('unauthenticated', '用户未登录,无法调用该函数'); } // 可选:校验用户是否有特定权限(比如从Firestore读取用户角色) const userId = context.auth.uid; return db.collection('users').doc(userId).get() .then(doc => { if (!doc.exists || !doc.data().isAllowed) { throw new functions.https.HttpsError('permission-denied', '无权限调用该函数'); } // 执行你的业务逻辑 return { success: true }; }) .catch(error => { throw new functions.https.HttpsError('internal', error.message); }); }
这样即使开放了函数的未授权访问,未登录或无权限的用户会被直接拦截,同时不会出现跨域问题。
方案2:仅允许特定域名调用云函数
如果想限制只有特定域名能调用函数,需要改用https.onRequest(因为https.onCall的CORS由Firebase自动处理,无法自定义),同时手动处理CORS和认证:
修改index.js:
const functions = require('firebase-functions') const admin = require('firebase-admin') const cors = require('cors')({ origin: 'https://www.mywebsite.com' }); // 指定允许的域名 admin.initializeApp() const db = admin.firestore() const function1 = require('./src/function1') exports.function1 = functions .region('us-east1') .https.onRequest((req, res) => { cors(req, res, () => { // 校验用户认证 const idToken = req.headers.authorization?.split('Bearer ')[1]; if (!idToken) { return res.status(401).send('未授权'); } admin.auth().verifyIdToken(idToken) .then(decodedToken => { const userId = decodedToken.uid; // 传递userId和db到handler return function1.handler(req.body, userId, db) .then(result => res.status(200).send(result)) .catch(error => res.status(400).send(error.message)); }) .catch(error => res.status(401).send('无效的认证令牌')); }); });
对应的客户端调用需要修改为手动携带认证令牌:
import { getAuth, getIdToken } from "firebase/auth"; const callFunction1 = async (data) => { const auth = getAuth(); const token = await getIdToken(auth.currentUser); const response = await fetch('https://us-east1-(projectid).cloudfunctions.net/function1', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return response.json(); } // 按钮调用 <button onClick={() => { callFunction1({/* 数据 */}) .then(result => {/* 处理结果 */}) .catch(error => {/* 处理错误 */}) }}>调用函数</button>
这种方式严格限制了调用域名,同时保留了用户认证校验。
能否通过auth.uid校验用户是否登录?用户能否伪造他人UID?
可以通过context.auth.uid校验登录状态。Firebase可调用函数的context.auth是由Firebase Auth服务验证令牌后生成的,无法被伪造——客户端传递的ID令牌会被自动验证,只有合法令牌才能获取到auth信息,不用担心UID被伪造。Admin SDK绕过Firestore安全规则是正常的,只要你正确校验了用户身份,就能确保只有授权用户能执行操作。浏览器GET方法访问函数返回400错误?
Firebase可调用函数(https.onCall)只接受POST请求,并且要求请求体是特定格式的JSON。用GET访问触发400错误是正常的,不符合onCall的请求规范。如果需要支持GET,需要改用https.onRequest实现。
内容的提问来源于stack exchange,提问作者aabdulahad

