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

如何解决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;
    }
  }
}

关键说明

  1. 改用signIn回调的原因:session回调会在每次页面刷新时触发,而signIn仅在用户登录时执行一次,避免重复执行创建逻辑。
  2. 两种方案的适用场景:方案1操作高效、逻辑简单,适合无需自定义文档ID的场景;方案2保留自动生成ID,适合需要关联其他集合数据的场景。
  3. 安全规则的作用:即使前端代码出现漏洞,后端规则也能拦截非法的重复数据写入,形成双重保障。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 22:54:53