Firebase Admin SDK 支持在用户帐户上定义自定义属性。这提供了在 Firebase 应用中实施各种访问控制策略的能力,包括基于角色的访问控制。这些自定义属性可以为用户提供不同级别的访问权限(角色),这些权限在应用程序的安全规则中强制执行。
可以为以下常见情况定义用户角色:
- 授予用户访问数据和资源的管理权限。
- 定义用户所属的不同组。
- 提供多级访问:
- 区分付费/未付费用户。
- 将版主与普通用户区分开来。
- 教师/学生申请等
- 为用户添加附加标识符。例如,Firebase 用户可以映射到另一个系统中的不同 UID。
让我们考虑一个您想要限制对数据库节点“adminContent”的访问的情况。您可以通过在管理员用户列表中查找数据库来做到这一点。但是,您可以使用名为admin
的自定义用户声明和以下实时数据库规则更有效地实现相同的目标:
{
"rules": {
"adminContent": {
".read": "auth.token.admin === true",
".write": "auth.token.admin === true",
}
}
}
自定义用户声明可通过用户的身份验证令牌访问。在上面的示例中,只有在其令牌声明中将admin
设置为 true 的用户才具有对adminContent
节点的读/写访问权限。由于 ID 令牌已经包含这些断言,因此不需要额外的处理或查找来检查管理员权限。此外,ID 令牌是交付这些自定义声明的可信机制。所有经过身份验证的访问都必须在处理相关请求之前验证 ID 令牌。
本页中描述的代码示例和解决方案来自Admin SDK提供的客户端 Firebase Auth API 和服务器端 Auth API。
通过 Admin SDK 设置和验证自定义用户声明
自定义声明可能包含敏感数据,因此它们只能由 Firebase Admin SDK 从特权服务器环境中设置。
节点.js
// Set admin privilege on the user corresponding to uid.
getAuth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
爪哇
// Set admin privilege on the user corresponding to uid.
Map<String, Object> claims = new HashMap<>();
claims.put("admin", true);
FirebaseAuth.getInstance().setCustomUserClaims(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
Python
# Set admin privilege on the user corresponding to uid.
auth.set_custom_user_claims(uid, {'admin': True})
# The new custom claims will propagate to the user's ID token the
# next time a new one is issued.
去
// Get an auth client from the firebase.App
client, err := app.Auth(ctx)
if err != nil {
log.Fatalf("error getting Auth client: %v\n", err)
}
// Set admin privilege on the user corresponding to uid.
claims := map[string]interface{}{"admin": true}
err = client.SetCustomUserClaims(ctx, uid, claims)
if err != nil {
log.Fatalf("error setting custom claims %v\n", err)
}
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
C#
// Set admin privileges on the user corresponding to uid.
var claims = new Dictionary<string, object>()
{
{ "admin", true },
};
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(uid, claims);
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
自定义声明对象不应包含任何OIDC保留键名称或Firebase 保留名称。自定义声明有效负载不得超过 1000 字节。
发送到后端服务器的 ID 令牌可以使用 Admin SDK 确认用户的身份和访问级别,如下所示:
节点.js
// Verify the ID token first.
getAuth()
.verifyIdToken(idToken)
.then((claims) => {
if (claims.admin === true) {
// Allow access to requested admin resource.
}
});
爪哇
// Verify the ID token first.
FirebaseToken decoded = FirebaseAuth.getInstance().verifyIdToken(idToken);
if (Boolean.TRUE.equals(decoded.getClaims().get("admin"))) {
// Allow access to requested admin resource.
}
Python
# Verify the ID token first.
claims = auth.verify_id_token(id_token)
if claims['admin'] is True:
# Allow access to requested admin resource.
pass
去
// Verify the ID token first.
token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
log.Fatal(err)
}
claims := token.Claims
if admin, ok := claims["admin"]; ok {
if admin.(bool) {
//Allow access to requested admin resource.
}
}
C#
// Verify the ID token first.
FirebaseToken decoded = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
object isAdmin;
if (decoded.Claims.TryGetValue("admin", out isAdmin))
{
if ((bool)isAdmin)
{
// Allow access to requested admin resource.
}
}
您还可以检查用户现有的自定义声明,这些声明可作为用户对象的属性使用:
节点.js
// Lookup the user associated with the specified uid.
getAuth()
.getUser(uid)
.then((userRecord) => {
// The claims can be accessed on the user record.
console.log(userRecord.customClaims['admin']);
});
爪哇
// Lookup the user associated with the specified uid.
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
System.out.println(user.getCustomClaims().get("admin"));
Python
# Lookup the user associated with the specified uid.
user = auth.get_user(uid)
# The claims can be accessed on the user record.
print(user.custom_claims.get('admin'))
去
// Lookup the user associated with the specified uid.
user, err := client.GetUser(ctx, uid)
if err != nil {
log.Fatal(err)
}
// The claims can be accessed on the user record.
if admin, ok := user.CustomClaims["admin"]; ok {
if admin.(bool) {
log.Println(admin)
}
}
C#
// Lookup the user associated with the specified uid.
UserRecord user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine(user.CustomClaims["admin"]);
您可以通过为customClaims
传递 null 来删除用户的自定义声明。
将自定义声明传播给客户端
通过 Admin SDK 在用户上修改新声明后,它们会通过 ID 令牌以下列方式传播到客户端上经过身份验证的用户:
- 修改自定义声明后,用户登录或重新验证。结果颁发的 ID 令牌将包含最新的声明。
- 旧令牌过期后,现有用户会话会刷新其 ID 令牌。
- 通过调用
currentUser.getIdToken(true)
强制刷新 ID 令牌。
访问客户端上的自定义声明
只能通过用户的 ID 令牌检索自定义声明。根据用户的角色或访问级别修改客户端 UI 可能需要访问这些声明。但是,在验证并解析其声明后,应始终通过 ID 令牌强制执行后端访问。自定义声明不应直接发送到后端,因为它们不能在令牌之外被信任。
最新声明传播到用户的 ID 令牌后,您可以通过检索 ID 令牌来获取它们:
JavaScript
firebase.auth().currentUser.getIdTokenResult()
.then((idTokenResult) => {
// Confirm the user is an Admin.
if (!!idTokenResult.claims.admin) {
// Show admin UI.
showAdminUI();
} else {
// Show regular user UI.
showRegularUI();
}
})
.catch((error) => {
console.log(error);
});
安卓
user.getIdToken(false).addOnSuccessListener(new OnSuccessListener<GetTokenResult>() {
@Override
public void onSuccess(GetTokenResult result) {
boolean isAdmin = result.getClaims().get("admin");
if (isAdmin) {
// Show admin UI.
showAdminUI();
} else {
// Show regular user UI.
showRegularUI();
}
}
});
迅速
user.getIDTokenResult(completion: { (result, error) in
guard let admin = result?.claims?["admin"] as? NSNumber else {
// Show regular user UI.
showRegularUI()
return
}
if admin.boolValue {
// Show admin UI.
showAdminUI()
} else {
// Show regular user UI.
showRegularUI()
}
})
Objective-C
user.getIDTokenResultWithCompletion:^(FIRAuthTokenResult *result,
NSError *error) {
if (error != nil) {
BOOL *admin = [result.claims[@"admin"] boolValue];
if (admin) {
// Show admin UI.
[self showAdminUI];
} else {
// Show regular user UI.
[self showRegularUI];
}
}
}];
自定义声明的最佳实践
自定义声明仅用于提供访问控制。它们并非旨在存储其他数据(例如配置文件和其他自定义数据)。虽然这似乎是一种方便的机制,但强烈建议不要这样做,因为这些声明存储在 ID 令牌中,并且可能导致性能问题,因为所有经过身份验证的请求始终包含与登录用户对应的 Firebase ID 令牌。
- 使用自定义声明来存储数据,仅用于控制用户访问。所有其他数据应通过实时数据库或其他服务器端存储单独存储。
- 自定义声明的大小有限。传递大于 1000 字节的自定义声明有效负载将引发错误。
示例和用例
以下示例说明了特定 Firebase 用例上下文中的自定义声明。
在创建用户时通过 Firebase 函数定义角色
在此示例中,自定义声明是在使用 Cloud Functions 创建用户时设置的。
可以使用 Cloud Functions 添加自定义声明,并使用实时数据库立即传播。该函数仅在使用onCreate
触发器注册时调用。一旦设置了自定义声明,它们就会传播到所有现有和未来的会话。下次用户使用用户凭据登录时,令牌将包含自定义声明。
客户端实现 (JavaScript)
const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.catch(error => {
console.log(error);
});
let callback = null;
let metadataRef = null;
firebase.auth().onAuthStateChanged(user => {
// Remove previous listener.
if (callback) {
metadataRef.off('value', callback);
}
// On user login add new listener.
if (user) {
// Check if refresh is required.
metadataRef = firebase.database().ref('metadata/' + user.uid + '/refreshTime');
callback = (snapshot) => {
// Force refresh to pick up the latest custom claims changes.
// Note this is always triggered on first call. Further optimization could be
// added to avoid the initial trigger when the token is issued and already contains
// the latest claims.
user.getIdToken(true);
};
// Subscribe new listener to changes on that node.
metadataRef.on('value', callback);
}
});
云函数逻辑
添加了一个新的数据库节点 (metadata/($uid)},其读/写限制为经过身份验证的用户。
const functions = require('firebase-functions');
const { initializeApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
const { getDatabase } = require('firebase-admin/database');
initializeApp();
// On sign up.
exports.processSignUp = functions.auth.user().onCreate(async (user) => {
// Check if user meets role criteria.
if (
user.email &&
user.email.endsWith('@admin.example.com') &&
user.emailVerified
) {
const customClaims = {
admin: true,
accessLevel: 9
};
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(user.uid, customClaims);
// Update real-time database to notify client to force refresh.
const metadataRef = getDatabase().ref('metadata/' + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
await metadataRef.set({refreshTime: new Date().getTime()});
} catch (error) {
console.log(error);
}
}
});
数据库规则
{
"rules": {
"metadata": {
"$user_id": {
// Read access only granted to the authenticated user.
".read": "$user_id === auth.uid",
// Write access only via Admin SDK.
".write": false
}
}
}
}
通过 HTTP 请求定义角色
以下示例通过 HTTP 请求对新登录的用户设置自定义用户声明。
客户端实现 (JavaScript)
const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider)
.then((result) => {
// User is signed in. Get the ID token.
return result.user.getIdToken();
})
.then((idToken) => {
// Pass the ID token to the server.
$.post(
'/setCustomClaims',
{
idToken: idToken
},
(data, status) => {
// This is not required. You could just wait until the token is expired
// and it proactively refreshes.
if (status == 'success' && data) {
const json = JSON.parse(data);
if (json && json.status == 'success') {
// Force token refresh. The token claims will contain the additional claims.
firebase.auth().currentUser.getIdToken(true);
}
}
});
}).catch((error) => {
console.log(error);
});
后端实施(管理 SDK)
app.post('/setCustomClaims', async (req, res) => {
// Get the ID token passed.
const idToken = req.body.idToken;
// Verify the ID token and decode its payload.
const claims = await getAuth().verifyIdToken(idToken);
// Verify user is eligible for additional privileges.
if (
typeof claims.email !== 'undefined' &&
typeof claims.email_verified !== 'undefined' &&
claims.email_verified &&
claims.email.endsWith('@admin.example.com')
) {
// Add custom claims for additional privileges.
await getAuth().setCustomUserClaims(claims.sub, {
admin: true
});
// Tell client to refresh token on user.
res.end(JSON.stringify({
status: 'success'
}));
} else {
// Return nothing.
res.end(JSON.stringify({ status: 'ineligible' }));
}
});
升级现有用户的访问级别时可以使用相同的流程。以免费用户升级到付费订阅为例。用户的 ID 令牌通过 HTTP 请求与支付信息一起发送到后端服务器。成功处理付款后,通过 Admin SDK 将用户设置为付费订阅者。成功的 HTTP 响应返回给客户端以强制刷新令牌。
通过后端脚本定义角色
可以将循环脚本(不是从客户端启动)设置为运行以更新用户自定义声明:
节点.js
getAuth()
.getUserByEmail('user@admin.example.com')
.then((user) => {
// Confirm user is verified.
if (user.emailVerified) {
// Add custom claims for additional privileges.
// This will be picked up by the user on token refresh or next sign in on new device.
return getAuth().setCustomUserClaims(user.uid, {
admin: true,
});
}
})
.catch((error) => {
console.log(error);
});
爪哇
UserRecord user = FirebaseAuth.getInstance()
.getUserByEmail("user@admin.example.com");
// Confirm user is verified.
if (user.isEmailVerified()) {
Map<String, Object> claims = new HashMap<>();
claims.put("admin", true);
FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), claims);
}
Python
user = auth.get_user_by_email('user@admin.example.com')
# Confirm user is verified
if user.email_verified:
# Add custom claims for additional privileges.
# This will be picked up by the user on token refresh or next sign in on new device.
auth.set_custom_user_claims(user.uid, {
'admin': True
})
去
user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
log.Fatal(err)
}
// Confirm user is verified
if user.EmailVerified {
// Add custom claims for additional privileges.
// This will be picked up by the user on token refresh or next sign in on new device.
err := client.SetCustomUserClaims(ctx, user.UID, map[string]interface{}{"admin": true})
if err != nil {
log.Fatalf("error setting custom claims %v\n", err)
}
}
C#
UserRecord user = await FirebaseAuth.DefaultInstance
.GetUserByEmailAsync("user@admin.example.com");
// Confirm user is verified.
if (user.EmailVerified)
{
var claims = new Dictionary<string, object>()
{
{ "admin", true },
};
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}
自定义声明也可以通过 Admin SDK 逐步修改:
节点.js
getAuth()
.getUserByEmail('user@admin.example.com')
.then((user) => {
// Add incremental custom claim without overwriting existing claims.
const currentCustomClaims = user.customClaims;
if (currentCustomClaims['admin']) {
// Add level.
currentCustomClaims['accessLevel'] = 10;
// Add custom claims for additional privileges.
return getAuth().setCustomUserClaims(user.uid, currentCustomClaims);
}
})
.catch((error) => {
console.log(error);
});
爪哇
UserRecord user = FirebaseAuth.getInstance()
.getUserByEmail("user@admin.example.com");
// Add incremental custom claim without overwriting the existing claims.
Map<String, Object> currentClaims = user.getCustomClaims();
if (Boolean.TRUE.equals(currentClaims.get("admin"))) {
// Add level.
currentClaims.put("level", 10);
// Add custom claims for additional privileges.
FirebaseAuth.getInstance().setCustomUserClaims(user.getUid(), currentClaims);
}
Python
user = auth.get_user_by_email('user@admin.example.com')
# Add incremental custom claim without overwriting existing claims.
current_custom_claims = user.custom_claims
if current_custom_claims.get('admin'):
# Add level.
current_custom_claims['accessLevel'] = 10
# Add custom claims for additional privileges.
auth.set_custom_user_claims(user.uid, current_custom_claims)
去
user, err := client.GetUserByEmail(ctx, "user@admin.example.com")
if err != nil {
log.Fatal(err)
}
// Add incremental custom claim without overwriting existing claims.
currentCustomClaims := user.CustomClaims
if currentCustomClaims == nil {
currentCustomClaims = map[string]interface{}{}
}
if _, found := currentCustomClaims["admin"]; found {
// Add level.
currentCustomClaims["accessLevel"] = 10
// Add custom claims for additional privileges.
err := client.SetCustomUserClaims(ctx, user.UID, currentCustomClaims)
if err != nil {
log.Fatalf("error setting custom claims %v\n", err)
}
}
C#
UserRecord user = await FirebaseAuth.DefaultInstance
.GetUserByEmailAsync("user@admin.example.com");
// Add incremental custom claims without overwriting the existing claims.
object isAdmin;
if (user.CustomClaims.TryGetValue("admin", out isAdmin) && (bool)isAdmin)
{
var claims = new Dictionary<string, object>(user.CustomClaims);
// Add level.
claims["level"] = 10;
// Add custom claims for additional privileges.
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}