Meilleur moyen de créer un jeton unique dans Rails?


156

Voici ce que j'utilise. Le jeton n'a pas nécessairement besoin d'être entendu pour deviner, il s'agit plus d'un identifiant d'url court qu'autre chose, et je veux être bref. J'ai suivi quelques exemples que j'ai trouvés en ligne et en cas de collision, je pense que le code ci-dessous recréera le jeton, mais je ne suis pas vraiment sûr. Je suis curieux de voir de meilleures suggestions, cependant, car cela semble un peu difficile sur les bords.

def self.create_token
    random_number = SecureRandom.hex(3)
    "1X#{random_number}"

    while Tracker.find_by_token("1X#{random_number}") != nil
      random_number = SecureRandom.hex(3)
      "1X#{random_number}"
    end
    "1X#{random_number}"
  end

Ma colonne de base de données pour le jeton est un index unique et j'utilise également validates_uniqueness_of :tokensur le modèle, mais comme ils sont créés automatiquement par lots en fonction des actions d'un utilisateur dans l'application (ils passent une commande et achètent les jetons, essentiellement), c'est impossible de faire en sorte que l'application génère une erreur.

Je pourrais aussi, je suppose, pour réduire les risques de collision, ajouter une autre chaîne à la fin, quelque chose de généré en fonction de l'heure ou quelque chose du genre, mais je ne veux pas que le jeton devienne trop long.

Réponses:


334

-- Mettre à jour --

Depuis le 9 janvier 2015. la solution est désormais implémentée dans l'implémentation du jeton sécurisé de Rails 5 ActiveRecord .

- Rails 4 & 3 -

Juste pour référence future, en créant un jeton aléatoire sécurisé et en garantissant son unicité pour le modèle (lors de l'utilisation de Ruby 1.9 et ActiveRecord):

class ModelName < ActiveRecord::Base

  before_create :generate_token

  protected

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless ModelName.exists?(token: random_token)
    end
  end

end

Éditer:

@kain a suggéré, et j'ai accepté, de remplacer begin...end..whilepar loop do...break unless...enddans cette réponse car l'implémentation précédente pourrait être supprimée à l'avenir.

Modifier 2:

Avec Rails 4 et les préoccupations, je recommanderais de déplacer cela en préoccupation.

# app/models/model_name.rb
class ModelName < ActiveRecord::Base
  include Tokenable
end

# app/models/concerns/tokenable.rb
module Tokenable
  extend ActiveSupport::Concern

  included do
    before_create :generate_token
  end

  protected

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless self.class.exists?(token: random_token)
    end
  end
end

ne pas utiliser begin / while, utiliser loop / do
kain

@kain N'importe quelle raison loop do(type de boucle "while ... do") doit être utilisée dans ce cas (où la boucle doit être exécutée au moins une fois) au lieu de begin...while(type de boucle "do ... while")?
Krule

7
ce code exact ne fonctionnera pas car random_token est compris dans la boucle.
Jonathan Mui

1
@Krule Maintenant que vous avez transformé cela en un problème, ne devriez-vous pas également vous débarrasser de ModelNamela méthode? Peut-être le remplacer à la self.classplace? Sinon, ce n'est pas très réutilisable, n'est-ce pas?
paracycle

1
La solution n'est pas obsolète, Secure Token est simplement implémentée dans Rails 5, mais elle ne peut pas être utilisée dans Rails 4 ou Rails 3 (auxquels cette question se rapporte)
Aleks

52

Ryan Bates utilise un petit peu de code dans son Railscast sur les invitations bêta . Cela produit une chaîne alphanumérique de 40 caractères.

Digest::SHA1.hexdigest([Time.now, rand].join)

3
Ouais, ce n'est pas mal. Je recherche généralement des chaînes beaucoup plus courtes, à utiliser dans le cadre d'une URL.
Slick23

Ouais, c'est au moins facile à lire et à comprendre. 40 caractères c'est bien dans certaines situations (comme les invitations bêta) et cela fonctionne bien pour moi jusqu'à présent.
Nate Bird

12
@ Slick23 Vous pouvez toujours saisir une partie de la chaîne aussi:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan

J'utilise ceci pour masquer les adresses IP lors de l'envoi de "l'identifiant client" au protocole de mesure de Google Analytics. C'est censé être un UUID, mais je prends juste les 32 premiers caractères du hexdigestpour une IP donnée.
thekingoftruth

1
Pour une adresse IP 32 bits, il serait assez facile d'avoir une table de recherche de tous les hexdigest possibles générés par @thekingoftruth, donc personne ne va penser que même une sous-chaîne du hachage sera irréversible.
mwfearnley

32

Cela peut être une réponse tardive, mais pour éviter d'utiliser une boucle, vous pouvez également appeler la méthode de manière récursive. Cela me paraît et me semble légèrement plus propre.

class ModelName < ActiveRecord::Base

  before_create :generate_token

  protected

  def generate_token
    self.token = SecureRandom.urlsafe_base64
    generate_token if ModelName.exists?(token: self.token)
  end

end

30

Il existe des moyens assez astucieux de le faire, illustrés dans cet article:

https://web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/2/creating-small-unique-tokens-in-ruby

Ma liste préférée est la suivante:

rand(36**8).to_s(36)
=> "uur0cj2h"

Il semble que la première méthode soit similaire à ce que je fais, mais je pensais que Rand n'était pas indépendant de la base de données?
Slick23

Et je ne suis pas sûr de suivre ceci: if self.new_record? and self.access_token.nil?... est-ce ce qui vérifie que le jeton n'est pas déjà stocké?
Slick23

4
Vous aurez toujours besoin de vérifications supplémentaires par rapport aux jetons existants. Je n'avais pas réalisé que ce n'était pas évident. Ajoutez validates_uniqueness_of :tokenet ajoutez simplement un index unique à la table avec une migration.
coreyward

6
auteur du billet de blog ici! Oui: j'ajoute toujours une contrainte de base de données ou similaire pour affirmer l'unicité dans ce cas.
Thibaut Barrère

1
Pour ceux qui recherchent le poste (qui n'existe plus) ... web.archive.org/web/20121026000606/http
//blog.logeek.fr/2009/7/

17

Si vous voulez quelque chose qui sera unique, vous pouvez utiliser quelque chose comme ceci:

string = (Digest::MD5.hexdigest "#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}")

cependant cela générera une chaîne de 32 caractères.

Il existe cependant un autre moyen:

require 'base64'

def after_create
update_attributes!(:token => Base64::encode64(id.to_s))
end

par exemple pour un identifiant comme 10000, le jeton généré serait comme "MTAwMDA =" (et vous pouvez facilement le décoder pour l'identifiant, faites simplement

Base64::decode64(string)

Je suis plus intéressé à faire en sorte que la valeur générée ne heurte pas les valeurs déjà générées et stockées, plutôt que des méthodes de création de chaînes uniques.
Slick23

La valeur générée n'entrera pas en conflit avec les valeurs déjà générées - base64 est déterministe, donc si vous avez des identifiants uniques, vous aurez des jetons uniques.
Esse

Je suis allé avec random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]où ID est l'ID du jeton.
Slick23

11
Il me semble que cela va à l' Base64::encode64(id.to_s)encontre de l'objectif d'utiliser un jeton. Vous utilisez très probablement un jeton pour masquer l'identifiant et rendre la ressource inaccessible à quiconque ne possède pas le jeton. Cependant, dans ce cas, quelqu'un pourrait simplement s'exécuter Base64::encode64(<insert_id_here>)et disposerait instantanément de tous les jetons pour chaque ressource de votre site.
Jon Lemmon du

Doit être changé pour cela pour fonctionnerstring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim

14

Cela peut être utile:

SecureRandom.base64(15).tr('+/=', '0aZ')

Si vous voulez supprimer un caractère spécial, mettez le premier argument '+ / =' et tout caractère mis dans le deuxième argument '0aZ' et 15 est la longueur ici.

Et si vous souhaitez supprimer les espaces supplémentaires et le caractère de nouvelle ligne, ajoutez des éléments tels que:

SecureRandom.base64(15).tr('+/=', '0aZ').strip.delete("\n")

J'espère que cela aidera n'importe qui.


3
Si vous ne voulez pas de caractères étranges comme "+ / =", vous pouvez simplement utiliser SecureRandom.hex (10) au lieu de base64.
Min Ming Lo

16
SecureRandom.urlsafe_base64réalise également la même chose.
iterion

7

vous pouvez utiliser has_secure_token https://github.com/robertomiranda/has_secure_token

est vraiment simple à utiliser

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"

joliment emballé! Merci: D
mswiszcz

1
J'obtiens la variable locale non définie 'has_secure_token'. Des idées pourquoi?
Adrian Matteo

3
@AdrianMatteo J'ai eu ce même problème. D'après ce que j'ai compris, il has_secure_tokenest livré avec Rails 5, mais j'utilisais 4.x. J'ai suivi les étapes de cet article et maintenant cela fonctionne pour moi.
Tamara Bernad


5

Pour créer un GUID mysql, varchar 32 approprié

SecureRandom.uuid.gsub('-','').upcase

Puisque nous essayons de remplacer un seul caractère «-», vous pouvez utiliser tr plutôt que gsub. SecureRandom.uuid.tr('-','').upcase. Vérifiez ce lien pour une comparaison entre tr et gsub.
Sree Raj

2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end

0

Je pense que le jeton doit être traité comme un mot de passe. En tant que tels, ils doivent être chiffrés dans DB.

Je ne fais pas quelque chose comme ça pour générer un nouveau jeton unique pour un modèle:

key = ActiveSupport::KeyGenerator
                .new(Devise.secret_key)
                .generate_key("put some random or the name of the key")

loop do
  raw = SecureRandom.urlsafe_base64(nil, false)
  enc = OpenSSL::HMAC.hexdigest('SHA256', key, raw)

  break [raw, enc] unless Model.exist?(token: enc)
end
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.