Sử dụng các điều kiện trong Quy tắc bảo mật cơ sở dữ liệu theo thời gian thực

Hướng dẫn này dựa trên hướng dẫn tìm hiểu ngôn ngữ cốt lõi của Quy tắc bảo mật Firebase để cho biết cách thêm điều kiện vào Quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực của Firebase.

Thành phần cơ bản của Quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực là điều kiện. Điều kiện là một biểu thức Boolean xác định xem có nên cho phép hay từ chối một thao tác cụ thể. Đối với các quy tắc cơ bản, việc sử dụng các giá trị cố định truefalse làm điều kiện hoạt động rất tốt. Tuy nhiên, ngôn ngữ Quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực cung cấp cho bạn các cách viết điều kiện phức tạp hơn có thể:

  • Kiểm tra quá trình xác thực người dùng
  • Đánh giá dữ liệu hiện có dựa trên dữ liệu mới gửi
  • Truy cập và so sánh các phần khác nhau của cơ sở dữ liệu
  • Xác thực dữ liệu đến
  • Sử dụng cấu trúc của các truy vấn đến cho logic bảo mật

Sử dụng biến $ để thu thập các phân đoạn đường dẫn

Bạn có thể thu thập các phần của đường dẫn để đọc hoặc ghi bằng cách khai báo các biến thu thập có tiền tố $. Điều này đóng vai trò là ký tự đại diện và lưu trữ giá trị của khoá đó để sử dụng bên trong các điều kiện quy tắc:

{
  "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 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 một 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 thao tác ghi dẫn đến việc tạo thêm phần tử con 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 }
    }
  }
}

Xác thực

Một trong những mẫu quy tắc bảo mật phổ biến nhất là kiểm soát quyền truy cập dựa trên trạng thái xác thực của người dùng. Ví dụ: ứng dụng của bạn có thể chỉ cho phép người dùng đã đăng nhập ghi dữ liệu.

Nếu ứng dụng của bạn sử dụng Xác thực Firebase, thì biến request.auth sẽ chứa thông tin xác thực cho ứng dụng yêu cầu dữ liệu. Để biết thêm thông tin về request.auth, hãy xem tài liệu tham khảo.

Firebase Authentication tích hợp với Firebase Realtime Database để cho phép bạn kiểm soát quyền truy cập dữ liệu theo từng người dùng bằng cách sử dụng các điều kiện. Sau khi người dùng xác thực, biến auth trong các quy tắc Quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực sẽ được điền sẵn thông tin của người dùng. Thông tin này bao gồm giá trị nhận dạng riêng biệt (uid) cũng như dữ liệu tài khoản được liên kết, chẳng hạn như mã Facebook hoặc địa chỉ email và các thông tin khác. Nếu triển khai nhà cung cấp dịch vụ xác thực tuỳ chỉnh, bạn có thể thêm các trường của riêng mình vào tải trọng xác thực của người dùng.

Phần này giải thích cách kết hợp ngôn ngữ Quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực của Firebase với thông tin xác thực về người dùng. Bằng cách kết hợp hai khái niệm này, bạn có thể kiểm soát quyền truy cập vào dữ liệu dựa trên danh tính người dùng.

Biến auth

Biến auth được xác định trước trong các quy tắc là giá trị rỗng trước khi quá trình xác thực diễn ra.

Sau khi người dùng được xác thực bằng Xác thực Firebase biến này sẽ chứa các thuộc tính sau:

nhà cung cấp Phương thức xác thực được sử dụng ("password", "anonymous", "facebook", "github", "google", hoặc "twitter").
uid Mã người dùng duy nhất, đảm bảo là duy nhất trên tất cả các nhà cung cấp.
token Nội dung của mã thông báo mã nhận dạng Firebase Auth. Hãy xem tài liệu tham khảo về auth.token để biết thêm thông tin chi tiết.

Dưới đây là một quy tắc mẫu sử dụng biến auth để đảm bảo rằng mỗi người dùng chỉ có thể ghi vào một đường dẫn dành riêng cho người dùng:

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

Cấu trúc cơ sở dữ liệu để hỗ trợ các điều kiện xác thực

Thông thường, bạn nên cấu trúc cơ sở dữ liệu theo cách giúp viết Security Rules dễ dàng hơn. Một mẫu phổ biến để lưu trữ dữ liệu người dùng trong Realtime Database là lưu trữ tất cả người dùng của bạn trong một nút users duy nhất có các phần tử con là giá trị uid cho mỗi người dùng. Nếu bạn muốn hạn chế quyền truy cập vào dữ liệu này để chỉ người dùng đã đăng nhập mới có thể xem dữ liệu của riêng họ, thì các quy tắc của bạn sẽ có dạng như sau.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

Làm việc với các yêu cầu tuỳ chỉnh về xác thực

Đối với các ứng dụng yêu cầu kiểm soát quyền truy cập tuỳ chỉnh cho nhiều người dùng, Firebase Authentication cho phép nhà phát triển đặt các yêu cầu trên người dùng Firebase. Bạn có thể truy cập các yêu cầu này trong biến auth.token trong các quy tắc. Dưới đây là một ví dụ về các quy tắc sử dụng yêu cầu tuỳ chỉnh hasEmergencyTowel:

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

Nhà phát triển tạo mã thông báo xác thực tuỳ chỉnh của riêng họ có thể tuỳ ý thêm các yêu cầu vào các mã thông báo này. Các yêu cầu này có trên biến auth.token trong các quy tắc.

Dữ liệu hiện có so với dữ liệu mới

Biến data được xác định trước dùng để tham chiếu đến dữ liệu trước khi thao tác ghi diễn ra. Ngược lại, biến newData chứa dữ liệu mới sẽ tồn tại nếu thao tác ghi thành công. newData đại diện cho kết quả hợp nhất của dữ liệu mới đang được ghi và dữ liệu hiện có.

Để minh hoạ, quy tắc này sẽ cho phép chúng ta tạo bản ghi mới hoặc xoá bản ghi hiện có, nhưng không cho phép thay đổi dữ liệu hiện có không phải là giá trị rỗng:

// 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()"

Tham chiếu dữ liệu trong các đường dẫn khác

Bạn có thể sử dụng mọi dữ liệu làm tiêu chí cho các quy tắc. Bằng cách sử dụng các biến được xác định trước root, datanewData, chúng ta có thể truy cập vào bất kỳ đường dẫn nào như đường dẫn đó sẽ tồn tại trước hoặc sau sự kiện ghi.

Hãy xem xét ví dụ này. Ví dụ này cho phép các thao tác ghi miễn là giá trị của nút /allow_writes/true, nút mẹ không đặt cờ readOnly và có một phần tử con tên là foo trong dữ liệu mới được ghi:

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

Xác thực dữ liệu

Bạn nên thực thi cấu trúc dữ liệu và xác thực định dạng cũng như nội dung của dữ liệu bằng cách sử dụng các quy tắc .validate. Các quy tắc này chỉ chạy sau khi quy tắc .write thành công để cấp quyền truy cập. Dưới đây là định nghĩa quy tắc .validate mẫu chỉ cho phép ngày ở định dạng YYYY-MM-DD trong khoảng thời gian từ năm 1900 đến năm 2099. Định dạng này được kiểm tra bằng biểu thức chính quy.

".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])$/)"

Quy tắc .validate là loại quy tắc bảo mật duy nhất không xếp tầng. Nếu bất kỳ quy tắc xác thực nào không thành công trên bất kỳ bản ghi con nào, thì toàn bộ thao tác ghi sẽ bị từ chối. Ngoài ra, các định nghĩa xác thực sẽ bị bỏ qua khi dữ liệu bị xoá (tức là khi giá trị mới đang được ghi là null).

Những điểm này có vẻ không quan trọng, nhưng thực tế lại là các tính năng quan trọng để viết Quy tắc bảo mật của Cơ sở dữ liệu thời gian thực Firebase mạnh mẽ. Hãy cân nhắc các quy tắc sau:

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

Hãy xem xét biến thể này và xem kết quả cho các thao tác ghi sau:

JavaScript
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
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu App Clip.
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
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu App Clip.
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);
Java
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);
Kiến trúc chuyển trạng thái đại diện (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

Bây giờ, hãy xem xét cùng một cấu trúc, nhưng sử dụng các quy tắc .write thay vì .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()"
      }
    }
  }
}

Trong biến thể này, bất kỳ thao tác nào sau đây sẽ thành công:

JavaScript
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
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu App Clip.
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
Lưu ý: Sản phẩm Firebase này không có trên mục tiêu App Clip.
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)
Java
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);
Kiến trúc chuyển trạng thái đại diện (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

Điều này minh hoạ sự khác biệt giữa các quy tắc .write.validate. Như đã minh hoạ, bạn nên viết tất cả các quy tắc này bằng .validate, ngoại trừ quy tắc newData.hasChildren(). Quy tắc này sẽ phụ thuộc vào việc có nên cho phép xoá hay không.

Quy tắc dựa trên truy vấn

Mặc dù không thể sử dụng quy tắc làm bộ lọc, nhưng bạn có thể giới hạn quyền truy cập vào các tập hợp con của dữ liệu bằng cách sử dụng các tham số truy vấn trong quy tắc. Sử dụng biểu thức query. trong các quy tắc để cấp quyền truy cập đọc hoặc ghi dựa trên các tham số truy vấn.

Ví dụ: quy tắc dựa trên truy vấn sau đây sử dụng quy tắc bảo mật dựa trên người dùng và quy tắc dựa trên truy vấn để hạn chế quyền truy cập vào dữ liệu trong tập hợp baskets chỉ cho các giỏ hàng mà người dùng đang hoạt động sở hữu:

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

Truy vấn sau đây (bao gồm các tham số truy vấn trong quy tắc) sẽ thành công:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

Tuy nhiên, các truy vấn không bao gồm các tham số trong quy tắc sẽ không thành công và gặp lỗi PermissionDenied:

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

Bạn cũng có thể sử dụng các quy tắc dựa trên truy vấn để giới hạn lượng dữ liệu mà ứng dụng tải xuống thông qua các thao tác đọc.

Ví dụ: quy tắc sau đây giới hạn quyền truy cập đọc chỉ cho 1.000 kết quả đầu tiên của một truy vấn, theo thứ tự ưu tiên:

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)

Bạn có thể sử dụng các biểu thức query. sau đây trong Quy tắc bảo mật của Cơ sở dữ liệu theo thời gian thực.

Biểu thức quy tắc dựa trên truy vấn
Biểu thức Loại Nội dung mô tả
query.orderByKey
query.orderByPriority
query.orderByValue
boolean Đúng đối với các truy vấn được sắp xếp theo khoá, mức độ ưu tiên hoặc giá trị. Sai trong trường hợp khác.
query.orderByChild string
null
Sử dụng chuỗi để biểu thị đường dẫn tương đối đến một nút con. Ví dụ: query.orderByChild === "address/zip". Nếu truy vấn không được sắp xếp theo một nút con, thì giá trị này sẽ là giá trị rỗng.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Truy xuất ranh giới của truy vấn đang thực thi hoặc trả về giá trị rỗng nếu không có ranh giới nào được đặt.
query.limitToFirst
query.limitToLast
number
null
Truy xuất giới hạn của truy vấn đang thực thi hoặc trả về giá trị rỗng nếu không có giới hạn nào được đặt.

Các bước tiếp theo

Sau khi thảo luận về các điều kiện, bạn đã hiểu rõ hơn về Security Rules và sẵn sàng:

Tìm hiểu cách xử lý các trường hợp sử dụng cốt lõi và tìm hiểu quy trình làm việc để phát triển, kiểm thử và triển khai Security Rules:

Tìm hiểu các tính năng dành riêng cho Realtime Database:Security Rules