Найкращий спосіб створити унікальний маркер в 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модель, але оскільки вони створюються партіями автоматично на основі дій користувача в додатку (вони замовляють замовлення та купують жетони, по суті), це не можливо, щоб програма додала помилку.

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

Відповіді:


333

- Оновлення -

З 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

не використовуйте початок / час, використовуйте 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]
Bijan

Я використовую це для придушення 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"

Схоже, перший метод схожий на те, що я роблю, але я подумав, що rand не є агностичним?
Slick23

І я не впевнений, що слідую за цим: if self.new_record? and self.access_token.nil?... це те, що перевіряється, щоб маркер не був збережений?
Slick23

4
Вам завжди знадобляться додаткові перевірки існуючих маркерів. Я не усвідомлював, що це не очевидно. Просто додайте validates_uniqueness_of :tokenта додайте унікальний індекс до таблиці з міграцією.
coreyward

6
автор публікації щоденника тут! Так: я завжди додаю db обмеження або подібне, щоб стверджувати єдиність у цьому випадку.
Thibaut Barrère

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

наприклад для id типу 10000, згенерований маркер виглядає як "MTAwMDA =" (і ви можете легко розшифрувати його для id, просто зробіть

Base64::decode64(string)

Мені більше цікаво гарантувати, що створене значення не зіткнеться з уже створеними та збереженими значеннями, а не методами створення унікальних рядків.
Slick23

згенероване значення не зіткнеться з уже створеними значеннями - base64 є детермінованим, тому якщо у вас є унікальні ідентифікатори, ви матимете унікальні лексеми.
Ессе

Я пішов з 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")

Сподіваюсь, це допоможе комусь.


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"

гарно загорнутий! Спасибі: 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.
Sree Raj

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

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.