Buka konsol

Mengamankan Data

Aturan Realtime Database adalah konfigurasi deklaratif untuk database Anda. Artinya, penetapan aturan terpisah dari logika produk. Hal ini memiliki sejumlah keuntungan: klien tidak bertanggung jawab untuk memberlakukan keamanan, implementasi yang berisi bug tidak akan merusak data Anda, dan mungkin yang paling penting, tidak perlu perantara, seperti server, untuk melindungi data dari dunia luar.

Membuat Struktur Aturan

Aturan Realtime Database terdiri dari ekspresi serupa JavaScript yang terdapat dalam dokumen JSON. Struktur aturan Anda harus mengikuti struktur data yang telah disimpan di database. Misalnya, katakanlah Anda memantau daftar pesan dan memiliki data yang terlihat seperti ini:

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

Aturan Anda harus disusun dengan cara yang sama. Berikut ini adalah contoh rangkaian aturan yang mungkin cocok untuk struktur data ini.

{
  "rules": {
    "messages": {
      "$message": {
        // only messages from the last ten minutes can be read
        ".read": "data.child('timestamp').val() > (now - 600000)",

        // new messages must have a string content and a number timestamp
        ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()"
      }
    }
  }
}

Tipe Aturan Keamanan

Ada 3 tipe aturan untuk meningkatkan keamanan: .write, .read, dan .validate. Berikut rangkuman ringkas kegunaannya:

Tipe Aturan
.read Menjelaskan apakah dan kapan data boleh dibaca oleh pengguna.
.write Menjelaskan apakah dan kapan data boleh ditulis.
.validate Menentukan tampilan nilai yang diformat dengan benar, apakah nilai memiliki atribut turunan, dan tipe data.

Variabel yang Telah Ditetapkan

Ada sejumlah variabel bermanfaat yang telah ditetapkan, yang bisa diakses dalam definisi aturan keamanan. Sebagian besar akan digunakan dalam contoh di bawah ini. Berikut adalah ringkasan setiap variabel dan link menuju referensi API yang sesuai.

Variabel yang Telah Ditetapkan
now Waktu saat ini dalam milidetik sejak era Linux. Ini bekerja dengan baik terutama untuk memvalidasi stempel waktu yang dibuat dengan firebase.database.ServerValue.TIMESTAMP SDK.
root RuleDataSnapshot yang mewakili lokasi root di database Firebase sebelum percobaan pengoperasian.
newData RuleDataSnapshot yang mewakili keadaan data setelah percobaan pengoperasian. Variabel ini mencakup data baru yang sedang ditulis dan data yang sudah ada.
data RuleDataSnapshot yang mewakili keadaan data sebelum percobaan pengoperasian.
$ variables Lokasi karakter pengganti yang digunakan untuk mewakili ID dan kunci turunan dinamis.
auth Mewakili payload token pengguna yang telah diautentikasi.

Variabel ini bisa digunakan di mana saja dalam aturan Anda. Misalnya, aturan keamanan di bawah ini memastikan bahwa data yang ditulis ke node /foo/ harus berupa string kurang dari 100 karakter:

{
  "rules": {
    "foo": {
      // /foo is readable by the world
      ".read": true,

      // /foo is writable by the world
      ".write": true,

      // data written to /foo must be a string less than 100 characters
      ".validate": "newData.isString() && newData.val().length < 100"
    }
  }
}

Data Yang Sudah Ada vs. Data Baru

Variabel data yang telah ditetapkan digunakan untuk merujuk ke data sebelum operasi tulis terjadi. Sebaliknya, variabel newData berisi data baru yang akan ada jika operasi tulis berhasil. newData mewakili hasil gabungan dari data yang baru ditulis dan data yang sudah ada.

Sebagai ilustrasi, aturan ini memungkinkan kita membuat data baru atau menghapus yang sudah ada, namun bukan untuk mengubah data bukan-null yang sudah ada:

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

Mereferensikan Data di Lokasi lain

Setiap data bisa digunakan sebagai kriteria untuk aturan. Dengan menggunakan variabel root, data, dan newData yang telah ditetapkan, kita dapat mengakses sembarang lokasi karena lokasi tersebut akan ada sebelum atau setelah peristiwa tulis.

Perhatikan contoh berikut, yang memungkinkan operasi tulis selama nilai node /allow_writes/ adalah true, node induk tidak memiliki tanda readOnly, dan terdapat node turunan bernama foo di data yang baru ditulis:

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

Aturan Menurun Baca dan Tulis

Aturan .read dan .write berfungsi dari atas ke bawah. Aturan yang lebih dangkal menimpa aturan yang lebih dalam. Jika aturan memberikan izin baca atau tulis pada lokasi tertentu, maka aturan itu juga memberikan akses ke semua node turunan di bawahnya. Pertimbangkan struktur berikut:

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

Struktur keamanan ini memungkinkan /bar/ untuk dibaca jika /foo/ berisi node turunan baz dengan nilai true. Aturan ".read": false di dalam /foo/bar/ tidak berpengaruh di sini karena akses tidak dapat dicabut oleh lokasi turunan.

Walaupun mungkin kelihatannya tidak intuitif, ini adalah bagian yang andal dari bahasa aturan dan memungkinkan hak istimewa akses yang sangat kompleks dapat diimplementasikan dengan usaha minimal. Gambarannya akan dijelaskan saat kita sampai ke topik keamanan berbasis pengguna pada bagian panduan selanjutnya.

Perhatikan bahwa aturan .validate tidak menurun. Semua aturan validasi harus terpenuhi di semua tingkat hierarki agar operasi tulis diizinkan.

Aturan Bukanlah Filter

Aturan diterapkan secara atomis. Artinya, operasi baca atau tulis akan langsung gagal jika tidak ada aturan di lokasi itu atau di lokasi induk yang memberikan akses. Sekalipun jika setiap lokasi turunan yang terpengaruh bisa diakses, operasi baca di lokasi induk akan gagal total. Perhatikan struktur ini:

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

Tanpa memahami bahwa aturan dievaluasi secara atomis, maka sepertinya mengambil lokasi /records/ akan menampilkan rec1 dan bukan rec2. Akan tetapi, hasil sesungguhnya adalah error:

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

Oleh karena operasi baca pada /records/ bersifat atomis, dan tidak ada aturan baca yang memberikan akses ke semua data pada /records/, ini akan menyebabkan error PERMISSION_DENIED. Jika mengevaluasi aturan ini dalam simulator keamanan di Firebase console, kita bisa melihat bahwa operasi baca ditolak:

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

Operasi ditolak karena tidak ada aturan baca yang mengizinkan akses ke lokasi /records/, namun perhatikan bahwa aturan untuk rec1 tidak pernah dievaluasi karena tidak berada di lokasi yang diminta. Untuk mengambil rec1, kita mengaksesnya secara langsung:

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!

Aturan Berbasis Kueri

Meskipun tidak dapat menggunakan aturan sebagai filter, Anda dapat membatasi akses ke subkumpulan data dengan menggunakan parameter kueri dalam aturan. Gunakan ekspresi query. dalam aturan Anda untuk memberikan akses baca atau tulis berdasarkan parameter kueri.

Misalnya, aturan berbasis kueri berikut menggunakan aturan keamanan berbasis pengguna dan aturan berbasis kueri untuk membatasi akses ke data yang ada di koleksi baskets hanya bagi keranjang belanja milik pengguna aktif:

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

Kueri berikut, yang mencakup parameter kueri dalam aturan, akan berhasil:

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

Namun, kueri yang tidak memasukkan parameter dalam aturan akan gagal dengan error PermissionDenied:

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

Anda juga dapat menggunakan aturan berbasis kueri untuk membatasi banyaknya data yang didownload klien melalui operasi baca.

Misalnya, aturan berikut membatasi akses baca hanya untuk 1.000 hasil kueri pertama, yang diurutkan berdasarkan prioritas:

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)

Ekspresi query. berikut tersedia dalam Aturan Realtime Database.

Ekspresi aturan berbasis kueri
Ekspresi Jenis Deskripsi
query.orderByKey
query.orderByPriority
query.orderByValue
boolean Benar untuk kueri yang diurutkan berdasarkan kunci, prioritas, atau nilai. Salah untuk kueri yang tidak diurutkan.
query.orderByChild string
null
Gunakan string untuk mewakili lokasi relatif ke node turunan. Misalnya, query.orderByChild == "address/zip". Jika kueri tidak diurutkan berdasarkan node turunan, nilai ini null.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Mengambil ikatan kueri pelaksana, atau menampilkan null jika tidak ada ikatan yang ditetapkan.
query.limitToFirst
query.limitToLast
number
null
Mengambil batasan kueri pelaksana, atau menampilkan null jika tidak ada batasan yang ditetapkan.

Memvalidasi Data

Memberlakukan struktur data serta memvalidasi format dan konten data harus dilakukan menggunakan aturan .validate, yang dijalankan hanya setelah aturan .write berhasil memberikan akses. Berikut adalah contoh definisi aturan .validate yang hanya mengizinkan tanggal dalam format YYYY-MM-DD antara tahun 1900-2099, yang diperiksa menggunakan ekspresi reguler.

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

Aturan .validate adalah satu-satunya tipe aturan keamanan yang tidak menurun. Jika ada aturan validasi yang gagal pada catatan turunan, maka seluruh operasi tulis akan ditolak. Selain itu, definisi validasi akan diabaikan jika data dihapus (yaitu ketika nilai baru yang ditulis berupa null).

Meskipun terkesan sepele, sebenarnya ini adalah fitur penting untuk menulis Aturan Firebase Realtime Database yang kuat. Perhatikan aturan berikut ini:

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

Dengan memerhatikan variasi ini, lihatlah hasil untuk operasi tulis berikut:

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
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);
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);
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

Kini lihat struktur yang sama, tapi dengan aturan .write sebagai ganti .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()"
      }
    }
  }
}

Dalam variasi ini, salah satu operasi berikut akan berhasil:

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
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)
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);
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

Hal ini menunjukkan perbedaan antara aturan .write dan .validate. Seperti yang ditunjukkan, semua aturan ini harus ditulis menggunakan .validate, dengan kemungkinan pengecualian aturan newData.hasChildren(), yang akan bergantung pada perizinan penghapusan.

Menggunakan Variabel $ untuk Menangkap Segmen Lokasi

Anda bisa menangkap bagian dari lokasi untuk membaca atau menulis dengan menyatakan variabel capture berawalan $. Hal ini berfungsi sebagai karakter pengganti, dan menyimpan nilai kunci tersebut untuk digunakan dalam deklarasi aturan:

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

Variabel $ dinamis juga dapat digunakan secara paralel dengan nama lokasi yang konstan. Dalam contoh ini, kita menggunakan variabel $other untuk mendeklarasikan aturan .validate yang memastikan bahwa widget tidak memiliki turunan selain title dan color. Setiap operasi tulis yang menyebabkan terciptanya turunan tambahan akan gagal.

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

Contoh Chat Anonim

Mari kita susun berbagai aturan dan membuat aplikasi chat anonim yang aman. Di sini kita akan mencantumkan aturan, dan versi chat yang berfungsi disertakan di bawah ini:

{
  "rules": {
    // default rules are false if not specified
    // setting these to true would make ALL CHILD PATHS readable/writable
    // ".read": false,
    // ".write": false,

    "room_names": {
      // the room names can be enumerated and read
      // they cannot be modified since no write rule
      // explicitly allows this
      ".read": true,

      "$room_id": {
        // this is just for documenting the structure of rooms, since
        // they are read-only and no write rule allows this to be set
        ".validate": "newData.isString()"
      }
    },

    "messages": {
      "$room_id": {
        // the list of messages in a room can be enumerated and each
        // message could also be read individually, the list of messages
        // for a room cannot be written to in bulk
        ".read": true,

        // room we want to write a message to must be valid
        ".validate": "root.child('room_names/'+$room_id).exists()",

        "$message_id": {
          // a new message can be created if it does not exist, but it
          // cannot be modified or deleted
          ".write": "!data.exists() && newData.exists()",
          // the room attribute must be a valid key in room_names/ (the room must exist)
          // the object to write must have a name, message, and timestamp
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",

          // the name must be a string, longer than 0 chars, and less than 20 and cannot contain "admin"
          "name": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')" },

          // the message must be longer than 0 chars and less than 50
          "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" },

          // messages cannot be added in the past or the future
          // clients should use firebase.database.ServerValue.TIMESTAMP
          // to ensure accurate timestamps
          "timestamp": { ".validate": "newData.val() <= now" },

          // no other fields can be included in a message
          "$other": { ".validate": false }
        }
      }
    }
  }
}

Langkah Berikutnya