Tìm hiểu cú pháp cốt lõi của ngôn ngữ Quy tắc bảo mật cơ sở dữ liệu theo thời gian thực

Quy tắc bảo mật cơ sở dữ liệu theo thời gian thực của Firebase cho phép bạn kiểm soát quyền truy cập vào dữ liệu được lưu trữ trong cơ sở dữ liệu của mình. Cú pháp quy tắc linh hoạt cho phép bạn tạo các quy tắc khớp với mọi thứ, từ tất cả các hoạt động ghi vào cơ sở dữ liệu cho đến các thao tác trên từng nút.

Quy tắc bảo mật của Cơ sở dữ liệu thời gian thực là cấu hình khai báo cho cơ sở dữ liệu của bạn. Điều này có nghĩa là các quy tắc được xác định riêng biệt với logic sản phẩm. Cách này có một số ưu điểm: ứng dụng không chịu trách nhiệm thực thi tính bảo mật, việc triển khai bị lỗi sẽ không ảnh hưởng đến dữ liệu của bạn và có lẽ quan trọng nhất là bạn không cần một trọng tài trung gian (chẳng hạn như máy chủ) để bảo vệ dữ liệu khỏi thế giới.

Chủ đề này mô tả cú pháp và cấu trúc cơ bản của Quy tắc bảo mật cơ sở dữ liệu theo thời gian thực dùng để tạo các bộ quy tắc hoàn chỉnh.

Xây dựng cấu trúc cho quy tắc bảo mật

Quy tắc bảo mật cho cơ sở dữ liệu theo thời gian thực bao gồm các biểu thức giống như JavaScript trong một tài liệu JSON. Cấu trúc của quy tắc phải tuân theo cấu trúc của dữ liệu mà bạn đã lưu trữ trong cơ sở dữ liệu.

Quy tắc cơ bản xác định một tập hợp các nút cần bảo mật, phương thức truy cập (ví dụ: đọc, ghi) liên quan và điều kiện cho phép hoặc từ chối quyền truy cập. Trong các ví dụ sau, điều kiện của chúng ta sẽ là các câu lệnh truefalse đơn giản, nhưng trong chủ đề tiếp theo, chúng ta sẽ đề cập đến các cách linh động hơn để thể hiện điều kiện.

Ví dụ: nếu chúng ta đang cố gắng bảo mật child_node trong parent_node, thì cú pháp chung cần tuân theo là:

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

Hãy áp dụng mẫu này. Ví dụ: giả sử bạn đang theo dõi một danh sách tin nhắn và có dữ liệu như sau:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

Các quy tắc của bạn nên có cấu trúc theo cách tương tự. Dưới đây là một bộ quy tắc về bảo mật chỉ đọc có thể phù hợp với cấu trúc dữ liệu này. Ví dụ này minh hoạ cách chúng ta chỉ định các nút cơ sở dữ liệu áp dụng cho các quy tắc và điều kiện để đánh giá các quy tắc tại các nút đó.

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

Thao tác cơ bản với quy tắc

Có 3 loại quy tắc để thực thi bảo mật dựa trên loại thao tác được thực hiện trên dữ liệu: .write, .read.validate. Dưới đây là thông tin tóm tắt nhanh về mục đích của các loại mã này:

Loại quy tắc
.read Mô tả liệu người dùng có được phép đọc dữ liệu hay không và khi nào được phép.
.write Mô tả liệu có được phép ghi dữ liệu hay không và khi nào được phép ghi dữ liệu.
.validate Xác định giao diện của một giá trị được định dạng chính xác, liệu giá trị đó có thuộc tính con hay không và loại dữ liệu.

Biến thu thập ký tự đại diện

Mọi câu lệnh cho quy tắc đều trỏ đến các nút. Một câu lệnh có thể trỏ đến một nút cụ thể hoặc sử dụng ký tự đại diện $ thu thập các biến để trỏ đến các tập hợp nút ở cấp hệ phân cấp. Sử dụng các biến thu thập này để lưu trữ giá trị của khoá nút để sử dụng bên trong các câu lệnh quy tắc tiếp theo. Kỹ thuật này cho phép bạn viết các điều kiện Rules phức tạp hơn. Chúng ta sẽ đề cập chi tiết hơn về điều này trong chủ đề tiếp theo.

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

Bạn cũng có thể sử dụng song song các biến $ động với tên đường dẫn hằng số. Trong ví dụ này, chúng ta đang sử dụng biến $other để khai báo quy tắc .validate nhằm đảm bảo rằng widget không có phần tử con nào khác ngoài titlecolor. Mọi lượt ghi khiến các phần tử con khác được tạo đều sẽ không thành công.

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

Quy tắc đọc và ghi theo kiểu thác nước

Các quy tắc .read.write hoạt động từ trên xuống dưới, trong đó các quy tắc nông hơn sẽ ghi đè các quy tắc sâu hơn. Nếu một quy tắc cấp quyền đọc hoặc ghi tại một đường dẫn cụ thể, thì quy tắc đó cũng cấp quyền truy cập vào tất cả nút con trong đó. Hãy xem xét cấu trúc sau:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

Cấu trúc bảo mật này cho phép đọc /bar/ bất cứ khi nào /foo/ chứa một baz con có giá trị true. Quy tắc ".read": false trong /foo/bar/ không có hiệu lực ở đây vì quyền truy cập không thể bị thu hồi theo đường dẫn con.

Mặc dù có vẻ không trực quan ngay lập tức, nhưng đây là một phần mạnh mẽ của ngôn ngữ quy tắc và cho phép triển khai các đặc quyền truy cập rất phức tạp với ít nỗ lực nhất. Điều này sẽ được minh hoạ khi chúng ta tìm hiểu về tính năng bảo mật dựa trên người dùng ở phần sau của hướng dẫn này.

Xin lưu ý rằng các quy tắc .validate không được áp dụng theo kiểu thác nước. Tất cả các quy tắc xác thực phải được đáp ứng ở tất cả các cấp của hệ phân cấp để cho phép ghi.

Quy tắc không phải là bộ lọc

Các quy tắc được áp dụng theo cách nguyên tử. Điều đó có nghĩa là thao tác đọc hoặc ghi sẽ ngay lập tức không thành công nếu không có quy tắc tại vị trí đó hoặc tại vị trí mẹ cấp quyền truy cập. Ngay cả khi có thể truy cập vào mọi đường dẫn con bị ảnh hưởng, việc đọc ở vị trí mẹ sẽ hoàn toàn không thành công. Hãy xem xét cấu trúc sau:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

Nếu không hiểu rằng các quy tắc được đánh giá một cách nguyên tử, có thể bạn sẽ thấy việc tìm nạp đường dẫn /records/ sẽ trả về rec1 nhưng không phải rec2. Tuy nhiên, kết quả thực tế là một lỗi:

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
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu Đoạn video ngắn.
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
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu Đoạn video ngắn.
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
  });
});
Kiến trúc chuyển trạng thái đại diện (REST)
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Vì thao tác đọc tại /records/ là nguyên tử và không có quy tắc đọc nào cấp quyền truy cập vào tất cả dữ liệu trong /records/, nên thao tác này sẽ gửi lỗi PERMISSION_DENIED. Nếu đánh giá quy tắc này trong trình mô phỏng bảo mật của bảng điều khiển Firebase, chúng ta có thể thấy thao tác đọc bị từ chối do không có quy tắc đọc nào cho phép truy cập vào đường dẫn /records/. Tuy nhiên, xin lưu ý rằng quy tắc cho rec1 chưa bao giờ được đánh giá vì quy tắc này không nằm trong đường dẫn mà chúng tôi yêu cầu. Để tìm nạp rec1, chúng ta cần truy cập trực tiếp vào rec1:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu Đoạn video ngắn.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu Đoạn video ngắn.
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
  }
});
Kiến trúc chuyển trạng thái đại diện (REST)
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Câu lệnh trùng lặp

Một nút có thể áp dụng nhiều quy tắc. Trong trường hợp nhiều biểu thức quy tắc xác định một nút, phương thức truy cập sẽ bị từ chối nếu bất kỳ điều kiện nào là 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"
      }
    }
  }
}

Trong ví dụ trên, các lượt đọc đến nút message1 sẽ bị từ chối vì quy tắc thứ hai luôn là false, mặc dù quy tắc đầu tiên luôn là true.

Các bước tiếp theo

Bạn có thể hiểu rõ hơn về Quy tắc bảo mật của Cơ sở dữ liệu thời gian thực Firebase: