如何解决Firebase用户表重复邮箱问题?求优化代码及安全规则
解决Firebase用户表重复邮箱问题
一、先清理现有重复数据
手动在Firebase控制台的users集合中删除balgia2@gmail.com的重复记录,保留一条即可;若需要合并数据,可将两条记录的字段(如bet值)合并后再保留。
二、代码层面修复(NextAuth逻辑优化)
当前代码的检查与添加操作非原子性,且session回调可能被多次触发,是并发场景下重复创建用户的核心原因。建议改用signIn回调处理用户创建,并通过以下两种方案保证唯一性:
方案1:用邮箱作为文档ID(最简方案)
直接将用户邮箱设为文档ID,天然避免重复:
import NextAuth from 'next-auth'; import GoogleProvider from "next-auth/providers/google" import { db } from "../../../Firebase"; import { doc, getDoc, setDoc } from 'firebase/firestore'; export default NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }), ], callbacks: { // 仅在登录时执行一次的signIn回调 async signIn({ user }) { try { const userDocRef = doc(db, "users", user.email); const userDoc = await getDoc(userDocRef); if (!userDoc.exists()) { // 不存在则创建用户 await setDoc(userDocRef, { email: user.email, name: user.name, image: user.image, status: "afk", bet: 0 }); } else { // 可选:同步最新的用户昵称、头像 await setDoc(userDocRef, { name: user.name, image: user.image }, { merge: true }); } return true; } catch (error) { console.error("登录时处理用户数据失败:", error); return false; } }, async session({ session }) { try { // 从Firebase拉取完整用户信息 const userDocRef = doc(db, "users", session.user.email); const userDoc = await getDoc(userDocRef); if (userDoc.exists()) { const userData = userDoc.data(); session.user = { ...session.user, id: userDoc.id, ...userData }; } return session; } catch (error) { console.error("获取会话用户信息失败:", error); return session; } }, }, secret: "myawesomesecret" })
方案2:用事务保证原子性(保留自动生成ID)
若需保留Firebase自动生成的文档ID,用事务确保检查与创建操作的原子性:
import NextAuth from 'next-auth'; import GoogleProvider from "next-auth/providers/google" import { db } from "../../../Firebase"; import { collection, query, where, doc, runTransaction } from 'firebase/firestore'; const usersRef = collection(db, "users"); export default NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }), ], callbacks: { async signIn({ user }) { try { await runTransaction(db, async (transaction) => { const userQuery = query(usersRef, where("email", "==", user.email)); const querySnapshot = await transaction.get(userQuery); if (querySnapshot.empty) { // 事务内原子创建用户 const newUserDoc = doc(usersRef); transaction.set(newUserDoc, { email: user.email, name: user.name, image: user.image, status: "afk", bet: 0 }); } }); return true; } catch (error) { console.error("事务处理用户数据失败:", error); return false; } }, // session回调同方案1 async session({ session }) { try { const userQuery = query(usersRef, where("email", "==", session.user.email)); const querySnapshot = await getDocs(userQuery); if (!querySnapshot.empty) { const userData = querySnapshot.docs[0].data(); session.user = { ...session.user, id: querySnapshot.docs[0].id, ...userData }; } return session; } catch (error) { console.error("获取会话用户信息失败:", error); return session; } }, }, secret: "myawesomesecret" })
三、Firebase安全规则加固(双重保障)
修改安全规则,从后端层面阻止重复邮箱写入:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId} { // 开放读取权限(可根据需求调整为仅用户自身可读) allow read: if true; // 创建时检查邮箱唯一,更新/删除时仅允许操作自身文档 allow create: if request.resource.data.email != null && !exists(query(/databases/$(database)/documents/users, where('email', '==', request.resource.data.email))); allow update, delete: if request.resource.data.email == resource.data.email; } // 关闭其他集合的全开放权限,按需配置 match /{document=**} { allow read, write: if false; } } }
关键说明
- 改用signIn回调的原因:
session回调会在每次页面刷新时触发,而signIn仅在用户登录时执行一次,避免重复执行创建逻辑。 - 两种方案的适用场景:方案1操作高效、逻辑简单,适合无需自定义文档ID的场景;方案2保留自动生成ID,适合需要关联其他集合数据的场景。
- 安全规则的作用:即使前端代码出现漏洞,后端规则也能拦截非法的重复数据写入,形成双重保障。
内容的提问来源于stack exchange,提问作者Ali Yaghi
相关产品推荐
相关产品推荐

