Найкращий спосіб створити унікальний маркер в 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.