Google is committed to advancing racial equity for Black communities. See how.
本頁面由 Cloud Translation API 翻譯而成。
Switch to English

使用自定義聲明和安全規則控制訪問

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在特權服務器環境中進行設置。

Node.js

 // Set admin privilege on the user corresponding to uid.

admin.auth().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.
 

蟒蛇

 # 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確認用戶的身份和訪問級別,如下所示:

Node.js

 // Verify the ID token first.
admin.auth().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.
}
 

蟒蛇

 # 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.
    }
}

 

您還可以檢查用戶的現有自定義聲明,這些聲明可以作為用戶對象的屬性使用:

Node.js

 // Lookup the user associated with the specified uid.
admin.auth().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"));
 

蟒蛇

 # 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()
  }
})
 

目標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添加自定義聲明,並隨Realtime Database立即傳播。該函數僅在使用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 admin = require('firebase-admin');
admin.initializeApp();

// On sign up.
exports.processSignUp = functions.auth.user().onCreate((user) => {
  // Check if user meets role criteria.
  if (user.email &&
      user.email.endsWith('@admin.example.com') &&
      user.emailVerified) {
    const customClaims = {
      admin: true,
      accessLevel: 9
    };
    // Set custom user claims on this newly created user.
    return admin.auth().setCustomUserClaims(user.uid, customClaims)
      .then(() => {
        // Update real-time database to notify client to force refresh.
        const metadataRef = admin.database().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.
        return 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', (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;
  // Verify the ID token and decode its payload.
  admin.auth().verifyIdToken(idToken).then((claims) => {
    // 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.
      admin.auth().setCustomUserClaims(claims.sub, {
        admin: true
      }).then(function() {
        // 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

 admin.auth().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 admin.auth().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);
}
 

蟒蛇

 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進行增量修改:

Node.js

 admin.auth().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 admin.auth().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);
}
 

蟒蛇

 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);
}