如何为React+Firebase站点配置用户白名单并实现访问权限校验
问题修复与实现方案
现有问题说明
你的实现思路本身是可行的,遇到的问题主要来自两个方面:一是isWhitelisted函数没有正确处理异步逻辑,二是调用时机选择不合理导致校验容易遗漏。
1. 修复isWhitelisted的异步错误
你原来的函数没有等待Firestore查询完成就直接返回了result,所以函数永远会返回false。可以直接优化为以下写法,同时提升查询效率:
const isWhitelisted = async (username: string): Promise<boolean> => { if (!username) return false; // 直接把查询条件都放在请求里,无需遍历结果,同时限制只查1条减少性能损耗 const snapshot = await projectFirestore .collection("permitted") .where("username", "==", username) .where("enabled", "==", true) .limit(1) .get(); return !snapshot.empty; }
2. 校验调用时机选择
推荐优先选择在UserProvider.tsx中做校验,这个位置全局监听身份状态变化,不管是用户主动登录、还是页面刷新后恢复登录态,都会触发校验,不会出现遗漏:
改造UserProvider.tsx
首先扩展UserContext的字段,增加白名单状态和加载状态,方便全局使用:
import React, { Component, createContext } from "react"; import firebase from "firebase/app"; import { auth, generateUserDocument } from "../config/firebase"; // 引入你写的isWhitelisted方法 interface UserContextValue { user: firebase.User | null; isWhitelisted: boolean; authLoading: boolean; } const UserContext = createContext<UserContextValue>({ user: null, isWhitelisted: false, authLoading: true }); class UserProvider extends Component { state = { user: null, isWhitelisted: false, authLoading: true }; componentDidMount = async () => { auth.onAuthStateChanged(async userAuth => { // 未登录状态直接重置 if (!userAuth) { this.setState({ user: null, isWhitelisted: false, authLoading: false }); return; } const user = await generateUserDocument(userAuth); // 这里的字段替换成你实际存储在permitted集合里的对应字段,比如email、displayName const checkResult = await isWhitelisted(user.displayName || user.email || ""); this.setState({ user, isWhitelisted: checkResult, authLoading: false }); }); }; render() { const { user, isWhitelisted, authLoading } = this.state; return ( <UserContext.Provider value={{ user, isWhitelisted, authLoading }}> {this.props.children} </UserContext.Provider> ); } } export { UserProvider, UserContext };
如果想在登录页提前拦截,也可以在LoginPage.tsx的signIn回调中加校验,注意要加await等待校验结果:
SignIn(provider) .then(async result => { // 替换为你实际用来匹配白名单的字段 const username = result.user?.displayName || result.user?.email || ""; const allowed = await isWhitelisted(username); if (!allowed) { // 不在白名单直接登出,提示错误 await auth.signOut(); setError("您不在站点许可访问列表内"); setAuthenticating(false); return; } history.push("/photos"); }) .catch(error => { setAuthenticating(false); setError(error.message); })
3. 更通用的权限处理方案
路由守卫拦截
封装私有路由组件,所有需要权限的页面都用该组件包裹,自动校验访问权限:
import React, { useContext } from 'react'; import { Route, Redirect, RouteProps } from 'react-router-dom'; import { UserContext } from './UserProvider'; const PrivateRoute: React.FC<RouteProps> = ({ children, ...rest }) => { const { user, isWhitelisted, authLoading } = useContext(UserContext); // 校验未完成时显示加载态 if (authLoading) return <div>权限校验中...</div>; return ( <Route {...rest} render={({ location }) => user && isWhitelisted ? ( children ) : ( <Redirect to={{ pathname: "/login", state: { from: location } }} /> ) } /> ); }; export default PrivateRoute;
使用示例:
// 路由配置处 <Switch> <Route path="/login" component={LoginPage} /> {/* 受保护的路由 */} <PrivateRoute path="/photos"> <PhotosPage /> </PrivateRoute> </Switch>
后端安全规则加固
前端的校验只能拦截普通用户,必须配置Firebase安全规则,防止用户绕过前端直接访问数据:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 白名单校验逻辑 function isWhitelisted() { return exists(/databases/$(database)/documents/permitted/$(request.auth.token.email)) && get(/databases/$(database)/documents/permitted/$(request.auth.token.email)).data.enabled == true; } // 受保护的集合配置权限 match /photos/{document=**} { allow read, write: if request.auth != null && isWhitelisted(); } // 白名单集合禁止普通用户修改 match /permitted/{document=**} { allow read: if request.auth != null; allow write: if false; } } }
提示:如果把permitted集合的文档ID设置为用户邮箱,会让安全规则和查询的效率更高,也能避免用户名重复的问题。
内容的提问来源于stack exchange,提问作者Russell Seymour
相关产品推荐
相关产品推荐

