web-dev-qa-db-fra.com

Limite de débit Firebase dans les règles de sécurité?

J'ai lancé mon premier projet de référentiel ouvert, EphChat , et les gens ont rapidement commencé à l'inonder de demandes.

Firebase dispose-t-il d'un moyen d'évaluer les demandes de limitation dans les règles de sécurité? Je suppose qu'il existe un moyen de le faire en utilisant l'heure de la demande et l'heure des données précédemment écrites, mais je ne trouve rien dans la documentation qui explique comment procéder.

Les règles de sécurité actuelles sont les suivantes.

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

Je souhaiterais limiter les écritures (et les lectures?) À la base de données pour l'objet Chambres entier, de sorte qu'une seule demande peut être effectuée par seconde (par exemple).

Merci!

27
Brian Mayer

L'astuce consiste à garder un audit de la dernière fois qu'un utilisateur a posté un message. Ensuite, vous pouvez imposer l'heure à laquelle chaque message est publié en fonction de la valeur d'audit:

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

Voyez-le en action dans ce violon . Voici l'essentiel de ce qui est dans le violon:

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);
            }
        });
    })
}
39
Kato

Je n'ai pas assez de réputation pour écrire dans le commentaire, mais j'accepte le commentaire de Victor. Si vous insérez la fb.child('messages').Push(...) dans une boucle (c'est-à-dire for (let i = 0; i < 100; i++) {...}), vous obtiendrez alors avec succès 60 à 80 messages (dans ce cadre de fenêtre de 500 ms).

Inspiré par la solution de Kato, je propose une modification des règles comme suit: 

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'....
        }
      }
    }
  }
}

Le code .js est assez similaire à la solution de Kato, sauf que getTimestamp renverrait {time: number, key: string} au prochain rappel. Ensuite, nous aurions juste à ref.update({[key]: data})

Cette solution évite la fenêtre temporelle de 500 ms, nous n’avons pas à nous inquiéter du fait que le client doit être assez rapide pour envoyer le message dans un délai de 500 ms. Si plusieurs demandes d'écriture sont envoyées (spamming), elles ne peuvent écrire que dans une seule clé de la variable messages. Facultativement, la règle de création uniquement dans messages empêche que cela se produise.

2
ChiNhan

Les réponses existantes utilisent deux mises à jour de la base de données: (1) marquer un horodatage et (2) associer l'horodatage marqué à l'écriture réelle. La réponse de Kato nécessite une fenêtre temporelle de 500 ms, tandis que celle de ChiNhan nécessite de se souvenir de la clé suivante.

Il existe un moyen plus simple de le faire dans une seule mise à jour de base de données. L'idée est d'écrire simultanément plusieurs valeurs dans la base de données à l'aide de la méthode update () . Les règles de sécurité valident les valeurs écrites afin que l'écriture ne dépasse pas le quota. Le quota est défini comme une paire de valeurs: quotaTimestamp et postCount . PostCount est le nombre de messages écrits dans la minute qui suit le quotaTimestamp. Les règles de sécurité rejettent simplement la prochaine écriture si postCount dépasse une certaine valeur. Le postCount est réinitialisé lorsque le quotaTimestamp est inférieur à 1 minute.

Voici comment poster un nouveau message:

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

Les règles de sécurité pour limiter la limite à 5 messages par minute au maximum:

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

Remarque: le paramètre serverTimeOffset doit être conservé pour éviter le décalage d'horloge.

0
Felix Halim