如何在使用Firestore的React网站中仅允许管理员类型用户登录
现有代码核心问题
- 存在严重安全漏洞:页面加载时直接拉取整个
Users集合的全量数据,相当于把所有用户的邮箱、明文密码完全暴露到前端,任何人打开浏览器控制台就能窃取所有用户信息,风险极高 - 逻辑错误:如果存在多个管理员用户,遍历集合后
adminFBEmail和adminFBPassword只会存储最后一个遍历到的管理员凭证,其余管理员都无法正常登录 - 设计错误:密码不应该明文存储在Firestore中,用户身份校验应该优先使用Firebase Auth自带的鉴权能力,而非前端自行对比密码
- 查询效率极低:用户量较大时拉取全表性能极差,完全没有必要
最优实现方案
核心逻辑为:先调用Firebase Auth完成邮箱密码的登录校验,登录成功后拿到当前用户的uid,再去Firestore查询对应用户的level字段是否为admin,是则放行,不是则提示无权限并自动登出。该方案既不会泄露其他用户数据,逻辑也完全准确。
额外注意:需要配合Firestore安全规则使用,禁止普通用户读取其他用户的
level字段,避免越权查询,规则参考:match /Users/{userId} { allow read: if request.auth != null && request.auth.uid == userId; }
修复后完整代码
import { getAuth, signInWithEmailAndPassword, signOut } from "firebase/auth"; import { getFirestore, doc, getDoc } from "firebase/firestore"; const Login = () => { const navigate = useNavigate(); const auth = getAuth(); const db = getFirestore(); const [loginLoading, setLoginLoading] = useState(false); const handleSubmit = async (values) => { setLoginLoading(true); try { // 第一步:调用Firebase Auth完成基础邮箱密码登录校验 const userCredential = await signInWithEmailAndPassword(auth, values.email, values.password); const loginUser = userCredential.user; // 第二步:查询当前登录用户的level字段判断是否为管理员 const userDoc = await getDoc(doc(db, "Users", loginUser.uid)); if (!userDoc.exists() || userDoc.data().level !== "admin") { await signOut(auth); // 非管理员直接登出,清除登录状态 alert("您没有管理员权限,无法登录"); return; } // 校验通过跳转后台 alert("管理员登录成功"); navigate('/app/dashboard', { replace: true }); } catch (err) { console.error(err); alert("登录失败,请检查邮箱密码是否正确"); } finally { setLoginLoading(false); } } return ( <> <Helmet> <title>Login | Power Cargo</title> </Helmet> <Box sx={{ backgroundColor: 'background.default', display: 'flex', flexDirection: 'column', height: '100%', justifyContent: 'center' }} > <Container maxWidth="sm"> <Formik initialValues={{ email: '', password: '' }} validationSchema={Yup.object().shape({ email: Yup.string().email('Must be a valid email').max(255).required('Email is required'), password: Yup.string().max(255).required('Password is required') })} onSubmit={handleSubmit} > {({ errors, handleBlur, handleChange, handleSubmit: formikSubmit, touched, values }) => ( <form onSubmit={formikSubmit} > <Box sx={{ mb: 3 }}> <Typography color="textPrimary" variant="h2" > Sign on the internal platform </Typography> </Box> <TextField error={Boolean(touched.email && errors.email)} fullWidth helperText={touched.email && errors.email} label="Email Address" margin="normal" name="email" onBlur={handleBlur} onChange={handleChange} type="email" value={values.email} variant="outlined" /> <TextField error={Boolean(touched.password && errors.password)} fullWidth helperText={touched.password && errors.password} label="Password" margin="normal" name="password" onBlur={handleBlur} onChange={handleChange} type="password" value={values.password} variant="outlined" /> <Box sx={{ py: 2 }}> <Button color="primary" disabled={loginLoading} fullWidth size="large" type="submit" variant="contained" > {loginLoading ? '登录中...' : 'Sign in now'} </Button> </Box> </form> )} </Formik> </Container> </Box> </> ); }
内容的提问来源于stack exchange,提问作者motionless570
相关产品推荐
相关产品推荐

