Limitación de la tasa de Firebase en las reglas de seguridad?

Lancé mi primer proyecto de repository abierto, EphChat , y la gente comenzó a inundarlo rápidamente con solicitudes.

¿Firebase tiene una forma de calificar las solicitudes de límite en las reglas de seguridad? Supongo que hay una forma de hacerlo utilizando el momento de la solicitud y el momento de los datos escritos previamente, pero no puedo encontrar nada en la documentación sobre cómo lo haría.

Las reglas de seguridad actuales son las siguientes.

{ "rules": { "rooms": { "$RoomId": { "connections": { ".read": true, ".write": "auth.username == newData.child('FBUserId').val()" }, "messages": { "$any": { ".write": "!newData.exists() || root.child('rooms').child(newData.child('RoomId').val()).child('connections').hasChild(newData.child('FBUserId').val())", ".validate": "newData.hasChildren(['RoomId','FBUserId','userName','userId','message']) && newData.child('message').val().length >= 1", ".read": "root.child('rooms').child(data.child('RoomId').val()).child('connections').hasChild(data.child('FBUserId').val())" } }, "poll": { ".write": "auth.username == newData.child('FBUserId').val()", ".read": true } } } } } 

Me gustaría calificar-limitar las escrituras (¿y las lecturas?) A la base de datos de todo el objeto Salas, por lo que solo se puede hacer una solicitud por segundo (por ejemplo).

¡Gracias!

El truco es mantener una auditoría de la última vez que un usuario publicó un mensaje. Luego puede aplicar el tiempo que cada mensaje se publica según el valor de auditoría:

 { "rules": { // this stores the last message I sent so I can throttle them by timestamp "last_message": { "$user": { // timestamp can't be deleted or I could just recreate it to bypass our throttle ".write": "newData.exists() && auth.uid === $user", // the new value must be at least 5000 milliseconds after the last (no more than one message every five seconds) // the new value must be before now (it will be since `now` is when it reaches the server unless I try to cheat) ".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+5000)" } }, "messages": { "$message_id": { // message must have a timestamp attribute and a sender attribute ".write": "newData.hasChildren(['timestamp', 'sender', 'message'])", "sender": { ".validate": "newData.val() === auth.uid" }, "timestamp": { // in order to write a message, I must first make an entry in timestamp_index // additionally, that message must be within 500ms of now, which means I can't // just re-use the same one over and over, thus, we've effectively required messages // to be 5 seconds apart ".validate": "newData.val() >= now - 500 && newData.val() === data.parent().parent().parent().child('last_message/'+auth.uid).val()" }, "message": { ".validate": "newData.isString() && newData.val().length < 500" }, "$other": { ".validate": false } } } } } 

Véalo en acción en este violín . Aquí está la esencia de lo que está en el violín:

 var fb = new Firebase(URL); var userId; // log in and store user.uid here // run our create routine createRecord(data, function (recordId, timestamp) { console.log('created record ' + recordId + ' at time ' + new Date(timestamp)); }); // updates the last_message/ path and returns the current timestamp function getTimestamp(next) { var ref = fb.child('last_message/' + userId); ref.set(Firebase.ServerValue.TIMESTAMP, function (err) { if (err) { console.error(err); } else { ref.once('value', function (snap) { next(snap.val()); }); } }); } function createRecord(data, next) { getTimestamp(function (timestamp) { // add the new timestamp to the record data var data = { sender: userId, timestamp: timestamp, message: 'hello world' }; var ref = fb.child('messages').push(data, function (err) { if (err) { console.error(err); } else { next(ref.name(), timestamp); } }); }) } 

No tengo suficiente reputación para escribir en el comentario, pero estoy de acuerdo con el comentario de Victor. Si inserta el fb.child('messages').push(...) en un bucle (es decir, for (let i = 0; i < 100; i++) {...} ) entonces presionaría con éxito 60- 80 meessages (en ese marco de ventana de 500ms.

Inspirado por la solución de Kato, propongo una modificación a las reglas de la siguiente manera:

 rules: { users: { "$uid": { "timestamp": { // similar to Kato's answer ".write": "auth.uid === $uid && newData.exists()" ,".read": "auth.uid === $uid" ,".validate": "newData.hasChildren(['time', 'key'])" ,"time": { ".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val() + 1000)" } ,"key": { } } ,"messages": { "$key": { /// this key has to be the same is the key in timestamp (checked by .validate) ".write": "auth.uid === $uid && !data.exists()" ///only 'create' allow ,".validate": "newData.hasChildren(['message']) && $key === root.child('/users/' + $uid + '/timestamp/key').val()" ,"message": { ".validate": "newData.isString()" } /// ...and any other datas such as 'time', 'to'.... } } } } } 

El código .js es bastante similar a la solución de Kato, excepto que getTimestamp devolvería {time: number, key: string} a la siguiente callback. Entonces solo tendríamos que ref.update({[key]: data})

Esta solución evita la ventana de tiempo de 500 ms, no tenemos que preocuparnos de que el cliente debe ser lo suficientemente rápido como para enviar el mensaje dentro de los 500 ms. Si se envían varias solicitudes de escritura (spam), solo pueden escribir en 1 sola clave en los messages . Opcionalmente, la regla de solo creación en messages impide que eso suceda.

Las respuestas existentes utilizan dos actualizaciones de base de datos: (1) marque una marca de tiempo y (2) adjunte la marca de tiempo marcada a la escritura real. La respuesta de Kato requiere una ventana de tiempo de 500 ms, mientras que la de ChiNhan requiere recordar la siguiente clave.

Hay una manera más simple de hacerlo en una única actualización de base de datos. La idea es escribir múltiples valores en la base de datos a la vez usando el método update () . Las reglas de seguridad validan los valores escritos para que la escritura no exceda la cuota. La cuota se define como un par de valores: quotaTimestamp y postCount . El postCount es la cantidad de publicaciones escritas dentro de 1 minuto de quotaTimestamp. Las reglas de seguridad simplemente rechazan la próxima escritura si el postCount excede un cierto valor. El postCount se restablece cuando quotaTimestamp es más de 1 minuto.

Aquí se muestra cómo publicar un nuevo mensaje:

 function postMessage(user, message) { const now = Date.now() + serverTimeOffset; if (!user.quotaTimestamp || user.quotaTimestamp + 60 * 1000 < now) { // Resets the quota when 1 minute has elapsed since the quotaTimestamp. user.quotaTimestamp = database.ServerValue.TIMESTAMP; user.postCount = 0; } user.postCount++; const values = {}; const messageId = // generate unique id values[`users/${user.uid}/quotaTimestamp`] = user.quotaTimestamp; values[`users/${user.uid}/postCount`] = user.postCount; values[`messages/${messageId}`] = { sender: ..., message: ..., ... }; return this.db.database.ref().update(values); } 

Las reglas de seguridad limitan el límite a un máximo de 5 publicaciones por minuto:

 { "rules": { "users": { "$uid": { ".read": "$uid === auth.uid", ".write": "$uid === auth.uid && newData.child('postCount').val() < = 5", "quotaTimestamp": { // Only allow updating quotaTimestamp if it's staler than 1 minute. ".validate": " newData.isNumber() && (newData.val() === now ? (data.val() + 60 * 1000 < now) : (data.val() == newData.val()))" }, "postCount": { // Only allow postCount to be incremented by 1 // or reset to 1 when the quotaTimestamp is being refreshed. ".validate": " newData.isNumber() && (data.exists() ? (data.val() + 1 === newData.val() || (newData.val() === 1 && newData.parent().child('quotaTimestamp').val() === now)) : (newData.val() === 1))" }, "$other": { ".validate": false } } }, "messages": { ... } } } 

Nota: serverTimeOffset se debe mantener para evitar el sesgo del reloj.