Melhor maneira de criar token exclusivo no Rails?

156

Aqui está o que estou usando. O token não precisa necessariamente ser ouvido para adivinhar; é mais como um identificador de URL curto do que qualquer outra coisa, e eu quero mantê-lo curto. Segui alguns exemplos que encontrei on-line e, no caso de uma colisão, acho que o código abaixo recriará o token, mas não tenho certeza. Estou curioso para ver melhores sugestões, no entanto, pois isso parece um pouco áspero nas bordas.

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

Minha coluna do banco de dados para o token é um índice exclusivo e também estou usando validates_uniqueness_of :tokenno modelo, mas como eles são criados em lotes automaticamente com base nas ações de um usuário no aplicativo (eles fazem um pedido e compram os tokens, essencialmente), é não é possível que o aplicativo gere um erro.

Acho que eu também poderia reduzir a chance de colisões, acrescentar outra sequência no final, algo gerado com base no tempo ou algo assim, mas não quero que o token fique muito tempo.

Slick23
fonte

Respostas:

333

- Atualização -

A partir de 9 de janeiro de 2015, a solução agora foi implementada na implementação de token seguro do Rails 5 ActiveRecord .

- Trilhos 4 e 3 -

Apenas para referência futura, criando token aleatório seguro e garantindo sua exclusividade para o modelo (ao usar 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

Editar:

@kain sugeriu, e eu concordei, para substituir begin...end..whilecom loop do...break unless...endnesta resposta porque a implementação anterior pode ter removido no futuro.

Edição 2:

Com o Rails 4 e preocupações, eu recomendaria mover isso para preocupação.

# 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
fonte
Não use começar / tempo, o uso de loop / fazer
Kain
@kain Alguma razão loop do(tipo de loop "while ... do") deve ser usada neste caso (onde o loop é necessário para executar pelo menos uma vez) em vez de begin...while(tipo de loop "do ... while")?
Krule 10/03/2013
7
esse código exato não funcionará, pois o random_token está no escopo do loop.
Jonathan Mui
1
@ Krr Agora que você transformou isso em uma preocupação, você também não deveria se livrar do ModelNamemétodo? Talvez substitua por self.class? Caso contrário, não é muito reutilizável, é?
precisa saber é
1
A solução não é preterida, o Secure token é simplesmente aplicado em guias 5, mas não pode ser usada em trilhos 4 ou carris 3 (que esta questão diz respeito a)
Aleks
52

Ryan Bates usa um bom código em seu Railscast em convites beta . Isso produz uma cadeia alfanumérica de 40 caracteres.

Digest::SHA1.hexdigest([Time.now, rand].join)
Nate Bird
fonte
3
Sim, isso não é ruim. Normalmente, procuro cadeias muito mais curtas, para usar como parte de um URL.
precisa
Sim, isso é pelo menos fácil de ler e entender. 40 caracteres são bons em algumas situações (como convites beta) e isso está funcionando bem para mim até agora.
Nate Bird
12
@ Slick23 Você sempre pode pegar uma parte da corda também: #Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan
Eu uso isso para ofuscar endereços IP ao enviar o "ID do cliente" para o protocolo de medição do Google Analytics. Ele deveria ser um UUID, mas eu apenas pego os 32 primeiros caracteres hexdigestpara qualquer IP.
thekingoftruth
1
Para um endereço IP de 32 bits, seria bastante fácil ter uma tabela de pesquisa de todos os hexadigest possíveis gerados pelo @thekingoftruth, para que ninguém pense que mesmo uma subseqüência do hash seja irreversível.
mwfearnley
32

Pode ser uma resposta tardia, mas para evitar o uso de um loop, você também pode chamar o método recursivamente. Parece e parece um pouco mais limpo para mim.

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

Existem algumas maneiras bastante lisas de fazer isso demonstradas neste artigo:

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

O meu favorito listado é o seguinte:

rand(36**8).to_s(36)
=> "uur0cj2h"
coreyward
fonte
Parece que o primeiro método é semelhante ao que estou fazendo, mas achei que o rand não era independente de banco de dados?
Slick23
E não tenho certeza se sigo o seguinte: if self.new_record? and self.access_token.nil?... é isso que está verificando para garantir que o token ainda não esteja armazenado?
Slick23
4
Você sempre precisará de verificações adicionais em relação aos tokens existentes. Não percebi que isso não era óbvio. Basta adicionar validates_uniqueness_of :tokene adicionar um índice exclusivo à tabela com uma migração.
Coreyward #
6
autor do post aqui! Sim: eu sempre adiciono uma restrição de banco de dados ou similar para afirmar a unicidade neste caso.
Thibaut Barrère #
1
Para quem procura a publicação (que não existe mais) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/…
King'ori Maina
17

Se você deseja algo único, pode usar algo como isto:

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

no entanto, isso gerará uma sequência de 32 caracteres.

No entanto, existe outra maneira:

require 'base64'

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

por exemplo, para ID como 10000, o token gerado seria "MTAwMDA =" (e você pode decodificá-lo facilmente para ID, basta fazer

Base64::decode64(string)
Esse
fonte
Estou mais interessado em garantir que o valor gerado não colide com os valores já gerados e armazenados, em vez de métodos para criar strings exclusivas.
Slick23
o valor gerado não colidirá com os valores já gerados - a base64 é determinística; portanto, se você tiver IDs exclusivos, terá tokens exclusivos.
Esse
Fui com random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]onde ID é o ID do token.
Slick23
11
Parece-me que Base64::encode64(id.to_s)anula o propósito de usar um token. Provavelmente, você está usando um token para ocultar a identificação e tornar o recurso inacessível para quem não possui o token. No entanto, nesse caso, alguém poderia simplesmente executar Base64::encode64(<insert_id_here>)e teria instantaneamente todos os tokens para todos os recursos do seu site.
31412 Jon Lemmon
Precisa ser mudado para que isso funcionestring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim
14

Isso pode ser útil:

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

Se você deseja remover qualquer caractere especial que seja colocado no primeiro argumento '+ / =' e qualquer caractere colocado no segundo argumento '0aZ' e 15 é o comprimento aqui.

E se você deseja remover os espaços extras e o caractere de nova linha, adicione coisas como:

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

Espero que isso ajude alguém.

Vik
fonte
3
Se você não deseja caracteres estranhos como "+ / =", basta usar SecureRandom.hex (10) em vez de base64.
Min Ming Lo
16
SecureRandom.urlsafe_base64consegue a mesma coisa também.
iterion
7

você pode usuário has_secure_token https://github.com/robertomiranda/has_secure_token

é realmente simples de usar

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
user2627938
fonte
bem embrulhado! Obrigado, D
mswiszcz 5/15
1
Eu recebo a variável local indefinida 'has_secure_token'. Alguma idéia do porquê?
Adrian Matteo
3
@AdrianMatteo Eu tive esse mesmo problema. Pelo que entendi, has_secure_tokenvem com o Rails 5, mas eu estava usando o 4.x. Eu segui as etapas deste artigo e agora funciona para mim.
Tamara Bernad
7

Tente desta maneira:

A partir do Ruby 1.9, a geração de uuid é incorporada. Use a SecureRandom.uuidfunção
Gerando Guids em Ruby

Isso foi útil para mim

Nickolay Kondratenko
fonte
5

Para criar um GUID adequado, mysql, varchar 32

SecureRandom.uuid.gsub('-','').upcase
Aaron Henderson
fonte
Como estamos tentando substituir um único caractere '-', você pode usar tr em vez de gsub. SecureRandom.uuid.tr('-','').upcase. Verifique este link para comparar entre tr e gsub.
Sree Raj
2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
miosser
fonte
0

Eu acho que o token deve ser tratado como a senha. Como tal, eles devem ser criptografados no banco de dados.

Estou fazendo algo assim para gerar um novo token exclusivo para um 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
fonte