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 bạn. 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 bất kỳ thứ gì, từ mọi lượt ghi vào cơ sở dữ liệu cho đến hoạt động trên từng nút.

Quy tắc bảo mật cơ sở dữ liệu theo 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. Chiến dịch này có một số ưu điểm: ứng dụng không chịu trách nhiệm thực thi bảo mật, lỗi sẽ không ảnh hưởng xấu đến dữ liệu của bạn và có lẽ quan trọng nhất là không cần trọng tài trung gian, chẳng hạn như máy chủ, để bảo vệ dữ liệu từ thế giới.

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

Xây dựng các quy tắc bảo mật

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

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

Chẳng hạn nếu chúng ta đang cố gắng bảo mật một child_node trong parent_node, 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 giống 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ự. Sau đây là một tập hợp các quy tắc bảo mật chỉ đọc có thể phù hợp với cấu trúc dữ liệu này. Chiến dịch này ví dụ minh hoạ cách chúng ta chỉ định các nút cơ sở dữ liệu và những quy tắc sẽ được áp dụng điều kiện để đánh giá 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 quy tắc cơ bản

Có ba loại quy tắc để thực thi bảo mật dựa trên loại đang thực hiện thao tác trên dữ liệu: .write, .read.validate. Ở đây là phần tóm tắt nhanh mục đích của chúng:

Loại quy tắc
.đọc Mô tả trường hợp và trường hợp người dùng được phép đọc dữ liệu.
.write Mô tả trường hợp và trường hợp được phép ghi dữ liệu.
xác thực Xác định hình thức hiển thị của một giá trị được định dạng chính xác, cho dù giá trị đó có các thuộc tính con và kiểu 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 hoặc sử dụng biến thu thập ký tự đại diện $ để trỏ đến tập hợp các nút tại một trong hệ thống phân cấp. Sử dụng các biến thu thập này để lưu trữ giá trị của 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 phức tạp hơn của Quy tắc mà chúng tôi sẽ đề cập chi tiết hơn 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 các biến $ động song song với đường dẫn không đổi tên. Trong ví dụ này, chúng ta sẽ dùng biến $other để khai báo một quy tắc .validate để đảm bảo rằng widget không có phần tử con nào khác ngoài titlecolor. Mọi hoạt động ghi dẫn đến việc tạo thêm các phần tử con đề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 }
    }
  }
}

Đọc và ghi tầng quy tắc

Quy tắc .read.write hoạt động từ trên xuống với mô hình hẹp hơn các quy tắc ghi đè quy tắc sâu hơn. Nếu một quy tắc cấp quyền đọc hoặc ghi cho một đường dẫn đó, thì công cụ này cũng cấp quyền truy cập vào tất cả các nút con ở bên dưới. 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/ mọi lúc /foo/ chứa phần tử con baz có giá trị true. Quy tắc ".read": false trong /foo/bar/ không có có hiệu lực tại đây, do đường dẫn con không thể thu hồi quyền truy cập.

Mặc dù có vẻ không trực quan ngay lập tức, nhưng đây là một phần quan trọng 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 mà không tốn nhiều công sức. Chiến dịch này sẽ được minh hoạ khi đề cập đến cơ chế bảo mật dựa trên người dùng ở phần sau của hướng dẫn này.

Lưu ý rằng các quy tắc .validate không phân tầng. Tất cả quy tắc xác thực phải được đáp ứng ở tất cả các cấp trong hệ phân cấp thì mới được phép ghi.

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

Các quy tắc được áp dụng một cách tỉ mỉ. Tức là lượt đọc hoặc ghi không thực hiện được thao tác ngay lập tức nếu không có quy tắc tại vị trí đó hoặc tại vị trí của cha mẹ cấp quyền truy cập. Ngay cả khi mọi đường dẫn con bị ảnh hưởng đều có thể truy cập được, việc đọc tại vị trí chính sẽ không thành công hoàn toàn. 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 chi tiết, có vẻ như chẳng hạn như 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 dùng được trên mục tiêu App Clip (Đoạn video ứng dụng).
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 dùng được trên mục tiêu App Clip (Đoạn video ứng dụng).
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ì hoạt động đọc tại /records/ là nguyên tử và không có đọc quy tắc cấp quyền truy cập vào tất cả dữ liệu trong /records/, thao tác này sẽ tạo ra lỗi PERMISSION_DENIED. Nếu chúng tôi đánh giá điều này trong trình mô phỏng bảo mật trong bảng điều khiển của Firebase, chúng ta có thể thấy rằng 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ì 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 tệp đó:

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 dùng được trên mục tiêu App Clip (Đoạn video ứng dụng).
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 dùng được trên mục tiêu App Clip (Đoạn video ứng dụng).
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

Có thể áp dụng nhiều quy tắc cho một nút. Trong trong trường hợp nhiều biểu thức quy tắc xác định một nút, thì phương thức truy cập là 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, giá trị đọc vào nút message1 sẽ là bị từ chối vì quy tắc thứ hai luôn là false, mặc dù quy tắc đầu tiên quy tắc 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ơ sở dữ liệu theo thời gian thực của Firebase: