Il modo migliore per creare token unici in Rails?


156

Ecco cosa sto usando. Il token non deve necessariamente essere sentito per indovinare, è più simile a un identificatore di URL breve di qualsiasi altra cosa, e voglio mantenerlo breve. Ho seguito alcuni esempi che ho trovato online e in caso di collisione, penso che il codice seguente ricrea il token, ma non ne sono davvero sicuro. Sono curioso di vedere suggerimenti migliori, tuttavia, poiché sembra un po 'agitato attorno ai bordi.

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

La colonna del mio database per il token è un indice univoco e sto anche utilizzando validates_uniqueness_of :tokenil modello, ma poiché questi vengono creati in batch automaticamente in base alle azioni di un utente nell'app (in pratica effettuano un ordine e acquistano i token), è non è possibile che l'app generi un errore.

Potrei anche, credo, ridurre la possibilità di collisioni, aggiungere un'altra stringa alla fine, qualcosa generato in base al tempo o qualcosa del genere, ma non voglio che il token duri troppo a lungo.

Risposte:


334

-- Aggiornare --

A partire dal 9 gennaio 2015. la soluzione è ora implementata nell'implementazione del token sicuro di Rails 5 ActiveRecord .

- Rotaie 4 e 3 -

Solo per riferimento futuro, creando token casuali sicuri e assicurandone l'unicità per il modello (quando si utilizzano Ruby 1.9 e 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

Modificare:

@kain ha suggerito, e ho accettato, di sostituirlo begin...end..whileconloop do...break unless...end in questa risposta perché l'implementazione precedente potrebbe essere rimossa in futuro.

Modifica 2:

Con Rails 4 e preoccupazioni, consiglierei di spostare questo problema.

# 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

non usare start / while, usa loop / do
kain il

@kain Qualunque motivo loop do(in questo caso dovrebbe essere usato il tipo di ciclo "while ... do") (dove è necessario eseguire il loop almeno una volta) anziché begin...while(tipo di loop "do ... while")?
Krule

7
questo codice esatto non funzionerà poiché random_token è incluso nel ciclo.
Jonathan Mui,

1
@Krule Ora che hai trasformato questo in una preoccupazione, non dovresti anche sbarazzarti del ModelNamemetodo? Forse sostituirlo con self.classinvece? Altrimenti, non è molto riutilizzabile, vero?
paraciclo

1
La soluzione non è obsoleta, Secure Token è semplicemente implementata in Rails 5, ma non può essere utilizzata in Rails 4 o Rails 3 (a cui si riferisce questa domanda)
Aleks

52

Ryan Bates usa un bel po 'di codice nel suo Railscast su inviti beta . Questo produce una stringa alfanumerica di 40 caratteri.

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

3
Sì, non è male. Di solito cerco stringhe molto più brevi da utilizzare come parte di un URL.
Slick23

Sì, questo è almeno facile da leggere e capire. 40 personaggi sono buoni in alcune situazioni (come gli inviti beta) e questo funziona bene per me finora.
Nate Bird,

12
@ Slick23 Puoi sempre prendere anche una parte della stringa:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan il

Lo uso per offuscare gli indirizzi IP quando invio l '"ID client" al protocollo di misurazione di Google Analytics. Dovrebbe essere un UUID, ma prendo solo i primi 32 caratteri del hexdigestper ogni dato IP.
thekingoftruth,

1
Per un indirizzo IP a 32 bit, sarebbe abbastanza facile avere una tabella di ricerca di tutti i possibili hexdigest generati da @thekingoftruth, quindi nessuno può pensare che anche una sottostringa dell'hash sia irreversibile.
mwfearnley,

32

Questa potrebbe essere una risposta tardiva, ma per evitare di usare un ciclo puoi anche chiamare il metodo in modo ricorsivo. Mi sembra leggermente più pulito.

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

Ci sono alcuni modi piuttosto slick di farlo dimostrato in questo articolo:

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

Il mio preferito è questo:

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

Sembra che il primo metodo sia simile a quello che sto facendo, ma pensavo che rand non fosse agnostico nel database?
Slick23,

E non sono sicuro di seguire questo: if self.new_record? and self.access_token.nil?... è quello che sta controllando per assicurarsi che il token non sia già memorizzato?
Slick23,

4
Avrai sempre bisogno di ulteriori controlli sui token esistenti. Non avevo capito che questo non era ovvio. Basta aggiungere validates_uniqueness_of :tokene aggiungere un indice univoco alla tabella con una migrazione.
coreyward

6
autore del post sul blog qui! Sì: aggiungo sempre un vincolo db o simile per affermare l'unicità in questo caso.
Thibaut Barrère


17

Se vuoi qualcosa che sarà unico puoi usare qualcosa del genere:

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

tuttavia questo genererà una stringa di 32 caratteri.

C'è comunque un altro modo:

require 'base64'

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

ad esempio per id come 10000, il token generato sarebbe come "MTAwMDA =" (e puoi facilmente decodificarlo per id, basta fare

Base64::decode64(string)

Sono più interessato a garantire che il valore generato non si scontrerà con i valori già generati e memorizzati, piuttosto che con i metodi per creare stringhe univoche.
Slick23,

il valore generato non si scontrerà con i valori già generati: base64 è deterministico, quindi se si hanno ID univoci, si avranno token univoci.
Esse,

Sono andato con random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]dove ID è l'ID del token.
Slick23,

11
Mi sembra che Base64::encode64(id.to_s)sconfigge lo scopo di usare un token. Molto probabilmente stai usando un token per oscurare l'id e rendere inaccessibile la risorsa a chiunque non abbia il token. Tuttavia, in questo caso, qualcuno potrebbe semplicemente eseguire Base64::encode64(<insert_id_here>)e avrebbero immediatamente tutti i token per ogni risorsa sul tuo sito.
Jon Lemmon,

Deve essere cambiato in questo per funzionarestring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim il

14

Questo può essere utile:

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

Se si desidera rimuovere qualsiasi carattere speciale oltre a inserire il primo argomento '+ / =' e qualsiasi carattere inserito nel secondo argomento '0aZ' e 15 è la lunghezza qui.

E se vuoi rimuovere gli spazi extra e il nuovo carattere di linea che aggiungi cose come:

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

Spero che questo possa aiutare a chiunque.


3
Se non vuoi caratteri strani come "+ / =", puoi semplicemente usare SecureRandom.hex (10) invece di base64.
Min Ming Lo

16
SecureRandom.urlsafe_base64raggiunge anche la stessa cosa.
iterione

7

puoi l'utente has_secure_token https://github.com/robertomiranda/has_secure_token

è davvero semplice da usare

class User
  has_secure_token :token1, :token2
end

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

ben avvolto! Grazie: D
mswiszcz,

1
Ottengo variabile locale non definita 'has_secure_token'. Qualche idea sul perché?
Adrian Matteo,

3
@AdrianMatteo Ho avuto lo stesso problema. Da quello che ho capito has_secure_tokenarriva con Rails 5, ma stavo usando 4.x. Ho seguito i passaggi di questo articolo e ora funziona per me.
Tamara Bernad,


5

Per creare un GUID varchar 32 appropriato, mysql

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

Dato che stiamo cercando di sostituire un singolo carattere "-", puoi usare tr anziché gsub. SecureRandom.uuid.tr('-','').upcase. Controlla questo link per il confronto tra tr e gsub.
Sree Raj,

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

0

Penso che il token dovrebbe essere gestito proprio come la password. Pertanto, dovrebbero essere crittografati nel DB.

Sto facendo qualcosa del genere per generare un nuovo token unico per un modello:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.