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

Firebase+NextAuth集成Stripe订阅:sub替代UID规则问题排查

问题背景

我正在为项目集成Stripe订阅功能,使用Firebase 9和NextAuth(当前仅用Google登录提供商)。由于NextAuth的Google提供商仅返回name、email、image三类数据,我在[..nextauth.ts]中添加回调以获取sub等更多数据(无法直接获取UID),代码如下:

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    }),

    // ...add more providers here
  ],
  callbacks: {
    async session({ session, token, user }: any) {
      session.user.token = token;
      return session;
    },
  },

};

export default NextAuth(authOptions);

该配置生效,已获取包含sub的token对象:

{
  user: {
    name: 'Lionel messi',
    email: 'random@gmail.com',
    image: 'https:/.googleusercontent.com/z/Arandom',
    token: {
      name: 'Lionel messi',
      email: 'random@gmail.com',
      picture: 'https:/.googleusercontent.com/z/Arandom',
      sub: '111random57437563475920',
      iat: "random",
      exp: "random",
      jti: 'c17388fc-dde4-4d9d-aadc-random'
    }
  }
}

但将Firebase安全规则中的uid替换为request.auth.token.sub后,订阅时网站报错:"There is no user record corresponding to the provided identifier.",安全规则代码如下:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow write, read: if true
         allow write: if request.auth !=null && request.auth.uid == ""
    }
  
    match /customers/request.auth.token.sub {
      allow read: if request.auth.uid == request.auth.token.sub


      match /checkout_sessions/{id} {
        allow read, write: if request.auth.uid == request.auth.token.sub;
      }
      match /subscriptions/{id} {
        allow read: if request.auth.uid == request.auth.token.sub;
      }
      match /payments/{id} {
        allow read: if request.auth.uid == request.auth.token.sub;
      }
    }

    match /products/{id} {
      allow read: if true;

      match /prices/{id} {
        allow read: if true;
      }

      match /tax_rates/{id} {
        allow read: if true;
      }
    }
  }
}

我的loadCheckout函数代码如下:

const loadCheckout = async (priceId: string) => {
  // @ts-ignore
  const uid = session?.user?.token?.sub;
  console.log(uid, "user ID");
  const payRef = await addDoc(
    collection(db, `customers/${uid}/checkout_sessions`),
    {
      price: priceId,
      success_url: window.location.origin,
      cancel_url: window.location.origin,
    }
  );

调用方式为:onClick={() => loadCheckout(productData?.prices?.priceId)},已确认能获取productData.prices.priceId。我确定问题出在安全规则或sub的获取方式上,希望得到以下帮助:

  1. 如何修复该问题完成订阅流程;
  2. 是否有其他方式通过回调直接获取UID;
  3. 安全规则中正确获取sub的方式是什么?

解决方案

1. 修复订阅流程的问题

核心问题在于安全规则写法错误和NextAuth与Firebase用户身份未关联:

步骤1:修正Firebase安全规则

原规则中match /customers/request.auth.token.sub是错误写法,需用通配符匹配路径参数,再验证参数与用户sub一致,同时移除全局宽松规则避免安全风险:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /customers/{userId} {
      // 验证当前用户的sub等于路径中的userId
      allow read, write: if request.auth.token.sub == userId;

      match /checkout_sessions/{id} {
        allow read, write: if request.auth.token.sub == userId;
      }
      match /subscriptions/{id} {
        allow read: if request.auth.token.sub == userId;
      }
      match /payments/{id} {
        allow read: if request.auth.token.sub == userId;
      }
    }

    match /products/{id} {
      allow read: if true;

      match /prices/{id} {
        allow read: if true;
      }

      match /tax_rates/{id} {
        allow read: if true;
      }
    }
  }
}

步骤2:关联NextAuth与Firebase用户

报错提示无对应用户记录,是因为NextAuth的Google登录不会自动创建Firebase用户,需在NextAuth的jwt回调中手动创建/关联:

import { initializeApp, getApps, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";

// 初始化Firebase Admin
if (!getApps().length) {
  initializeApp({
    credential: cert({
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
    }),
  });
}

export const authOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      // 首次登录时关联Firebase用户
      if (account) {
        const auth = getAuth();
        let firebaseUser;
        try {
          firebaseUser = await auth.getUserByEmail(token.email!);
        } catch {
          // 无用户则创建
          firebaseUser = await auth.createUser({
            email: token.email!,
            displayName: token.name,
            photoURL: token.picture,
          });
        }
        token.firebaseUid = firebaseUser.uid;
        token.sub = account.sub;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.firebaseUid = token.firebaseUid;
      session.user.sub = token.sub;
      return session;
    },
  },
};

export default NextAuth(authOptions);

步骤3:调整loadCheckout函数

使用关联后的Firebase UID(更符合Firebase生态)或sub访问集合:

const loadCheckout = async (priceId: string) => {
  const firebaseUid = session?.user?.firebaseUid;
  if (!firebaseUid) return;
  
  const payRef = await addDoc(
    collection(db, `customers/${firebaseUid}/checkout_sessions`),
    {
      price: priceId,
      success_url: window.location.origin,
      cancel_url: window.location.origin,
    }
  );
};

2. 直接获取Firebase UID的其他方式

除了jwt回调,还可以在signIn回调中处理用户关联,同步返回UID:

callbacks: {
  async signIn({ user, account }) {
    if (account?.provider === 'google') {
      const auth = getAuth();
      let firebaseUser;
      try {
        firebaseUser = await auth.getUserByEmail(user.email!);
      } catch {
        firebaseUser = await auth.createUser({
          email: user.email!,
          displayName: user.name,
          photoURL: user.image,
        });
      }
      user.firebaseUid = firebaseUser.uid;
      return true;
    }
    return false;
  },
  // 后续在jwt、session回调中传递firebaseUid
}

3. 安全规则中正确获取sub的方式

  • 确保NextAuth的JWT被Firebase Auth验证,或使用自定义令牌登录Firebase(推荐后者);
  • 匹配路径时用通配符{paramName},再对比request.auth.token.sub == paramName,示例:
    match /customers/{userId} {
      allow read, write: if request.auth.token.sub == userId;
    }
    
  • 若使用Firebase原生UID,直接用request.auth.uid == userId即可,这是Firebase安全规则的标准用法,兼容性更好。

内容的提问来源于stack exchange,提问作者pion-dev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 14:54:57