این راهنما براساس راهنمای آشنایی با زبان اصلی «قوانین امنیتی Firebase» ساخته شده است تا نحوه افزودن شرایط به «قوانین امنیتی پایگاه داده بیدرنگ Firebase» را نشان دهد.
واحد سازنده اصلی «قوانین امنیتی پایگاه داده همزمان» شرط است. شرط عبارت بولی است که تعیین میکند آیا عملیات خاصی باید مجاز یا رد شود. برای قوانین پایه، استفاده از ثابتهای true و false بهعنوان
شرط کاملاً خوب کار میکند. اما زبان «قوانین امنیتی پایگاه داده همزمان»
روشهایی برای نوشتن شرایط پیچیدهتر دراختیارتان قرار میدهد که میتوانند:
- بررسی اصالتسنجی کاربر
- ارزیابی دادههای موجود درمقابل دادههای جدیداً ارسالشده
- به بخشهای مختلف پایگاه دادهتان دسترسی پیدا کنید و آنها را مقایسه کنید
- اعتبارسنجی دادههای ورودی
- استفاده از ساختار پُرسمانهای ورودی برای منطق امنیتی
استفاده از متغیرهای $ برای ضبط بخشهای مسیر
میتوانید بخشهایی از مسیر را برای خواندن یا نوشتن با اعلام متغیرهای ضبط با پیشوند $ ضبط کنید.
این بهعنوان کارت جوکر عمل میکند و مقدار آن کلید را برای استفاده در شرایط قانون ذخیره میکند:
{ "rules": { "rooms": { // this rule applies to any child of /rooms/, the key for each room id // is stored inside $room_id variable for reference "$room_id": { "topic": { // the room's topic can be changed if the room id has "public" in it ".write": "$room_id.contains('public')" } } } } }
متغیرهای پویا $ را میتوان بهطور موازی با نامهای مسیر ثابت نیز استفاده کرد. در این مثال، از متغیر $other برای اعلام
قانون .validate استفاده میکنیم که تضمین میکند
widget هیچ فرزند دیگری بهجز title و color ندارد.
هرگونه نوشتن که منجر به ایجاد فرزندان اضافی شود ناموفق خواهد بود.
{
"rules": {
"widget": {
// a widget can have a title or color attribute
"title": { ".validate": true },
"color": { ".validate": true },
// but no other child paths are allowed
// in this case, $other means any key excluding "title" and "color"
"$other": { ".validate": false }
}
}
}اصالتسنجی
یکی از رایجترین الگوهای قانون امنیتی، کنترل دسترسی براساس وضعیت اصالتسنجی کاربر است. برای مثال، ممکن است برنامهتان بخواهد فقط به کاربران واردشده به سیستم اجازه دهد داده بنویسند.
اگر برنامهتان از «اصالتسنجی Firebase» استفاده میکند، متغیر request.auth حاوی
اطلاعات اصالتسنجی برای مشتری درخواستکننده داده است.
برای اطلاعات بیشتر درباره request.auth، اسناد مرجع را ببینید.
Firebase Authentication با Firebase Realtime Database ادغام میشود تا به شما اجازه دهد دسترسی به دادهها را براساس هر کاربر بااستفاده از شرایط کنترل کنید. وقتی کاربر اصالتسنجی شد، متغیر auth در «قوانین امنیتی پایگاه داده بیدرنگ» شما با اطلاعات کاربر پر خواهد شد. این اطلاعات شامل شناسه یکتای آنها (uid)
و همچنین دادههای حساب پیوندشده، مانند شناسه Facebook یا نشانی ایمیل، و
اطلاعات دیگر است. اگر ارائهدهنده اصالتسنجی سفارشی پیادهسازی کنید، میتوانید فیلدهای خودتان را
به بار داده اصالتسنجی کاربرتان اضافه کنید.
این بخش توضیح میدهد که چگونه زبان «قوانین امنیتی پایگاه داده بیدرنگ Firebase» را با اطلاعات اصالتسنجی درباره کاربران خود ترکیب کنید. با ترکیب این دو مفهوم، میتوانید دسترسی به دادهها را براساس هویت کاربر کنترل کنید.
متغیر auth
متغیر ازپیشتعریفشده auth در قوانین قبلاز
اصالتسنجی تهی است.
وقتی کاربر با اصالتسنجی Firebase اصالتسنجی شود، ویژگیهای زیر را خواهد داشت:
| ارائهدهنده خدمات | روش اصالتسنجی استفادهشده («گذرواژه»، «ناشناس»، «فیسبوک»، «گیتهاب»، «گوگل»، یا «توییتر»). |
| uid | شناسه کاربر یکتا، تضمینشده برای یکتا بودن در همه ارائهدهندگان. |
| کد |
محتوای کد Firebase Auth ID. برای جزئیات بیشتر،
اسناد مرجع
auth.token را ببینید.
|
در اینجا نمونه قانونی را میبینید که از متغیر auth استفاده میکند تا مطمئن شود
هر کاربر فقط میتواند در مسیر مختص کاربر بنویسد:
{
"rules": {
"users": {
"$user_id": {
// grants write access to the owner of this user account
// whose uid must exactly match the key ($user_id)
".write": "$user_id === auth.uid"
}
}
}
}ساختاربندی پایگاه داده برای پشتیبانی از شرایط اصالتسنجی
معمولاً مفید است که پایگاه دادهتان را بهگونهای ساختاربندی کنید که نوشتن
Security Rules آسانتر شود. یک الگوی رایج برای ذخیره دادههای کاربر در Realtime Database این است که همه کاربران خود را در یک گره users ذخیره کنید که فرزندان آن مقادیر uid برای هر کاربر هستند. اگر میخواستید دسترسی به این دادهها را محدود کنید تا فقط کاربر واردشده به سیستم بتواند دادههای خودش را ببیند، قوانین شما چیزی شبیه به این خواهد بود.
{
"rules": {
"users": {
"$uid": {
".read": "auth !== null && auth.uid === $uid"
}
}
}
}کار با ادعاهای سفارشی اصالتسنجی
برای برنامههایی که به کنترل دسترسی سفارشی برای کاربران مختلف نیاز دارند، Firebase Authentication
به توسعهدهندگان اجازه میدهد ادعاهایی را برای کاربر Firebase تنظیم کنند.
این ادعاها در متغیر auth.token در قوانین شما قابلدسترسی هستند.
در اینجا نمونهای از قوانینی که از ادعای سفارشی hasEmergencyTowel استفاده میکنند آورده شده است:
{
"rules": {
"frood": {
// A towel is about the most massively useful thing an interstellar
// hitchhiker can have
".read": "auth.token.hasEmergencyTowel === true"
}
}
}توسعهدهندگانی که نشانهای اصالتسنجی سفارشی خودشان را ایجاد میکنند میتوانند بهطور اختیاری ادعاهایی به این نشانها اضافه کنند. این ادعاها در متغیر auth.token در قوانین شما دردسترس است.
دادههای موجود درمقابل دادههای جدید
متغیر ازپیش تعریفشده data برای ارجاع به دادهها قبلاز انجام عملیات نوشتن استفاده میشود. برعکس، متغیر newData
حاوی دادههای جدیدی است که درصورت موفقیتآمیز بودن عملیات نوشتن وجود خواهد داشت.
newData نشاندهنده نتیجه ادغامشده دادههای جدیدی است که نوشته میشود
و دادههای موجود.
برای مثال، این قانون به ما اجازه میدهد رکوردهای جدید ایجاد کنیم یا رکوردهای موجود را حذف کنیم، اما نمیتوانیم در دادههای غیرتهی موجود تغییر ایجاد کنیم:
// we can write as long as old data or new data does not exist // in other words, if this is a delete or a create, but not an update ".write": "!data.exists() || !newData.exists()"
ارجاع دادن به دادهها در مسیرهای دیگر
هر دادهای میتواند بهعنوان معیار برای قوانین استفاده شود. بااستفاده از متغیرهای ازپیش تعریفشده
root، data، و newData، میتوانیم
به هر مسیری که قبل یا بعداز رویداد نوشتن وجود دارد دسترسی پیدا کنیم.
این مثال را درنظر بگیرید که تا زمانی که مقدار گره /allow_writes/ برابر با true باشد، گره والد پرچم readOnly نداشته باشد، و فرزندی بهنام foo در دادههای جدیداً نوشتهشده وجود داشته باشد، عملیات نوشتن را مجاز میکند:
".write": "root.child('allow_writes').val() === true &&
!data.parent().child('readOnly').exists() &&
newData.child('foo').exists()"درحال اعتبارسنجی دادهها
اجرای ساختارهای داده و اعتبارسنجی قالب و محتوای دادهها باید
بااستفاده از قوانین .validate انجام شود که فقط پساز موفقیت قانون
.write برای اعطای دسترسی اجرا میشوند. در زیر نمونهای از تعریف قانون .validate آورده شده است که فقط تاریخهایی را در قالب YYYY-MM-DD بین سالهای ۱۹۰۰ تا ۲۰۹۹ مجاز میداند و بااستفاده از عبارت باقاعده بررسی میشود.
".validate": "newData.isString() &&
newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"قوانین .validate تنها نوع قانون امنیتی هستند که آبشاری نیستند. اگر هرکدام از
قوانین اعتبارسنجی در هرکدام از رکوردهای فرزند ناموفق باشد، کل عملیات نوشتن رد خواهد شد.
بهعلاوه، وقتی دادهها حذف میشوند (یعنی وقتی مقدار جدیدی که نوشته میشود null است)، تعاریف اعتبارسنجی نادیده گرفته میشوند.
این موارد ممکن است نکاتی جزئی بهنظر برسند، اما درواقع ویژگیهای مهمی برای نوشتن «قوانین امنیتی پایگاه داده بیدرنگ Firebase» قدرتمند هستند. قوانین زیر را درنظر بگیرید:
{
"rules": {
// write is allowed for all paths
".write": true,
"widget": {
// a valid widget must have attributes "color" and "size"
// allows deleting widgets (since .validate is not applied to delete rules)
".validate": "newData.hasChildren(['color', 'size'])",
"size": {
// the value of "size" must be a number between 0 and 99
".validate": "newData.isNumber() &&
newData.val() >= 0 &&
newData.val() <= 99"
},
"color": {
// the value of "color" must exist as a key in our mythical
// /valid_colors/ index
".validate": "root.child('valid_colors/' + newData.val()).exists()"
}
}
}
}با درنظر گرفتن این متغیر، به نتایج عملیات نوشتن زیر نگاه کنید:
جاوا اسکریپت
var ref = db.ref("/widget"); // PERMISSION_DENIED: does not have children color and size ref.set('foo'); // PERMISSION DENIED: does not have child color ref.set({size: 22}); // PERMISSION_DENIED: size is not a number ref.set({ size: 'foo', color: 'red' }); // SUCCESS (assuming 'blue' appears in our colors list) ref.set({ size: 21, color: 'blue'}); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child('size').set(99);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"]; // PERMISSION_DENIED: does not have children color and size [ref setValue: @"foo"]; // PERMISSION DENIED: does not have child color [ref setValue: @{ @"size": @"foo" }]; // PERMISSION_DENIED: size is not a number [ref setValue: @{ @"size": @"foo", @"color": @"red" }]; // SUCCESS (assuming 'blue' appears in our colors list) [ref setValue: @{ @"size": @21, @"color": @"blue" }]; // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate [[ref child:@"size"] setValue: @99];
Swift
var ref = FIRDatabase.database().reference().child("widget") // PERMISSION_DENIED: does not have children color and size ref.setValue("foo") // PERMISSION DENIED: does not have child color ref.setValue(["size": "foo"]) // PERMISSION_DENIED: size is not a number ref.setValue(["size": "foo", "color": "red"]) // SUCCESS (assuming 'blue' appears in our colors list) ref.setValue(["size": 21, "color": "blue"]) // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
جاوا
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("widget"); // PERMISSION_DENIED: does not have children color and size ref.setValue("foo"); // PERMISSION DENIED: does not have child color ref.child("size").setValue(22); // PERMISSION_DENIED: size is not a number Map<String,Object> map = new HashMap<String, Object>(); map.put("size","foo"); map.put("color","red"); ref.setValue(map); // SUCCESS (assuming 'blue' appears in our colors list) map = new HashMap<String, Object>(); map.put("size", 21); map.put("color","blue"); ref.setValue(map); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
REST (انتقال بازنمودی وضعیت)
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json
# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json
# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json
# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json
# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.jsonاکنون بیایید به همان ساختار نگاه کنیم، اما بااستفاده از قوانین .write بهجای .validate:
{ "rules": { // this variant will NOT allow deleting records (since .write would be disallowed) "widget": { // a widget must have 'color' and 'size' in order to be written to this path ".write": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical valid_colors/ index // BUT ONLY IF WE WRITE DIRECTLY TO COLOR ".write": "root.child('valid_colors/'+newData.val()).exists()" } } } }
در این نوع، هریک از عملیات زیر موفقیتآمیز خواهد بود:
جاوا اسکریپت
var ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.set({size: 99999, color: 'red'}); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child('size').set(99);
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl:URL]; // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored [ref setValue: @{ @"size": @9999, @"color": @"red" }]; // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") [[ref childByAppendingPath:@"size"] setValue: @99];
Swift
var ref = Firebase(url:URL) // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.setValue(["size": 9999, "color": "red"]) // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.childByAppendingPath("size").setValue(99)
جاوا
Firebase ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored Map<String,Object> map = new HashMap<String, Object>(); map.put("size", 99999); map.put("color", "red"); ref.setValue(map); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child("size").setValue(99);
REST (انتقال بازنمودی وضعیت)
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json
# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
این تصویر تفاوتهای بین قوانین .write و .validate را نشان میدهد.
همانطور که نشان داده شد، همه این قوانین باید بااستفاده از .validate نوشته شوند،
بهاستثنای قانون newData.hasChildren() که به این بستگی دارد که آیا
حذف کردن مجاز است یا نه.
قوانین مبتنی بر پُرسمان
اگرچه نمیتوانید از قوانین بهعنوان فیلتر استفاده کنید،
میتوانید بااستفاده از پارامترهای پُرسمان در قوانینتان، دسترسی به زیرمجموعههای داده را محدود کنید.
از عبارتهای query. در قوانینتان استفاده کنید تا دسترسی خواندن یا نوشتن را براساس پارامترهای پُرسمان اعطا کنید.
برای مثال، قانون مبتنی بر پُرسمان زیر از قوانین امنیتی مبتنی بر کاربر
و قوانین مبتنی بر پُرسمان برای محدود کردن دسترسی به دادهها در مجموعه baskets
فقط به سبدهای خریدی که کاربر فعال مالک آنها است استفاده میکند:
"baskets": {
".read": "auth.uid !== null &&
query.orderByChild === 'owner' &&
query.equalTo === auth.uid" // restrict basket access to owner of basket
}
پُرسمان زیر که شامل پارامترهای پُرسمان در قانون است، موفق خواهد بود:
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb) // Would succeed
بااینحال، پُرسمانهایی که پارامترهای موجود در قانون را ندارند با خطای PermissionDenied ناموفق خواهند بود:
db.ref("baskets").on("value", cb) // Would fail with PermissionDenied
همچنین میتوانید از قوانین مبتنی بر پُرسمان برای محدود کردن مقدار دادهای که کارخواه ازطریق عملیات خواندن بارگیری میکند استفاده کنید.
برای مثال، قانون زیر دسترسی خواندن را فقط به ۱۰۰۰ نتیجه اول پُرسمان محدود میکند، که براساس اولویت مرتب شدهاند:
messages: {
".read": "query.orderByKey &&
query.limitToFirst <= 1000"
}
// Example queries:
db.ref("messages").on("value", cb) // Would fail with PermissionDenied
db.ref("messages").limitToFirst(1000)
.on("value", cb) // Would succeed (default order by key)
عبارتهای query. زیر در «قوانین امنیتی پایگاه داده بیدرنگ» دردسترس است.
| عبارتهای قانون مبتنی بر پُرسمان | ||
|---|---|---|
| Expression | تایپ کردن | شرح |
| query.orderByKey query.orderByPriority query.orderByValue |
بولی | True برای پُرسمانهای مرتبشده براساس کلید، اولویت، یا مقدار. درغیراینصورت نادرست را برمیگرداند. |
| query.orderByChild | رشته خالی |
از رشته برای نشان دادن مسیر نسبی به گره فرزند استفاده کنید. برای مثال،
query.orderByChild === "address/zip". اگر پُرسمان براساس گره فرزند مرتب نشده باشد، این مقدار تهی است.
|
| query.startAt query.endAt query.equalTo |
رشته عدد بولی خالی |
حدود پُرسمان درحال اجرا را بازیابی میکند، یا اگر حدودی تنظیم نشده باشد، مقدار تهی برمیگرداند. |
| query.limitToFirst query.limitToLast |
number null |
حد پُرسمان درحال اجرا را بازیابی میکند، یا اگر حدی تنظیم نشده باشد، مقدار تهی برمیگرداند. |
مراحل بعدی
پساز این بحث درباره شرایط، درک پیچیدهتری از Security Rules دارید و آمادهاید تا:
با نحوه مدیریت موارد استفاده اصلی و گردش کار برای توسعه، آزمایش، و استقرار Security Rules آشنا شوید:
- با مجموعه کامل Security Rules متغیرهای ازپیش تعریفشدهای که میتوانید برای ساختن شرایط استفاده کنید آشنا شوید.
- قوانینی بنویسید که به سناریوهای رایج بپردازد.
- با مرور موقعیتهایی که باید «قوانین» ناامن را شناسایی و از آنها اجتناب کنید، دانش خود را تقویت کنید.
- با «مجموعه شبیهساز محلی Firebase» و نحوه استفاده از آن برای آزمایش Security Rules آشنا شوید.
- روشهای دردسترس برای استقرار Security Rules را مرور کنید.
با ویژگیهای Security Rules که مختص Realtime Database است آشنا شوید:
- با نحوه نمایهسازی Realtime Database آشنا شوید.
- REST API را برای استقرار Security Rules مرور کنید.