Check out what’s new from Firebase@ Google I/O 2021, and join our alpha program for early access to the new Remote Config personalization feature. Learn more

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

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

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

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

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

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

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

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

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

سي #

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

سي #

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

سي #

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

يمكنك حذف المطالبات المخصصة للمستخدم عن طريق تمرير القيمة الفارغة لـ 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 عند إنشاء المستخدم

في هذا المثال ، يتم تعيين المطالبات المخصصة على المستخدم عند الإنشاء باستخدام وظائف السحابة.

يمكن إضافة المطالبات المخصصة باستخدام وظائف السحابة ، ونشرها على الفور باستخدام 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);
  }
});

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

تمت إضافة عقدة قاعدة بيانات جديدة (البيانات الوصفية / ($ uid)} مع تقييد القراءة / الكتابة للمستخدم المصادق عليه.

const functions = require('firebase-functions');

const admin = require('firebase-admin');
admin.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 admin.auth().setCustomUserClaims(user.uid, customClaims);

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

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

}

سي #

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

يمكن أيضًا تعديل المطالبات المخصصة بشكل تدريجي عبر 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)
	}

}

سي #

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