Der beste Weg, um ein einzigartiges Token in Rails zu erstellen?

156

Hier ist was ich benutze. Das Token muss nicht unbedingt gehört werden, um es zu erraten. Es ist eher eine kurze URL-Kennung als alles andere, und ich möchte es kurz halten. Ich habe einige Beispiele befolgt, die ich online gefunden habe, und im Falle einer Kollision denke ich, dass der folgende Code das Token neu erstellt, bin mir aber nicht sicher. Ich bin jedoch gespannt auf bessere Vorschläge, da sich dies an den Rändern etwas rau anfühlt.

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

Meine Datenbankspalte für das Token ist ein eindeutiger Index, den ich auch validates_uniqueness_of :tokenfür das Modell verwende. Da diese jedoch in Stapeln automatisch basierend auf den Aktionen eines Benutzers in der App erstellt werden (sie geben eine Bestellung auf und kaufen die Token im Wesentlichen), ist dies der Fall Es ist nicht möglich, dass die App einen Fehler auslöst.

Ich könnte auch die Wahrscheinlichkeit von Kollisionen verringern, am Ende eine weitere Zeichenfolge anhängen, etwas, das basierend auf der Zeit oder ähnlichem generiert wird, aber ich möchte nicht, dass das Token zu lang wird.

Slick23
quelle

Antworten:

333

- Update -

Ab dem 9. Januar 2015. Die Lösung ist jetzt in der sicheren Token-Implementierung von Rails 5 ActiveRecord implementiert .

- Schienen 4 & 3 -

Nur zum späteren Nachschlagen, Erstellen eines sicheren zufälligen Tokens und Sicherstellen, dass es für das Modell eindeutig ist (bei Verwendung von Ruby 1.9 und 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

Bearbeiten:

@kain vorgeschlagen, und ich stimmte zu ersetzen , begin...end..whilemit loop do...break unless...endin dieser Antwort , weil frühere Umsetzung könnte in der Zukunft entfernt bekommen.

Bearbeiten 2:

Bei Rails 4 und Bedenken würde ich empfehlen, dies auf Bedenken zu verschieben.

# 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
Krule
quelle
benutze nicht begin / while, benutze loop / do
kain
@kain loop doIn diesem Fall sollte anstelle von begin...while(Schleifentyp "do ... while") ein Grund (Schleifentyp "while ... do") verwendet werden (wobei die Schleife mindestens einmal ausgeführt werden muss ).
Krule
7
Dieser genaue Code funktioniert nicht, da random_token innerhalb der Schleife liegt.
Jonathan Mui
1
@Krule Nun, da Sie dies zu einem Problem gemacht haben, sollten Sie das nicht auch ModelNamein der Methode loswerden ? Vielleicht durch self.classstattdessen ersetzen ? Ansonsten ist es nicht sehr wiederverwendbar, oder?
Paracycle
1
Die Lösung ist nicht veraltet, Secure Token ist einfach in Rails 5 implementiert, kann aber nicht in Rails 4 oder Rails 3 (auf die sich diese Frage bezieht) verwendet werden
Aleks
52

Ryan Bates verwendet in seinem Railscast auf Beta-Einladungen ein schönes Stück Code . Dies erzeugt eine alphanumerische Zeichenfolge mit 40 Zeichen.

Digest::SHA1.hexdigest([Time.now, rand].join)
Nate Bird
quelle
3
Ja, das ist nicht schlecht. Normalerweise suche ich nach viel kürzeren Zeichenfolgen, die ich als Teil einer URL verwenden kann.
Slick23
Ja, das ist zumindest leicht zu lesen und zu verstehen. 40 Zeichen sind in einigen Situationen gut (wie Beta-Einladungen) und das funktioniert bisher gut für mich.
Nate Bird
12
@ Slick23 Sie können immer einen Teil der Zeichenfolge auch greifen:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan
Ich verwende dies, um IP-Adressen zu verschleiern, wenn ich die "Client-ID" an das Messprotokoll von Google Analytics sende. Es soll eine UUID sein, aber ich nehme nur die ersten 32 Zeichen der hexdigestfür eine bestimmte IP.
thekingoftruth
1
Für eine 32-Bit-IP-Adresse wäre es ziemlich einfach, eine Nachschlagetabelle aller möglichen Hexdigest-Werte zu erstellen, die von @thekingoftruth generiert werden. Denken Sie also nicht, dass selbst eine Teilzeichenfolge des Hash irreversibel ist.
mwfearnley
32

Dies kann eine späte Antwort sein, aber um die Verwendung einer Schleife zu vermeiden, können Sie die Methode auch rekursiv aufrufen. Es sieht und fühlt sich für mich etwas sauberer an.

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
Marius Pop
quelle
30

In diesem Artikel werden einige ziemlich raffinierte Methoden gezeigt:

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

Mein Favorit ist folgender:

rand(36**8).to_s(36)
=> "uur0cj2h"
Coreyward
quelle
Es sieht so aus, als ob die erste Methode meiner Arbeit ähnelt, aber ich dachte, Rand wäre nicht datenbankunabhängig?
Slick23
Und ich bin mir nicht sicher, ob ich das befolge: if self.new_record? and self.access_token.nil?... prüft das, ob das Token noch nicht gespeichert ist?
Slick23
4
Sie benötigen immer zusätzliche Überprüfungen für vorhandene Token. Mir war nicht klar, dass dies nicht offensichtlich war. Fügen Sie validates_uniqueness_of :tokender Tabelle einfach einen eindeutigen Index hinzu und fügen Sie ihn mit einer Migration hinzu.
Coreyward
6
Autor des Blogposts hier! Ja: Ich füge immer eine DB-Einschränkung oder ähnliches hinzu, um die Einheitlichkeit in diesem Fall zu bestätigen.
Thibaut Barrère
1
Für diejenigen, die nach dem Beitrag suchen (der nicht mehr existiert) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/…
King'ori Maina
17

Wenn Sie etwas Einzigartiges wollen, können Sie Folgendes verwenden:

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

Dadurch wird jedoch eine Zeichenfolge mit 32 Zeichen generiert.

Es gibt jedoch einen anderen Weg:

require 'base64'

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

Beispiel: Für eine ID wie 10000 wäre das generierte Token "MTAwMDA =" (und Sie können es einfach für eine ID dekodieren, machen Sie es einfach

Base64::decode64(string)
Esse
quelle
Ich bin mehr daran interessiert sicherzustellen, dass der generierte Wert nicht mit den bereits generierten und gespeicherten Werten kollidiert, als mit Methoden zum Erstellen eindeutiger Zeichenfolgen.
Slick23
Der generierte Wert kollidiert nicht mit bereits generierten Werten. base64 ist deterministisch. Wenn Sie also eindeutige IDs haben, verfügen Sie über eindeutige Token.
Esse
Ich ging mit random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]wo ID die ID des Tokens ist.
Slick23
11
Es scheint mir, dass Base64::encode64(id.to_s)der Zweck der Verwendung eines Tokens zunichte gemacht wird. Höchstwahrscheinlich verwenden Sie ein Token, um die ID zu verschleiern und die Ressource für jeden unzugänglich zu machen, der nicht über das Token verfügt. In diesem Fall könnte jedoch nur jemand ausgeführt werden, Base64::encode64(<insert_id_here>)und er hätte sofort alle Token für jede Ressource auf Ihrer Site.
Jon Lemmon
string = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim
14

Dies kann hilfreich sein:

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

Wenn Sie ein Sonderzeichen entfernen möchten, als das erste Argument '+ / =' und jedes Zeichen, das in das zweite Argument '0aZ' und 15 eingegeben wurde, ist die Länge hier.

Und wenn Sie die zusätzlichen Leerzeichen und das neue Linienzeichen entfernen möchten, fügen Sie Folgendes hinzu:

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

Hoffe das wird jedem helfen.

Vik
quelle
3
Wenn Sie keine seltsamen Zeichen wie "+ / =" möchten, können Sie einfach SecureRandom.hex (10) anstelle von base64 verwenden.
Min Ming Lo
16
SecureRandom.urlsafe_base64erreicht das gleiche auch.
Iteration
7

Sie können has_secure_token https://github.com/robertomiranda/has_secure_token verwenden

ist wirklich einfach zu bedienen

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
user2627938
quelle
schön verpackt! Danke: D
mswiszcz
1
Ich erhalte die undefinierte lokale Variable 'has_secure_token'. Irgendwelche Ideen warum?
Adrian Matteo
3
@AdrianMatteo Ich hatte das gleiche Problem. Soweit ich verstanden habe, has_secure_tokenkommt das mit Rails 5, aber ich habe 4.x verwendet. Ich habe die Schritte in diesem Artikel befolgt und jetzt funktioniert es für mich.
Tamara Bernad
7

Versuchen Sie Folgendes:

Ab Ruby 1.9 ist die UUID-Generierung integriert. Verwenden Sie die SecureRandom.uuidFunktion.
Generieren von Guids in Ruby

Das war hilfreich für mich

Nickolay Kondratenko
quelle
5

Um eine richtige, mysql, varchar 32 GUID zu erstellen

SecureRandom.uuid.gsub('-','').upcase
Aaron Henderson
quelle
Da wir versuchen, ein einzelnes Zeichen '-' zu ersetzen, können Sie tr anstelle von gsub verwenden. SecureRandom.uuid.tr('-','').upcase. Überprüfen Sie diesen Link zum Vergleich zwischen tr und gsub.
Sree Raj
2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
miosser
quelle
0

Ich denke, Token sollte genauso behandelt werden wie ein Passwort. Als solche sollten sie in DB verschlüsselt werden.

Ich mache so etwas, um ein einzigartiges neues Token für ein Modell zu generieren:

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
cappie013
quelle