如何使用Firebase规则创建基于角色的访问控制(RBAC)与自定义声明
在构建应用程序时,并非所有用户都应具有相同的访问级别。例如,管理员可能能够更新或删除某些数据(日志除外),而普通用户应仅能读取数据。这就是基于角色的访问控制(RBAC)的作用所在。
Firebase通过自定义声明和安全规则使这成为可能。在本文中,您将学习如何:
- 使用Firebase Admin SDK向用户添加自定义声明
- 使用Firebase安全规则强制执行RBAC
- 使用不同角色测试规则
最终,您将拥有一个可直接在Firestore中强制执行管理员和用户等角色的工作设置。
目录
- 理解Firebase自定义声明
- 使用Firebase Admin SDK分配角色
- 编写和执行Firestore安全规则
- 使用Next.js和Firebase构建前端
- 测试RBAC工作流程
- 结论
前提条件
要跟随学习,您应该:
- 已设置Firebase项目并启用身份验证和Firestore
- 熟悉JavaScript/Node.js
- 已安装Firebase SDK和Admin SDK
如果您是Firebase新手,请在继续之前查看官方设置指南。
步骤1:理解Firebase自定义声明
Firebase自定义声明允许您将额外信息(如角色)附加到用户的身份验证令牌。您可以使用Admin SDK在服务器端设置此信息。它们包含在用户的request.auth.token中,您不能直接从客户端设置它们(出于安全原因)。
示例:添加声明后,用户的ID令牌可能如下所示:
1
2
3
4
5
|
{
"user_id": "abc123",
"email": "jane@example.com",
"role": "admin"
}
|
在此示例中,role字段确定应用程序中的访问权限。Firebase自动将此声明包含在用户的ID令牌中,因此可以在服务器和Firestore规则中安全验证。
步骤2:使用Firebase Admin SDK分配角色
Firebase Admin SDK允许您从后端(或通过脚本)安全地管理用户和分配角色。
首先,在Node环境中安装Admin SDK(不在前端应用中):
1
|
npm install firebase-admin
|
然后使用Firebase服务帐户凭据初始化它:
1
2
3
4
5
6
|
const admin = require("firebase-admin");
const serviceAccount = require("./service-account.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
|
要获取service-account.json文件,请导航到Firebase设置 > 项目设置 > 服务帐户。
单击生成私钥,它将自动下载JSON文件。您可以重命名文件或按原样使用。
您现在可以定义一个简单的函数来设置用户角色:
1
2
3
4
|
async function setUserRole(uid, role) {
await admin.auth().setCustomUserClaims(uid, { role });
console.log(`Role ${role} assigned to user ${uid}`);
}
|
role参数可以是您定义的任何内容,例如:
"admin":完全读写访问权限
"editor":可以创建或修改有限内容
"user":只读访问权限
您分配给用户的角色取决于应用程序的需求。在大多数应用程序中,您将从简单开始,可能只是管理员和用户,并随着时间的推移扩展。
使用示例:
定义函数后,使用用户的UID调用它:
1
|
setUserRole("USER_UID_HERE", "admin");
|
这将安全地将自定义声明附加到用户。
注意:用户必须注销并重新登录(或刷新其令牌),新声明才能生效。
步骤3:为RBAC编写Firestore安全规则
Firestore安全规则控制数据的读取或写入方式。它们在任何客户端请求到达数据库之前执行,确保不会绕过安全逻辑。
打开Firestore规则(firestore.rules)并定义基于角色的访问,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
// 任何登录用户都可以读取
allow read: if request.auth != null;
// 只有管理员可以写入
allow write: if request.auth.token.role == "admin";
}
}
}
|
以下是正在发生的事情:
request.auth != null:确保用户已登录
request.auth.token.role == "admin":仅授予具有管理员角色的用户写入权限
您可以将其扩展为多个角色:
1
|
allow write: if request.auth.token.role in ["admin", "editor"];
|
快速参考
在管理Firebase RBAC时请记住以下几点:
- 保持角色简单(例如,管理员、编辑者、用户)。不要过度复杂化
- 不要将角色存储在Firestore文档中。而是通过自定义声明强制执行
- 在部署之前始终在本地测试规则
- 记住在更新声明后用户必须刷新其令牌
步骤4:使用Next.js和Firebase构建前端
让我们使用Next.js和Firebase通过一个工作演示将其变为现实。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
firebase-rbac/
├── firebase-admin-scripts/ # 用于设置用户角色的服务器端脚本
│ ├── assignRole.js # 使用Firebase Admin SDK分配自定义声明
│ ├── .env # 包含服务帐户路径和测试UID
│ └── fir-rbac-...json # Firebase Admin SDK服务帐户json文件
│
├── src/
│ ├── app/
│ │ ├── page.js # 用于登录+帖子显示的主要Next.js页面
│ │ ├── layout.js # 全局布局
│ │ └── globals.css # Tailwind全局样式
│ └── lib/
│ └── firebase.js # Firebase客户端初始化
│
├── .env.local # Firebase Web配置(NEXT_PUBLIC_变量)
├── package.json
|
在您的.env.local中,使用Firebase项目配置信息完成这些变量:
1
2
3
4
5
6
|
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project-id.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project-id.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id
|
Firebase初始化(src/lib/firebase.js):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
|
演示组件(src/app/page.js):
此组件允许您登录、查看帖子,如果您是管理员,还可以创建新帖子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
"use client";
import { useState, useEffect } from "react";
import { auth, db } from "@/lib/firebase";
import {
signInWithEmailAndPassword,
onAuthStateChanged,
signOut,
} from "firebase/auth";
import { collection, getDocs, addDoc } from "firebase/firestore";
export default function Page() {
const [user, setUser] = useState(null);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [posts, setPosts] = useState([]);
const [newPost, setNewPost] = useState("");
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (u) => {
setUser(u);
if (u) await loadPosts();
else setPosts([]);
});
return () => unsubscribe();
}, []);
const loadPosts = async () => {
const snapshot = await getDocs(collection(db, "posts"));
setPosts(snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })));
};
const handleLogin = async (e) => {
e.preventDefault();
try {
await signInWithEmailAndPassword(auth, email, password);
alert("Logged in!");
setEmail("");
setPassword("");
} catch (error) {
console.error("Login failed:", error.message);
alert("Login failed: " + error.message);
}
};
const handleLogout = async () => {
await signOut(auth);
setUser(null);
};
const handleAddPost = async () => {
try {
await addDoc(collection(db, "posts"), { text: newPost });
setNewPost("");
await loadPosts();
alert("New Post added!");
} catch (e) {
alert("Opps!! Only admins can add posts.");
console.error(e.message);
}
};
return (
<main className="min-h-screen flex flex-col items-center justify-center bg-gray-900 text-gray-100 px-4">
<div className="w-full max-w-md bg-gray-800 rounded-2xl shadow-lg p-8 space-y-6">
<h1 className="text-2xl font-bold text-center text-indigo-400">
Firebase RBAC Demo (Next.js)
</h1>
{/* 登录表单 */}
{!user ? (
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="w-full px-3 py-2 rounded-md bg-gray-700 border border-gray-600 text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
</div>
<div>
<label className="block text-gray-300 text-sm mb-1">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-3 py-2 rounded-md bg-gray-700 border border-gray-600 text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
</div>
<button
type="submit"
className="w-full px-6 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 transition font-medium text-white"
>
Login
</button>
</form>
) : (
<div className="space-y-6">
<div className="flex flex-col items-center">
<p className="text-gray-300 mb-2">
Logged in as{" "}
<span className="font-semibold text-indigo-400">
{user.email}
</span>
</p>
<button
onClick={handleLogout}
className="text-sm text-red-400 hover:text-red-300 underline"
>
Logout
</button>
</div>
<section className="border-t border-gray-700 pt-4">
<h2 className="text-lg font-semibold text-indigo-300 mb-3">
Posts
</h2>
{posts.length > 0 ? (
<ul className="space-y-2">
{posts.map((p) => (
<li
key={p.id}
className="bg-gray-700 rounded-md px-3 py-2 text-gray-200"
>
{p.text}
</li>
))}
</ul>
) : (
<p className="text-gray-400 italic">No posts yet.</p>
)}
<div className="mt-4 flex items-center gap-2">
<input
value={newPost}
onChange={(e) => setNewPost(e.target.value)}
placeholder="New post"
className="flex-1 px-3 py-2 rounded-md bg-gray-700 border border-gray-600 text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
<button
onClick={handleAddPost}
className="px-4 py-2 rounded-md bg-indigo-600 hover:bg-indigo-500 transition font-medium text-white"
>
Add
</button>
</div>
</section>
</div>
)}
</div>
<footer className="mt-8 text-gray-500 text-sm">
Built with Next.js + Firebase | © FreeCodeCamp 2025
</footer>
</main>
);
}
|
步骤5:测试RBAC工作流程
现在一切设置完毕,是时候测试整个基于角色的访问控制流程,以确保规则和角色正常工作。
启用身份验证
转到Firebase控制台,选择您的项目,然后导航到身份验证 > 登录方法。选择添加新提供商。然后启用电子邮件/密码身份验证。这将允许您直接从应用程序创建和登录测试用户。
配置Firestore规则
接下来,您需要更新Firestore规则。导航到Firestore数据库,位于构建下拉菜单中。进入后,单击规则,您将能够更新规则。
用您之前定义的RBAC规则替换默认规则。这些规则确保只有经过身份验证的用户才能读取数据,只有管理员才能创建或修改帖子。
然后发布更新版本,您就可以开始了。
为用户分配角色
要测试管理员权限,请将管理员角色分配给您的测试用户之一。打开终端,切换到firebase-admin-scripts目录,并运行:
1
2
|
cd firebase-admin-scripts
node assignRole.js
|
这将执行Admin SDK脚本,该脚本向测试用户添加自定义声明。设置角色后,您将收到一条消息,确认管理员角色已分配给指定的用户ID。
如果用户已经登录,则用户必须注销并重新登录,新角色才能生效。
运行应用程序
现在您可以启动Next.js开发服务器:
在浏览器中访问http://localhost:3000。您应该找到Firebase RBAC演示应用程序。
验证基于角色的访问
尝试以被分配管理员角色的用户身份登录。登录后,您应该能够成功创建新帖子。接下来,以普通用户身份登录。您会注意到您可以查看现有帖子,但任何添加新帖子的尝试都将失败,并显示"Permission denied"警报。
如果您看到这些行为,那么您的RBAC系统正在按预期工作!
通过在Firestore层强制执行权限,您可以确保安全处理是集中式的,并且不能通过操纵客户端代码来绕过。这种方法使您的应用程序安全且可扩展,即使您的角色和数据变得更加复杂。
后续步骤:
- 添加更多角色(如编辑者等)
- 将RBAC与文档级验证结合以实现细粒度控制
- 探索Firebase的安全规则
结论
您刚刚学习了Firebase中简单但重要的基于角色的访问控制(RBAC)功能。在本指南中,我们介绍了自定义声明以及如何使用Admin SDK设置角色。您还学习了如何在Firestore安全规则中强制执行这些角色。