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

使用SessionCookie调用Firebase可调用云函数遭遇403未认证错误

问题描述

用户登录时成功获取SessionCookie,但调用Firebase可调用云函数时收到403错误,提示请求未认证,同时伴随CORS错误。

技术栈

  • Firebase + Next.js 13(基于app/目录)
  • 登录流程按官方文档实现,颁发SessionCookie,API端点托管在Vercel

认证流程代码

客户端登录函数

signInWithEmailAndPassword(auth, data.email, data.password)
.then(async (userCredential) => {
  const { user } = userCredential

  // 获取用户ID Token并发送至sessionLogin端点以设置SessionCookie
  return user.getIdToken()
    .then(idToken => {
      return fetch('/api/sessionLogin', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ idToken })
      })
    })
})

服务器端sessionLogin API端点

import { NextResponse } from "next/server"
import { adminAuth } from "../../../../firebase/firebaseAdmin"

export async function POST(request) {

  // 获取传入的ID Token
  const body = await request.json()
  const idToken = body.idToken.toString()

  // 设置Session有效期为5天
  const expiresIn = 60 * 60 * 24 * 5 * 1000

  return adminAuth
    .createSessionCookie(idToken, { expiresIn })
    .then(
      (sessionCookie) => {

        // 初始化响应
        const response = new NextResponse(null, { status: 200, statusText: "OK" })

        // 设置SessionCookie的Cookie策略
        response.cookies.set({
          name: "sessionCookie",
          value: sessionCookie,
          maxAge: expiresIn,
          sameSite: "lax",
          httpOnly: true,
          secure: true,
          path: "/"
        })
        
        return response
      },
      (error) => {
        const response = new NextResponse(null, { status: 401, statusText: "UNAUTHORIZED REQUEST!" })
        return response
      }
    )
}

可调用云函数代码

客户端调用

// 客户端Header.js文件
// ...(导入及初始化代码)

const createPost = httpsCallable(functions, 'createPost')
createPost({ authorId: sessionCookie.uid })

云函数端

// Firebase云函数functions/index.js文件
// ...(导入及初始化代码)

initializeApp()
setGlobalOptions({ region: "us-central1" })

export const createPost = onCall({ cors: true }, async (request) => {
  ...
})

错误表现

  • 预检请求返回403,实际请求触发CORS错误,提示无Access-Control-Allow-Origin头
  • 云函数日志显示:The request was not authenticated. Either allow unauthenticated invocations or set the proper Authorization header.
  • 本地模拟器无此问题
  • 与官方文档差异:未使用csrfToken,未设置auth persistence为NONE(需保留客户端认证状态)

解决方案

核心原因

Firebase可调用云函数默认不会自动读取SessionCookie完成认证;CORS错误是因为认证失败时云函数提前返回,未正确注入CORS响应头。

方案一:通过Next.js API代理调用云函数(推荐)

利用Next.js API在服务器端读取SessionCookie并验证,再转发请求到云函数,避免客户端暴露敏感信息:

1. 创建代理API端点

// app/api/callCreatePost/route.js
import { NextResponse } from "next/server"
import { adminAuth } from "@/firebase/firebaseAdmin"

export async function POST(request) {
  // 读取SessionCookie
  const sessionCookie = request.cookies.get("sessionCookie")?.value
  if (!sessionCookie) {
    return NextResponse.json({ error: "未认证" }, { status: 401 })
  }

  try {
    // 验证SessionCookie合法性
    const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie, true)
    
    // 调用云函数,携带认证信息
    const cloudFunctionResponse = await fetch("https://us-central1-scipress-dev.cloudfunctions.net/createPost", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // 用自定义Token完成云函数认证
        "Authorization": `Bearer ${await adminAuth.createCustomToken(decodedClaims.uid)}`
      },
      body: JSON.stringify({ authorId: decodedClaims.uid })
    })

    const result = await cloudFunctionResponse.json()
    return NextResponse.json(result)
  } catch (error) {
    return NextResponse.json({ error: "认证失败" }, { status: 401 })
  }
}

2. 修改客户端调用逻辑

// 客户端Header.js
fetch('/api/callCreatePost', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  }
})

方案二:直接用ID Token认证云函数

若需直接从客户端调用云函数,可通过ID Token完成认证:

1. 客户端保存ID Token并携带请求头

// 登录函数修改,保存ID Token
signInWithEmailAndPassword(auth, data.email, data.password)
.then(async (userCredential) => {
  const { user } = userCredential
  const idToken = await user.getIdToken()
  // 临时存储ID Token(注意:localStorage存在XSS风险,仅作演示)
  localStorage.setItem('idToken', idToken)
  
  return fetch('/api/sessionLogin', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ idToken })
  })
})

// 调用云函数时携带Token
const idToken = localStorage.getItem('idToken')
const createPost = httpsCallable(functions, 'createPost', {
  headers: {
    'Authorization': `Bearer ${idToken}`
  }
})
createPost({ authorId: (await auth.currentUser).uid })

2. 云函数验证ID Token

import { https } from "firebase-functions/v2"
import { getAuth } from "firebase-admin/auth"

export const createPost = onCall({ cors: true }, async (request) => {
  // 优先使用Firebase自动解析的认证信息,否则手动从请求头提取
  let authUser = request.auth
  if (!authUser) {
    const authHeader = request.rawRequest.headers.authorization
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new https.HttpsError('unauthenticated', '用户未认证')
    }
    const idToken = authHeader.split('Bearer ')[1]
    const decodedToken = await getAuth().verifyIdToken(idToken)
    authUser = { uid: decodedToken.uid }
  }

  // 业务逻辑:使用authUser.uid处理请求
  // ...
})

方案三:修复CORS配置

确保云函数无论成功/失败都返回正确的CORS头,可指定允许的域名:

export const createPost = onCall({ 
  cors: {
    origin: ["http://localhost:3000", "你的生产域名"],
    methods: ["POST"]
  } 
}, async (request) => {
  try {
    // 认证与业务逻辑
    // ...
  } catch (error) {
    // 抛出标准HttpsError,自动携带CORS头
    throw new https.HttpsError(error.code || 'unknown', error.message)
  }
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 01:55:54