กฎความปลอดภัยของฐานข้อมูลเรียลไทม์ของ Firebase ช่วยให้คุณควบคุมการเข้าถึงข้อมูลที่จัดเก็บไว้ในฐานข้อมูลได้ ไวยากรณ์ของกฎที่ยืดหยุ่นช่วยให้คุณสร้างกฎที่ตรงกับทุกอย่างได้ ตั้งแต่การเขียนทั้งหมดลงในฐานข้อมูลไปจนถึงการดำเนินการในแต่ละโหนด
กฎความปลอดภัยของ Realtime Database คือการกำหนดค่าเชิงประกาศสำหรับฐานข้อมูล ซึ่งหมายความว่ากฎจะได้รับการกำหนดแยกต่างหากจากตรรกะของผลิตภัณฑ์ ซึ่งมีข้อดีหลายประการ ได้แก่ ไคลเอ็นต์ไม่ต้องรับผิดชอบในการบังคับใช้การรักษาความปลอดภัย การติดตั้งใช้งานที่มีข้อบกพร่องจะไม่ทำให้ข้อมูลของคุณถูกบุกรุก และที่สำคัญที่สุดคือไม่จำเป็นต้องมีผู้ตัดสินระดับกลาง เช่น เซิร์ฟเวอร์ เพื่อปกป้องข้อมูลจากโลกภายนอก
หัวข้อนี้อธิบายไวยากรณ์และโครงสร้างพื้นฐานของกฎความปลอดภัยของ Realtime Database ที่ใช้สร้างชุดกฎที่สมบูรณ์
การจัดโครงสร้างกฎความปลอดภัย
กฎความปลอดภัยของ Realtime Database ประกอบด้วยนิพจน์ที่คล้ายกับ JavaScript ซึ่งอยู่ในเอกสาร JSON โครงสร้างของกฎควรเป็นไปตามโครงสร้างของข้อมูลที่คุณจัดเก็บไว้ในฐานข้อมูล
กฎพื้นฐานจะระบุชุดโหนดที่จะรักษาความปลอดภัย วิธีการเข้าถึง (เช่น อ่าน เขียน) ที่เกี่ยวข้อง และเงื่อนไขที่อนุญาตหรือปฏิเสธการเข้าถึง
ในตัวอย่างต่อไปนี้ เงื่อนไขของเราจะเป็นคำสั่ง true และ false แบบง่ายๆ แต่ในหัวข้อถัดไป เราจะพูดถึงวิธีแสดงเงื่อนไขแบบไดนามิกมากขึ้น
ตัวอย่างเช่น หากเราพยายามรักษาความปลอดภัยให้กับ child_node ภายใต้ parent_node ไวยากรณ์ทั่วไปที่ต้องปฏิบัติตามมีดังนี้
{
"rules": {
"parent_node": {
"child_node": {
".read": <condition>,
".write": <condition>,
".validate": <condition>,
}
}
}
}มาลองใช้รูปแบบนี้กัน ตัวอย่างเช่น สมมติว่าคุณกำลังติดตามรายการข้อความและมีข้อมูลที่มีลักษณะดังนี้
{
"messages": {
"message0": {
"content": "Hello",
"timestamp": 1405704370369
},
"message1": {
"content": "Goodbye",
"timestamp": 1405704395231
},
...
}
}กฎของคุณควรมีโครงสร้างในลักษณะเดียวกัน ต่อไปนี้เป็นชุดกฎสำหรับการรักษาความปลอดภัยแบบอ่านอย่างเดียวที่อาจเหมาะสมกับโครงสร้างข้อมูลนี้ ตัวอย่างนี้แสดงวิธีระบุโหนดฐานข้อมูลที่ใช้กฎและเงื่อนไขในการประเมินกฎที่โหนดเหล่านั้น
{ "rules": { // For requests to access the 'messages' node... "messages": { // ...and the individual wildcarded 'message' nodes beneath // (we'll cover wildcarding variables more a bit later).... "$message": { // For each message, allow a read operation if <condition>. In this // case, we specify our condition as "true", so read access is always granted. ".read": "true", // For read-only behavior, we specify that for write operations, our // condition is false. ".write": "false" } } } }
การดำเนินการตามกฎพื้นฐาน
กฎสำหรับการบังคับใช้การรักษาความปลอดภัยมี 3 ประเภทตามประเภทของการดำเนินการที่ทำกับข้อมูล ได้แก่ .write, .read และ .validate สรุปวัตถุประสงค์ของแต่ละรายการโดยย่อมีดังนี้
| ประเภทของกฎ | |
|---|---|
| .read | อธิบายว่าผู้ใช้ได้รับอนุญาตให้อ่านข้อมูลหรือไม่และเมื่อใด |
| .write | อธิบายว่าอนุญาตให้เขียนข้อมูลหรือไม่และเมื่อใด |
| .validate | กำหนดลักษณะของค่าที่มีรูปแบบถูกต้อง ไม่ว่าจะมีแอตทริบิวต์ย่อยหรือไม่ และประเภทข้อมูล |
ตัวแปรการบันทึกไวลด์การ์ด
คำสั่งกฎทั้งหมดจะชี้ไปยังโหนด คำสั่งสามารถชี้ไปยังโหนดที่เฉพาะเจาะจงหรือใช้$ไวลด์การ์ดตัวแปรการบันทึกเพื่อชี้ไปยังชุดโหนดที่ระดับของลำดับชั้น ใช้ตัวแปรการบันทึกเหล่านี้เพื่อจัดเก็บค่าของคีย์โหนดเพื่อใช้ภายในคำสั่งกฎที่ตามมา เทคนิคนี้ช่วยให้คุณเขียนSecurity Rulesเงื่อนไขที่ซับซ้อนมากขึ้นได้ ซึ่งเราจะอธิบายอย่างละเอียดยิ่งขึ้นในหัวข้อถัดไป
{ "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 }
}
}
}กฎการอ่านและเขียนแบบเรียงซ้อน
กฎ .read และ .write จะทำงานจากบนลงล่าง โดยกฎที่อยู่ตื้นกว่าจะลบล้างกฎที่อยู่ลึกกว่า หากกฎให้สิทธิ์อ่านหรือเขียนในเส้นทางหนึ่งๆ กฎนั้นจะให้สิทธิ์เข้าถึงโหนดระดับล่างทั้งหมดที่อยู่ภายใต้โหนดนั้นด้วย ลองพิจารณาโครงสร้างต่อไปนี้
{
"rules": {
"foo": {
// allows read to /foo/*
".read": "data.child('baz').val() === true",
"bar": {
/* ignored, since read was allowed already */
".read": false
}
}
}
}โครงสร้างความปลอดภัยนี้อนุญาตให้อ่าน /bar/ ได้ทุกเมื่อที่ /foo/ มี baz ย่อยที่มีค่า true
กฎ ".read": false ใน /foo/bar/ ไม่มีผลที่นี่ เนื่องจากไม่สามารถเพิกถอนสิทธิ์เข้าถึงโดยเส้นทางย่อย
แม้ว่าอาจดูไม่เข้าใจง่ายในทันที แต่ก็เป็นส่วนสำคัญของภาษาของกฎและช่วยให้ใช้สิทธิ์การเข้าถึงที่ซับซ้อนมากได้โดยใช้ความพยายามเพียงเล็กน้อย เราจะอธิบายเรื่องนี้เมื่อพูดถึงความปลอดภัยตามผู้ใช้ในส่วนถัดไปของคู่มือนี้
โปรดทราบว่ากฎ .validate จะไม่มีผลแบบเรียงซ้อน กฎการตรวจสอบทั้งหมดต้องเป็นไปตามเงื่อนไขในทุกระดับของลำดับชั้นเพื่อให้สามารถเขียนได้
กฎไม่ใช่ตัวกรอง
ระบบจะใช้กฎในลักษณะอะตอมมิก ซึ่งหมายความว่าการดำเนินการอ่านหรือเขียนจะล้มเหลวทันทีหากไม่มีกฎในตำแหน่งนั้นหรือในตำแหน่งหลักที่ให้สิทธิ์เข้าถึง แม้ว่าเส้นทางย่อยที่ได้รับผลกระทบทั้งหมดจะเข้าถึงได้ แต่การอ่านที่ตำแหน่งหลักจะล้มเหลวโดยสิ้นเชิง ลองพิจารณาโครงสร้างต่อไปนี้
{
"rules": {
"records": {
"rec1": {
".read": true
},
"rec2": {
".read": false
}
}
}
}หากไม่เข้าใจว่ากฎได้รับการประเมินแบบแยกส่วน ก็อาจดูเหมือนว่าการดึงข้อมูลเส้นทาง /records/ จะแสดงผล rec1 แต่ไม่แสดงผล rec2 แต่ผลลัพธ์จริงคือข้อผิดพลาดต่อไปนี้
JavaScript
var db = firebase.database(); db.ref("records").once("value", function(snap) { // success method is not called }, function(err) { // error callback triggered with PERMISSION_DENIED });
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // success block is not called } withCancelBlock:^(NSError * _Nonnull error) { // cancel block triggered with PERMISSION_DENIED }];
Swift
var ref = FIRDatabase.database().reference() ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in // success block is not called }, withCancelBlock: { error in // cancel block triggered with PERMISSION_DENIED })
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("records"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { // success method is not called } @Override public void onCancelled(FirebaseError firebaseError) { // error callback triggered with PERMISSION_DENIED }); });
REST
curl https://docs-examples.firebaseio.com/rest/records/ # response returns a PERMISSION_DENIED error
เนื่องจากการดำเนินการอ่านที่ /records/ เป็นแบบอะตอมมิก และไม่มีกฎการอ่านที่ให้สิทธิ์เข้าถึงข้อมูลทั้งหมดภายใต้ /records/ การดำเนินการนี้จะแสดงข้อผิดพลาด PERMISSION_DENIED หากเราประเมินกฎนี้ในเครื่องจำลองความปลอดภัยในคอนโซล Firebase เราจะเห็นว่าการดำเนินการอ่านถูกปฏิเสธเนื่องจากไม่มีกฎการอ่านที่อนุญาตให้เข้าถึงเส้นทาง /records/ อย่างไรก็ตาม โปรดทราบว่าระบบไม่เคยประเมินกฎสำหรับ rec1 เนื่องจากไม่ได้อยู่ในเส้นทางที่เราร้องขอ หากต้องการดึงข้อมูล rec1 เราจะต้องเข้าถึงโดยตรง ดังนี้
JavaScript
var db = firebase.database(); db.ref("records/rec1").once("value", function(snap) { // SUCCESS! }, function(err) { // error callback is not called });
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // SUCCESS! }];
Swift
var ref = FIRDatabase.database().reference() ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in // SUCCESS! })
Java
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("records/rec1"); ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { // SUCCESS! } @Override public void onCancelled(FirebaseError firebaseError) { // error callback is not called } });
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1 # SUCCESS!
คำสั่งที่ทับซ้อนกัน
กฎมากกว่า 1 ข้ออาจมีผลกับโหนดได้ ในกรณีที่นิพจน์กฎหลายรายการระบุโหนด วิธีการเข้าถึงจะถูกปฏิเสธหากเงื่อนไขใดก็ตามเป็น false:
{
"rules": {
"messages": {
// A rule expression that applies to all nodes in the 'messages' node
"$message": {
".read": "true",
".write": "true"
},
// A second rule expression applying specifically to the 'message1` node
"message1": {
".read": "false",
".write": "false"
}
}
}
}ในตัวอย่างข้างต้น การอ่านไปยังโหนด message1 จะถูกปฏิเสธเนื่องจากกฎข้อที่ 2 เป็น false เสมอ แม้ว่ากฎข้อแรกจะเป็น true เสมอก็ตาม
ขั้นตอนถัดไป
คุณสามารถทำความเข้าใจกฎความปลอดภัยของฐานข้อมูลเรียลไทม์ของ Firebase ให้ลึกซึ้งยิ่งขึ้นได้โดยทำดังนี้
เรียนรู้แนวคิดหลักถัดไปของภาษา Security Rules ซึ่งก็คือเงื่อนไขแบบไดนามิกที่ช่วยให้ Security Rules ตรวจสอบการให้สิทธิ์ผู้ใช้ เปรียบเทียบข้อมูลที่มีอยู่กับข้อมูลขาเข้า ตรวจสอบข้อมูลขาเข้า ตรวจสอบโครงสร้างของคำค้นหาที่มาจากไคลเอ็นต์ และอื่นๆ
ตรวจสอบกรณีการใช้งานด้านความปลอดภัยทั่วไปและคำจำกัดความของกฎความปลอดภัยของ Firebase ที่จัดการกรณีการใช้งานเหล่านั้น