التحكم في الوصول باستخدام المطالبات المخصصة وقواعد الأمان

يدعم Firebase Admin SDK تحديد السمات المخصصة في حسابات المستخدمين. يوفر ذلك القدرة على تنفيذ استراتيجيات مختلفة للتحكم في الوصول، بما في ذلك التحكم في الوصول المستند إلى الدور، في تطبيقات Firebase. يمكن لهذه السمات المخصصة أن تمنح المستخدمين مستويات مختلفة من الوصول (الأدوار)، والتي يتم فرضها في قواعد أمان التطبيق.

يمكن تحديد أدوار المستخدم للحالات الشائعة التالية:

  • منح المستخدم امتيازات إدارية للوصول إلى البيانات والموارد.
  • تحديد المجموعات المختلفة التي ينتمي إليها المستخدم.
  • توفير الوصول متعدد المستويات:
    • التمييز بين المشتركين المدفوعين وغير المدفوعين.
    • تمييز المشرفين عن المستخدمين العاديين.
    • تطبيق المعلم/الطالب، الخ.
  • إضافة معرف إضافي على المستخدم. على سبيل المثال، يمكن لمستخدم Firebase التعيين إلى معرف UID مختلف في نظام آخر.

لنفكر في حالة تريد فيها تقييد الوصول إلى عقدة قاعدة البيانات "adminContent". يمكنك القيام بذلك من خلال البحث في قاعدة البيانات في قائمة المستخدمين الإداريين. ومع ذلك، يمكنك تحقيق نفس الهدف بشكل أكثر كفاءة باستخدام مطالبة مستخدم مخصصة تسمى admin باستخدام قاعدة قاعدة بيانات Realtime التالية:

{
  "rules": {
    "adminContent": {
      ".read": "auth.token.admin === true",
      ".write": "auth.token.admin === true",
    }
  }
}

يمكن الوصول إلى مطالبات المستخدم المخصصة عبر رموز المصادقة المميزة للمستخدم. في المثال أعلاه، فقط المستخدمين الذين لديهم admin تم تعيينه على true في مطالبة الرمز المميز الخاصة بهم سيكون لديهم حق الوصول للقراءة/الكتابة إلى عقدة adminContent . نظرًا لأن رمز المعرف يحتوي بالفعل على هذه التأكيدات، فلا حاجة إلى معالجة أو بحث إضافي للتحقق من أذونات المسؤول. بالإضافة إلى ذلك، يعد رمز المعرف آلية موثوقة لتسليم هذه المطالبات المخصصة. يجب أن يتحقق كل الوصول المصادق من صحة رمز المعرف قبل معالجة الطلب المرتبط.

تستمد أمثلة التعليمات البرمجية والحلول الموضحة في هذه الصفحة من كل من واجهات برمجة تطبيقات Firebase Auth من جانب العميل وواجهات برمجة تطبيقات Auth من جانب الخادم التي توفرها Admin SDK .

قم بتعيين مطالبات المستخدم المخصصة والتحقق من صحتها عبر 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.
  });

جافا

// 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.

ج#

// 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 بايت.

يمكن لرمز المعرف الذي يتم إرساله إلى خادم الواجهة الخلفية تأكيد هوية المستخدم ومستوى الوصول باستخدام Admin SDK كما يلي:

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

بايثون

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

ج#

// 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']);
  });

جافا

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

ج#

// Lookup the user associated with the specified uid.
UserRecord user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine(user.CustomClaims["admin"]);

يمكنك حذف المطالبات المخصصة للمستخدم عن طريق تمرير null إلى customClaims .

نشر المطالبات المخصصة للعميل

بعد تعديل المطالبات الجديدة على مستخدم عبر Admin SDK، يتم نشرها إلى مستخدم تمت مصادقته على جانب العميل عبر رمز المعرف بالطرق التالية:

  • يقوم المستخدم بتسجيل الدخول أو إعادة المصادقة بعد تعديل المطالبات المخصصة. سيحتوي رمز المعرف الذي تم إصداره نتيجة لذلك على أحدث المطالبات.
  • تحصل جلسة المستخدم الحالية على تحديث رمز المعرف الخاص بها بعد انتهاء صلاحية الرمز المميز الأقدم.
  • يتم فرض تحديث رمز المعرف عن طريق استدعاء currentUser.getIdToken(true) .

الوصول إلى المطالبات المخصصة على العميل

لا يمكن استرداد المطالبات المخصصة إلا من خلال رمز معرف المستخدم. قد يكون الوصول إلى هذه المطالبات ضروريًا لتعديل واجهة مستخدم العميل بناءً على دور المستخدم أو مستوى الوصول. ومع ذلك، يجب دائمًا فرض الوصول إلى الواجهة الخلفية من خلال رمز المعرف بعد التحقق من صحته وتحليل مطالباته. لا ينبغي إرسال المطالبات المخصصة مباشرة إلى الواجهة الخلفية، حيث لا يمكن الوثوق بها خارج الرمز المميز.

بمجرد نشر أحدث المطالبات إلى رمز معرف المستخدم، يمكنك الحصول عليها عن طريق استرداد رمز معرف:

جافا سكريبت

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

ج موضوعية

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

أفضل الممارسات للمطالبات المخصصة

تُستخدم المطالبات المخصصة فقط لتوفير التحكم في الوصول. وهي غير مصممة لتخزين بيانات إضافية (مثل الملف الشخصي والبيانات المخصصة الأخرى). على الرغم من أن هذا قد يبدو وكأنه آلية مناسبة للقيام بذلك، إلا أنه لا ينصح به بشدة حيث يتم تخزين هذه المطالبات في رمز المعرف وقد يتسبب في مشكلات في الأداء نظرًا لأن جميع الطلبات التي تمت مصادقتها تحتوي دائمًا على رمز معرف Firebase المميز المطابق للمستخدم الذي قام بتسجيل الدخول.

  • استخدم المطالبات المخصصة لتخزين البيانات للتحكم في وصول المستخدم فقط. يجب تخزين جميع البيانات الأخرى بشكل منفصل عبر قاعدة البيانات في الوقت الفعلي أو أي وسيلة تخزين أخرى من جانب الخادم.
  • المطالبات المخصصة محدودة الحجم. سيؤدي تمرير حمولة مطالبات مخصصة أكبر من 1000 بايت إلى حدوث خطأ.

أمثلة وحالات الاستخدام

توضح الأمثلة التالية المطالبات المخصصة في سياق حالات استخدام محددة لـ Firebase.

تحديد الأدوار عبر وظائف Firebase عند إنشاء المستخدم

في هذا المثال، يتم تعيين المطالبات المخصصة للمستخدم عند الإنشاء باستخدام Cloud Functions.

يمكن إضافة المطالبات المخصصة باستخدام Cloud Functions، ونشرها على الفور باستخدام Realtime Database. يتم استدعاء الوظيفة فقط عند التسجيل باستخدام مشغل onCreate . بمجرد تعيين المطالبات المخصصة، يتم نشرها في جميع الجلسات الحالية والمستقبلية. في المرة التالية التي يقوم فيها المستخدم بتسجيل الدخول باستخدام بيانات اعتماد المستخدم، يحتوي الرمز المميز على المطالبات المخصصة.

التنفيذ من جانب العميل (جافا سكريبت)

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

منطق الوظائف السحابية

تتم إضافة عقدة قاعدة بيانات جديدة (بيانات التعريف/($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.

التنفيذ من جانب العميل (جافا سكريبت)

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

يمكن استخدام نفس التدفق عند ترقية مستوى وصول المستخدم الحالي. خذ على سبيل المثال ترقية مستخدم مجاني إلى اشتراك مدفوع. يتم إرسال رمز معرف المستخدم مع معلومات الدفع إلى خادم الواجهة الخلفية عبر طلب 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);
  });

جافا

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

}

ج#

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

جافا

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

}

ج#

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