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

如何在使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 10:06:03