¿La mejor manera de crear tokens únicos en Rails?

156

Esto es lo que estoy usando. El token no necesariamente tiene que ser escuchado para adivinar, es más como un identificador de URL corto que cualquier otra cosa, y quiero que sea breve. He seguido algunos ejemplos que he encontrado en línea y, en caso de colisión, creo que el siguiente código recreará el token, pero no estoy muy seguro. Sin embargo, tengo curiosidad por ver mejores sugerencias, ya que esto se siente un poco áspero por los bordes.

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 columna de mi base de datos para el token es un índice único y también lo estoy usando validates_uniqueness_of :tokenen el modelo, pero debido a que estos se crean en lotes automáticamente en función de las acciones del usuario en la aplicación (hacen un pedido y compran los tokens, esencialmente), es no es factible que la aplicación arroje un error.

Supongo que también podría reducir la posibilidad de colisiones, agregar otra cadena al final, algo generado en función del tiempo o algo así, pero no quiero que el token se alargue demasiado.

Slick23
fuente

Respuestas:

334

- Actualización -

A partir del 9 de enero de 2015. la solución ahora está implementada en la implementación segura de tokens de ActiveRecord de Rails 5 .

- Rieles 4 y 3 -

Solo para referencia futura, creando un token aleatorio seguro y asegurando que sea único para el modelo (cuando se usa Ruby 1.9 y 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

Editar:

@kain sugirió, y yo estaba de acuerdo, para reemplazar begin...end..whilecon loop do...break unless...enden esta respuesta porque la aplicación anterior podría obtener eliminado en el futuro.

Edición 2:

Con Rails 4 y preocupaciones, recomendaría mover esto a preocupación.

# 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
fuente
no use begin / while, use loop / do
kain
@kain ¿Debe utilizarse alguna razón loop do(tipo de bucle "while ... do") en este caso (donde se requiere que el loop se ejecute al menos una vez) en lugar de begin...while(tipo de bucle "do ... while")?
Krule
77
este código exacto no funcionará ya que random_token tiene un alcance dentro del ciclo.
Jonathan Mui
1
@Krule Ahora que has convertido esto en una preocupación, ¿no deberías deshacerte también ModelNamedel método? Tal vez reemplazarlo con en su self.classlugar? De lo contrario, no es muy reutilizable, ¿verdad?
paracycle
1
La solución no está en desuso, Secure Token simplemente se implementa en Rails 5, pero no se puede usar en Rails 4 o Rails 3 (a lo que se refiere esta pregunta)
Aleks
52

Ryan Bates usa un poco de código en sus invitaciones de Railscast en beta . Esto produce una cadena alfanumérica de 40 caracteres.

Digest::SHA1.hexdigest([Time.now, rand].join)
Nate Bird
fuente
3
Sí, eso no está mal. Por lo general, estoy buscando cadenas mucho más cortas, para usar como parte de una URL.
Slick23
Sí, esto es al menos fácil de leer y entender. 40 caracteres es bueno en algunas situaciones (como invitaciones beta) y esto me está funcionando bien hasta ahora.
Nate Bird
12
@ Slick23 Siempre puedes tomar una porción de la cuerda también:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan
Lo uso para ofuscar las direcciones IP cuando envío la "identificación del cliente" al protocolo de medición de Google Analytics. Se supone que es un UUID, pero solo tomo los primeros 32 caracteres de hexdigestcualquier IP dada.
thekingoftruth
1
Para una dirección IP de 32 bits, sería bastante fácil tener una tabla de búsqueda de todos los posibles hexadecimales generados por @thekingoftruth, así que nadie piense que incluso una subcadena del hash será irreversible.
mwfearnley
32

Esto podría ser una respuesta tardía, pero para evitar el uso de un bucle, también puede llamar al método de forma recursiva. Se ve y se siente un poco más limpio para mí.

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
fuente
30

Hay algunas formas bastante ingeniosas de hacer esto demostradas en este artículo:

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

Mi lista favorita es esta:

rand(36**8).to_s(36)
=> "uur0cj2h"
coreyward
fuente
Parece que el primer método es similar a lo que estoy haciendo, pero ¿pensé que rand no era independiente de la base de datos?
Slick23
Y no estoy seguro de seguir esto: if self.new_record? and self.access_token.nil?... ¿es eso lo que está comprobando para asegurarse de que el token no esté almacenado?
Slick23
44
Siempre necesitará verificaciones adicionales contra los tokens existentes. No me di cuenta de que esto no era obvio. Simplemente agregue validates_uniqueness_of :tokeny agregue un índice único a la tabla con una migración.
coreyward
66
autor de la publicación del blog aquí! Sí: siempre agrego una restricción db o similar para afirmar la unicidad en este caso.
Thibaut Barrère
1
Para aquellos que buscan la publicación (que ya no existe) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/…
King'ori Maina
17

Si quieres algo que sea único, puedes usar algo como esto:

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

sin embargo, esto generará una cadena de 32 caracteres.

Sin embargo, hay otra manera:

require 'base64'

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

por ejemplo, para id como 10000, el token generado sería como "MTAwMDA =" (y puedes decodificarlo fácilmente para id, solo crea

Base64::decode64(string)
Esse
fuente
Estoy más interesado en asegurarme de que el valor generado no choque con los valores ya generados y almacenados, en lugar de los métodos para crear cadenas únicas.
Slick23
el valor generado no colisionará con los valores ya generados: base64 es determinista, por lo que si tiene identificadores únicos, tendrá tokens únicos.
Esse
Fui con random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]donde ID es la ID del token.
Slick23
11
Me parece que Base64::encode64(id.to_s)derrota el propósito de usar un token. Lo más probable es que esté usando un token para ocultar la identificación y hacer que el recurso sea inaccesible para cualquier persona que no tenga el token. Sin embargo, en este caso, alguien podría simplemente correr Base64::encode64(<insert_id_here>)e instantáneamente tendrían todos los tokens para cada recurso en su sitio.
Jon Lemmon
Necesita ser cambiado para que esto funcionestring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim
14

Esto puede ser útil:

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

Si desea eliminar cualquier carácter especial que se ponga en el primer argumento '+ / =' y cualquier carácter puesto en el segundo argumento '0aZ' y 15 es la longitud aquí.

Y si desea eliminar los espacios adicionales y el nuevo carácter de línea, agregue cosas como:

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

Espero que esto ayude a cualquiera.

Vik
fuente
3
Si no desea caracteres extraños como "+ / =", puede usar SecureRandom.hex (10) en lugar de base64.
Min Ming Lo
16
SecureRandom.urlsafe_base64logra lo mismo también.
Iterion
7

puedes usar has_secure_token https://github.com/robertomiranda/has_secure_token

es realmente simple de usar

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
usuario2627938
fuente
bien envuelto! Gracias: D
mswiszcz
1
Obtengo la variable local indefinida 'has_secure_token'. Alguna idea de por qué?
Adrian Matteo
3
@AdrianMatteo Tuve este mismo problema. Por lo que he entendido, has_secure_tokenviene con Rails 5, pero estaba usando 4.x. He seguido los pasos de este artículo y ahora me funciona.
Tamara Bernad
7

Intenta de esta manera:

A partir de Ruby 1.9, la generación de uuid está integrada. Usa la SecureRandom.uuidfunción.
Generando Guías en Ruby

Esto fue útil para mi

Nickolay Kondratenko
fuente
5

Para crear un GUID apropiado, mysql, varchar 32

SecureRandom.uuid.gsub('-','').upcase
Aaron Henderson
fuente
Como estamos tratando de reemplazar un solo carácter '-', puede usar tr en lugar de gsub. SecureRandom.uuid.tr('-','').upcase. Consulte este enlace para comparar tr y gsub.
Sree Raj
2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
miosser
fuente
0

Creo que el token debe manejarse como una contraseña. Como tal, deben estar encriptados en DB.

Estoy haciendo algo como esto para generar un nuevo token único para un modelo:

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
fuente