了解 2023 年 Google I/O 大会上介绍的 Firebase 亮点。了解详情

Özel Talepler ve Güvenlik Kuralları ile Erişimi Kontrol Edin

Firebase Admin SDK, kullanıcı hesaplarında özel özniteliklerin tanımlanmasını destekler. Bu, Firebase uygulamalarında rol tabanlı erişim denetimi de dahil olmak üzere çeşitli erişim denetimi stratejilerini uygulama olanağı sağlar. Bu özel öznitelikler, kullanıcılara bir uygulamanın güvenlik kurallarında zorunlu kılınan farklı erişim düzeyleri (roller) verebilir.

Kullanıcı rolleri, aşağıdaki genel durumlar için tanımlanabilir:

  • Bir kullanıcıya verilere ve kaynaklara erişmesi için yönetici ayrıcalıkları vermek.
  • Bir kullanıcının ait olduğu farklı grupları tanımlama.
  • Çok seviyeli erişim sağlama:
    • Ücretli/ücretsiz abone ayrımı.
    • Moderatörleri normal kullanıcılardan ayırt etmek.
    • Öğretmen/öğrenci uygulaması vb.
  • Bir kullanıcıya ek bir tanımlayıcı ekleyin. Örneğin, bir Firebase kullanıcısı, başka bir sistemde farklı bir UID ile eşlenebilir.

"adminContent" veritabanı düğümüne erişimi sınırlamak istediğiniz bir durumu ele alalım. Bunu, yönetici kullanıcılar listesindeki bir veritabanı aramasıyla yapabilirsiniz. Ancak, aşağıdaki Gerçek Zamanlı Veritabanı kuralıyla admin adlı özel bir kullanıcı talebini kullanarak aynı amacı daha verimli bir şekilde gerçekleştirebilirsiniz:

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

Özel kullanıcı taleplerine, kullanıcının kimlik doğrulama belirteçleri aracılığıyla erişilebilir. Yukarıdaki örnekte, yalnızca belirteç taleplerinde admin değeri true olarak ayarlanmış olan kullanıcılar adminContent düğümüne okuma/yazma erişimine sahip olacaktır. Kimlik belirteci bu iddiaları zaten içerdiğinden, yönetici izinlerini kontrol etmek için ek işleme veya arama gerekmez. Ayrıca, kimlik belirteci, bu özel talepleri iletmek için güvenilir bir mekanizmadır. Tüm kimliği doğrulanmış erişim, ilişkili isteği işlemeden önce kimlik belirtecini doğrulamalıdır.

Bu sayfada açıklanan kod örnekleri ve çözümler, hem istemci tarafı Firebase Auth API'lerinden hem de Admin SDK tarafından sağlanan sunucu tarafı Auth API'lerinden alınmıştır.

Yönetici SDK'sı aracılığıyla özel kullanıcı taleplerini ayarlayın ve doğrulayın

Özel talepler hassas veriler içerebilir, bu nedenle yalnızca Firebase Admin SDK tarafından ayrıcalıklı bir sunucu ortamından ayarlanmalıdır.

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.

piton

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

Gitmek

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

Özel talep nesnesi, OIDC ayrılmış anahtar adlarını veya Firebase ayrılmış adlarını içermemelidir. Özel talep yükü 1000 baytı aşmamalıdır.

Bir arka uç sunucusuna gönderilen bir kimlik belirteci, Yönetici SDK'sını kullanarak kullanıcının kimliğini ve erişim düzeyini aşağıdaki gibi onaylayabilir:

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

piton

# Verify the ID token first.
claims = auth.verify_id_token(id_token)
if claims['admin'] is True:
    # Allow access to requested admin resource.
    pass

Gitmek

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

Ayrıca, bir kullanıcının, kullanıcı nesnesinde bir özellik olarak bulunan mevcut özel taleplerini de kontrol edebilirsiniz:

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

piton

# 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'))

Gitmek

// 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 için null değerini vererek bir kullanıcının özel taleplerini silebilirsiniz.

Müşteriye özel talepleri yaymak

Yeni hak talepleri Yönetici SDK'sı aracılığıyla bir kullanıcı üzerinde değiştirildikten sonra, kimlik belirteci aracılığıyla istemci tarafında kimliği doğrulanmış bir kullanıcıya aşağıdaki şekillerde yayılır:

  • Bir kullanıcı, özel talepler değiştirildikten sonra oturum açar veya yeniden kimlik doğrulaması yapar. Sonuç olarak verilen kimlik belirteci en son talepleri içerecektir.
  • Mevcut bir kullanıcı oturumu, eski bir belirtecin süresi dolduktan sonra kimlik belirtecini yeniler.
  • Bir kimlik belirteci, currentUser.getIdToken(true) çağrılarak zorunlu olarak yenilenir.

İstemcideki özel taleplere erişin

Özel talepler yalnızca kullanıcının kimlik belirteci aracılığıyla alınabilir. Kullanıcının rolüne veya erişim düzeyine göre istemci kullanıcı arabirimini değiştirmek için bu hak taleplerine erişim gerekli olabilir. Ancak, arka uç erişimi, her zaman kimlik belirteci tarafından doğrulandıktan ve talepleri ayrıştırıldıktan sonra zorunlu kılınmalıdır. Özel talepler, belirteç dışında güvenilemeyeceğinden doğrudan arka uca gönderilmemelidir.

En son talepler bir kullanıcının kimlik belirtecine yayıldığında, bunları kimlik belirtecini alarak alabilirsiniz:

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

Süratli

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

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

Özel hak talepleri için en iyi uygulamalar

Özel talepler yalnızca erişim denetimi sağlamak için kullanılır. Ek verileri (profil ve diğer özel veriler gibi) depolamak için tasarlanmamışlardır. Bu, bunu yapmak için uygun bir mekanizma gibi görünse de, bu iddialar kimlik belirtecinde depolandığından ve kimliği doğrulanmış tüm istekler her zaman oturum açmış kullanıcıya karşılık gelen bir Firebase ID belirteci içerdiğinden performans sorunlarına neden olabileceğinden kesinlikle önerilmez.

  • Yalnızca kullanıcı erişimini denetlemek için verileri depolamak için özel talepleri kullanın. Diğer tüm veriler, gerçek zamanlı veritabanı veya diğer sunucu tarafı depolama yoluyla ayrı olarak saklanmalıdır.
  • Özel talepler boyut olarak sınırlıdır. 1000 bayttan büyük bir özel talep yükünün iletilmesi bir hata verecektir.

Örnekler ve kullanım durumları

Aşağıdaki örnekler, belirli Firebase kullanım örnekleri bağlamında özel talepleri göstermektedir.

Kullanıcı oluşturmada Firebase İşlevleri aracılığıyla rolleri tanımlama

Bu örnekte, Cloud Functions kullanılarak oluşturulurken bir kullanıcı üzerinde özel talepler ayarlanır.

Özel talepler, Cloud Functions kullanılarak eklenebilir ve Realtime Database ile hemen yayılabilir. İşlev, yalnızca bir onCreate tetikleyicisi kullanılarak kayıt yapıldığında çağrılır. Özel talepler ayarlandıktan sonra, mevcut ve gelecekteki tüm oturumlara yayılırlar. Kullanıcı, kullanıcı kimlik bilgileriyle bir sonraki oturum açtığında, belirteç özel talepleri içerir.

İstemci tarafı uygulaması (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);
  }
});

Bulut İşlevleri mantığı

Kimliği doğrulanmış kullanıcıyla sınırlı okuma/yazma ile yeni bir veritabanı düğümü (meta veri/($uid)} eklenir.

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

Veritabanı kuralları

{
  "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
      }
    }
  }
}

Bir HTTP isteği aracılığıyla rolleri tanımlama

Aşağıdaki örnek, bir HTTP isteği aracılığıyla yeni oturum açmış bir kullanıcıyla ilgili özel kullanıcı taleplerini ayarlar.

İstemci tarafı uygulaması (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);
});

Arka uç uygulaması (Yönetici SDK'sı)

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

Aynı akış, mevcut bir kullanıcının erişim düzeyini yükseltirken kullanılabilir. Örneğin, ücretli bir aboneliğe yükseltme yapan ücretsiz bir kullanıcıyı ele alalım. Kullanıcının kimlik belirteci, ödeme bilgileriyle birlikte bir HTTP isteği aracılığıyla arka uç sunucusuna gönderilir. Ödeme başarıyla işlendiğinde, kullanıcı Admin SDK aracılığıyla ücretli abone olarak ayarlanır. Belirteç yenilemeyi zorlamak için istemciye başarılı bir HTTP yanıtı döndürülür.

Arka uç komut dosyası aracılığıyla rolleri tanımlama

Kullanıcı özel taleplerini güncellemek için yinelenen bir komut dosyası (istemciden başlatılmamış) çalıştırılacak şekilde ayarlanabilir:

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

piton

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

Gitmek

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

Özel talepler, Yönetici SDK aracılığıyla aşamalı olarak da değiştirilebilir:

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

piton

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)

Gitmek

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