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的获取方式上,希望得到以下帮助:
- 如何修复该问题完成订阅流程;
- 是否有其他方式通过回调直接获取UID;
- 安全规则中正确获取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

