Лучший способ создать уникальный токен в Rails?

156

Вот что я использую. Маркер не обязательно должен быть услышан, чтобы угадать, он больше похож на короткий URL-идентификатор, чем на что-либо еще, и я хочу, чтобы он был коротким. Я следовал за некоторыми примерами, которые я нашел в Интернете, и в случае столкновения, я думаю, код ниже воссоздает токен, но я не совсем уверен. Мне любопытно видеть лучшие предложения, хотя, поскольку это кажется немного грубым по краям.

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

Столбец моей базы данных для токена является уникальным индексом, и я также использую validates_uniqueness_of :tokenего для модели, но поскольку они создаются партиями автоматически на основе действий пользователя в приложении (они размещают заказ и покупают токены, по сути), это не возможно, чтобы приложение выдало ошибку.

Я мог бы также, я думаю, уменьшить вероятность коллизий, добавить еще одну строку в конце, что-то сгенерированное на основе времени или что-то в этом роде, но я не хочу, чтобы токен становился слишком длинным.

Slick23
источник

Ответы:

334

-- Обновить --

По состоянию на 9 января 2015 г. решение теперь реализовано в Rails 5 ActiveRecord для реализации безопасного токена .

- Рельсы 4 и 3 -

Просто для дальнейшего использования, создания безопасного случайного токена и обеспечения его уникальности для модели (при использовании Ruby 1.9 и 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

Редактировать:

@kain предложил, и я согласился, чтобы заменить begin...end..whileс loop do...break unless...endтаким ответом , потому что предыдущая реализация может получить удалена в будущем.

Изменить 2:

С Rails 4 и проблемами я бы порекомендовал перенести это в беспокойство.

# 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
Круле
источник
не используйте begin / while, используйте loop / do
kain
@kain В loop doэтом случае следует использовать какую-либо причину (тип цикла "while ... do") (где цикл требуется запустить хотя бы один раз) вместо begin...while(тип цикла "do ... while")?
Круле
7
этот точный код не будет работать, так как random_token находится в цикле.
Джонатан Муй
1
@Krule Теперь, когда вы превратили это в Концерн, не должны ли вы также избавиться от ModelNameметода? Может быть, заменить его self.classвместо этого? В противном случае, это не очень многоразово, не так ли?
парацикл
1
Решение не устарело, Secure Token просто реализовано в Rails 5, но его нельзя использовать в Rails 4 или Rails 3 (к чему относится этот вопрос)
Aleks
52

Райан Бейтс использует небольшой кусок кода в своем Railscast для бета-приглашений . Это создает 40-символьную буквенно-цифровую строку.

Digest::SHA1.hexdigest([Time.now, rand].join)
Нейт Берд
источник
3
Да, это не плохо. Я обычно ищу более короткие строки, чтобы использовать их как часть URL.
Slick23
Да, это по крайней мере легко читать и понимать. 40 символов хороши в некоторых ситуациях (например, бета-приглашения), и до сих пор это работает хорошо для меня.
Нейт Берд
12
@ Slick23 Вы всегда можете взять часть строки также:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Биджан
Я использую это для маскировки IP-адресов при отправке «идентификатора клиента» в протокол измерений Google Analytics. Это должен быть UUID, но я просто беру первые 32 знака hexdigestдля любого IP.
thekingoftruth
1
Для 32-битного IP-адреса было бы довольно легко иметь таблицу поиска всех возможных hexdigest, сгенерированных @thekingoftruth, поэтому никто не думает, что даже подстрока хеша будет необратимой.
mwfearnley
32

Это может быть поздний ответ, но во избежание использования цикла вы также можете вызвать метод рекурсивно. Это выглядит и чувствует себя немного чище для меня.

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

В этой статье продемонстрировано несколько приятных способов сделать это:

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

Мой любимый в списке это:

rand(36**8).to_s(36)
=> "uur0cj2h"
coreyward
источник
Похоже, что первый метод похож на то, что я делаю, но я подумал, что rand не зависит от базы данных?
Slick23
И я не уверен, что следую этому: if self.new_record? and self.access_token.nil?... это то, что проверяет, чтобы убедиться, что токен еще не сохранен?
Slick23
4
Вам всегда будут нужны дополнительные проверки против существующих токенов. Я не понимал, что это не было очевидно. Просто добавьте validates_uniqueness_of :tokenи добавьте уникальный индекс в таблицу с миграцией.
Coreyward
6
Автор поста в блоге здесь! Да: я всегда добавляю ограничение db или подобное, чтобы утверждать уникальность в этом случае.
Тибо Баррер
1
Для тех, кто ищет пост (которого больше нет) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/…
King'ori Maina
17

Если вы хотите что-то уникальное, вы можете использовать что-то вроде этого:

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

однако это сгенерирует строку из 32 символов.

Однако есть и другой способ:

require 'base64'

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

например, для идентификатора, подобного 10000, сгенерированный токен будет выглядеть как «MTAwMDA =» (и вы можете легко декодировать его для идентификатора, просто сделайте

Base64::decode64(string)
Esse
источник
Меня больше интересует, чтобы сгенерированное значение не конфликтовало с уже сгенерированными и сохраненными значениями, а не с методами создания уникальных строк.
Slick23
сгенерированное значение не будет конфликтовать с уже сгенерированными значениями - base64 является детерминированным, поэтому, если у вас есть уникальные идентификаторы, у вас будут уникальные токены.
Esse
Я пошел с, random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]где ID является идентификатором токена.
Slick23
11
Мне кажется, что Base64::encode64(id.to_s)побеждает цель использования токена. Скорее всего, вы используете токен, чтобы скрыть идентификатор и сделать ресурс недоступным для всех, у кого нет токена. Однако в этом случае кто-то может просто запустить, Base64::encode64(<insert_id_here>)и он сразу же получит все токены для каждого ресурса на вашем сайте.
Джон Леммон
Нужно изменить это на работуstring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Касим
14

Это может быть полезно:

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

Если вы хотите удалить какой-либо специальный символ, введите первый аргумент «+ / =» и любой символ, указанный во втором аргументе «0aZ» и 15, - это длина.

И если вы хотите удалить лишние пробелы и символ новой строки, то добавьте такие вещи, как:

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

Надеюсь, это кому-нибудь поможет.

Vik
источник
3
Если вам не нужны странные символы, такие как «+ / =», вы можете просто использовать SecureRandom.hex (10) вместо base64.
Мин Мин Ло
16
SecureRandom.urlsafe_base64достигает того же самого также.
Итерион
7

Вы можете использовать has_secure_token https://github.com/robertomiranda/has_secure_token

действительно прост в использовании

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
user2627938
источник
красиво завернутый! Спасибо: D
mswiszcz
1
Я получаю неопределенную локальную переменную has_secure_token. Есть идеи почему?
Адриан Маттео
3
@AdrianMatteo У меня была такая же проблема. Из того, что я понял, has_secure_tokenидет с Rails 5, но я использовал 4.x. Я следовал инструкциям этой статьи, и теперь она работает для меня.
Тамара Бернад
5

Чтобы создать правильный, mysql, varchar 32 GUID

SecureRandom.uuid.gsub('-','').upcase
Аарон Хендерсон
источник
Поскольку мы пытаемся заменить один символ '-', вы можете использовать tr вместо gsub. SecureRandom.uuid.tr('-','').upcase, Проверьте эту ссылку для сравнения между tr и gsub.
Шри Радж
2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
miosser
источник
0

Я думаю, что токен должен обрабатываться так же, как пароль. Как таковые, они должны быть зашифрованы в БД.

Я делаю что-то вроде этого, чтобы сгенерировать уникальный новый токен для модели:

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
источник