Firebase Admin SDK 支援在使用者帳戶中定義自訂屬性。這樣即可在 Firebase 應用程式中實作各種存取權控管策略,包括角色式存取權控管策略。這些自訂屬性可為使用者授予不同層級的存取權 (角色),這些層級會在應用程式的安全性規則中強制執行。
您可以針對下列常見情況定義使用者角色:
- 授予使用者存取資料和資源的管理員權限。
- 定義使用者所屬的不同群組。
- 提供多層級存取權:
- 區分付費/非付費訂閱者。
- 區分管理員和一般使用者。
- 老師/學生應用程式等
- 為使用者新增其他 ID。舉例來說,Firebase 使用者可以對應至其他系統中的其他 UID。
假設您要限制資料庫節點「adminContent」的存取權。您可以利用管理員使用者清單上的資料庫查詢執行這項操作。不過,您可以透過下列即時資料庫規則,使用名為 admin
的自訂使用者憑證附加資訊,更有效率地達成相同目標:
{
"rules": {
"adminContent": {
".read": "auth.token.admin === true",
".write": "auth.token.admin === true",
}
}
}
自訂使用者憑證附加資訊可透過使用者的驗證權杖存取。在上述範例中,只有在權杖憑證附加資訊中將 admin
設為 true 的使用者,才能擁有 adminContent
節點的讀取/寫入權限。由於 ID 權杖已包含這些斷言,因此不需要進行額外處理或查詢來檢查管理員權限。此外,ID 權杖也是提供這些自訂憑證附加資訊的可靠機制。所有已驗證的存取權都必須在處理關聯要求之前,先驗證 ID 權杖。
本頁說明的程式碼範例和解決方案擷取自用戶端 Firebase Auth API 和 Admin SDK 提供的伺服器端驗證 API。
透過 Admin SDK 設定及驗證自訂使用者憑證附加資訊
自訂憑證附加資訊可包含機密資料,因此只能透過 Firebase Admin SDK 的特殊權限伺服器環境進行設定。
Node.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.
});
Java
// 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.
Go
// 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 確認使用者的識別資訊和存取層級,如下所示:
Node.js
// Verify the ID token first.
getAuth()
.verifyIdToken(idToken)
.then((claims) => {
if (claims.admin === true) {
// Allow access to requested admin resource.
}
});
Java
// 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
Go
// 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.
}
}
您也可以查看使用者現有的自訂憑證附加資訊,這些憑證可做為使用者物件的屬性使用:
Node.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']);
});
Java
// 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'))
Go
// 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
傳遞空值,藉此刪除使用者的自訂憑證附加資訊。
將自訂聲明套用到用戶端
透過 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);
});
Android
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();
}
}
});
Swift
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);
}
});
Cloud Functions 邏輯
已新增資料庫節點 (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);
});
後端實作 (Admin 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 回應傳回給用戶端,以強制更新權杖。
透過後端指令碼定義角色
週期性指令碼 (非從用戶端啟動) 可設為執行,以更新使用者自訂憑證附加資訊:
Node.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);
});
Java
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
})
Go
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 逐步修改自訂聲明:
Node.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);
});
Java
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)
Go
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 = user.CustomClaims.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
// Add level.
var level = 10;
claims["level"] = level;
// Add custom claims for additional privileges.
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync(user.Uid, claims);
}