Najlepszy sposób na stworzenie unikalnego tokena w Railsach?

156

Oto, czego używam. Token niekoniecznie musi zostać usłyszany, aby odgadnąć, bardziej przypomina krótki identyfikator adresu URL niż cokolwiek innego i chcę, aby był krótki. I już po kilka przykładów znalazłem w Internecie oraz w razie kolizji, myślę, że kod poniżej będzie odtworzyć token, ale nie jestem pewien rzeczywistym. Jestem jednak ciekawy, aby zobaczyć lepsze sugestie, ponieważ wydaje się to trochę szorstkie na krawędziach.

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

Moja kolumna bazy danych dla tokena to unikalny indeks i używam go również validates_uniqueness_of :tokenna modelu, ale ponieważ są one tworzone automatycznie partiami na podstawie działań użytkownika w aplikacji (zasadniczo składają zamówienie i kupują tokeny), nie jest możliwe, aby aplikacja zgłosiła błąd.

Mógłbym też, jak sądzę, zmniejszyć ryzyko kolizji, dołączyć na końcu kolejny ciąg, coś wygenerowanego na podstawie czasu lub coś w tym rodzaju, ale nie chcę, aby token był zbyt długi.

Slick23
źródło

Odpowiedzi:

333

-- Aktualizacja --

Od 9 stycznia 2015r. Rozwiązanie jest teraz zaimplementowane w bezpiecznej implementacji tokena Rails 5 ActiveRecord .

- Szyny 4 i 3 -

Tylko dla przyszłego odniesienia, tworząc bezpieczny losowy token i zapewniając jego unikalność dla modelu (przy użyciu Ruby 1.9 i 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

Edytować:

@kain zasugerował, i zgodziłem się, aby wymienić begin...end..whilez loop do...break unless...endtą odpowiedzią, ponieważ poprzednia realizacja może zostaną usunięte w przyszłości.

Edycja 2:

W przypadku Rails 4 i obaw, polecałbym przenieść to na problem.

# 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
źródło
nie używaj początku / podczas, użyj pętli / do
kain
@kain Jakikolwiek powód loop do(pętla typu „while ... do”) powinna być używana w tym przypadku (gdzie pętla jest wymagana do uruchomienia przynajmniej raz) zamiast begin...while(pętla typu „do ... while”)?
Krule,
7
ten dokładny kod nie zadziała, ponieważ random_token jest objęty zakresem pętli.
Jonathan Mui
1
@Krule Teraz, gdy przekształciłeś to w problem, czy nie powinieneś również pozbyć się ModelNametej metody? Może zamiast tego zamienić na self.class? W przeciwnym razie nie nadaje się do wielokrotnego użytku, prawda?
paracycle
1
Rozwiązanie nie jest przestarzałe, Secure Token jest po prostu zaimplementowane w Rails 5, ale nie może być używane w Rails 4 lub Rails 3 (do których odnosi się to pytanie)
Aleks
52

Ryan Bates używa niezłego fragmentu kodu w swoim Railscast na zaproszeniach do bety . Daje to 40-znakowy ciąg alfanumeryczny.

Digest::SHA1.hexdigest([Time.now, rand].join)
Nate Bird
źródło
3
Tak, to nie jest złe. Zwykle szukam znacznie krótszych ciągów do wykorzystania jako część adresu URL.
Slick 23
Tak, jest to przynajmniej łatwe do odczytania i zrozumienia. 40 znaków jest dobre w niektórych sytuacjach (np. Zaproszenia do bety) i jak na razie działa to dobrze.
Nate Bird
12
@ Slick23 Zawsze możesz też pobrać część ciągu:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan
Używam tego do zaciemniania adresów IP podczas wysyłania „identyfikatora klienta” do protokołu pomiarowego Google Analytics. To powinien być UUID, ale biorę po prostu pierwsze 32 znaki z hexdigestdanego adresu IP.
thekingoftruth
1
W przypadku 32-bitowego adresu IP dość łatwo byłoby mieć tablicę wyszukiwania wszystkich możliwych hexdigest wygenerowanych przez @thekingoftruth, więc nikt nie myśli, że nawet podciąg hasha będzie nieodwracalny.
mwfearnley
32

Może to być późna odpowiedź, ale aby uniknąć używania pętli, możesz również wywołać metodę rekurencyjnie. Wygląda i wydaje mi się nieco czystszy.

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
źródło
30

W tym artykule przedstawiono kilka całkiem sprytnych sposobów:

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

Moja ulubiona lista to:

rand(36**8).to_s(36)
=> "uur0cj2h"
coreyward
źródło
Wygląda na to, że pierwsza metoda jest podobna do tego, co robię, ale myślałem, że rand nie jest agnostykiem bazy danych?
Slick23
I nie jestem pewien, czy rozumiem to: if self.new_record? and self.access_token.nil?... czy to właśnie sprawdza, czy token nie jest już przechowywany?
Slick23
4
Zawsze będziesz potrzebować dodatkowych kontroli istniejących tokenów. Nie zdawałem sobie sprawy, że to nie jest oczywiste. Po prostu dodaj validates_uniqueness_of :tokeni dodaj unikalny indeks do tabeli z migracją.
coreyward
6
autor wpisu na blogu tutaj! Tak: zawsze dodaję ograniczenie db lub coś podobnego, aby potwierdzić niepowtarzalność w tym przypadku.
Thibaut Barrère
1
Dla szukających postu (który już nie istnieje) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/…
King'ori Maina
17

Jeśli chcesz czegoś, co będzie wyjątkowe, możesz użyć czegoś takiego:

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

jednakże spowoduje to wygenerowanie ciągu 32 znaków.

Jest jednak inny sposób:

require 'base64'

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

na przykład dla id, takiego jak 10000, wygenerowany token miałby postać „MTAwMDA =” (i można go łatwo zdekodować dla identyfikatora, wystarczy

Base64::decode64(string)
Esse
źródło
Bardziej interesuje mnie zapewnienie, że wygenerowana wartość nie koliduje z wartościami już wygenerowanymi i przechowywanymi, a nie metodami tworzenia unikatowych ciągów.
Slick23
wygenerowana wartość nie będzie kolidować z wartościami już wygenerowanymi - base64 jest deterministyczny, więc jeśli masz unikalne identyfikatory, będziesz mieć unikalne tokeny.
Esse
Poszedłem z, random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]gdzie ID to identyfikator tokena.
Slick23
11
Wydaje mi się, że jest to Base64::encode64(id.to_s)sprzeczne z celem używania tokena. Najprawdopodobniej używasz tokena, aby ukryć identyfikator i uniemożliwić dostęp do zasobu każdemu, kto nie ma tego tokena. Jednak w tym przypadku ktoś mógłby po prostu uruchomić Base64::encode64(<insert_id_here>)i natychmiast miałby wszystkie tokeny dla każdego zasobu w Twojej witrynie.
Jon Lemmon,
Musi zostać zmieniony na to, aby działałstring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim
14

Może to być pomocne:

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

Jeśli chcesz usunąć dowolny znak specjalny niż umieszczony w pierwszym argumencie „+ / =”, a dowolny znak umieszczony w drugim argumencie „0aZ” i 15 to długość tutaj.

A jeśli chcesz usunąć dodatkowe spacje i znak nowego wiersza, dodaj następujące rzeczy:

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

Mam nadzieję, że to pomoże każdemu.

Vik
źródło
3
Jeśli nie chcesz dziwnych znaków, takich jak „+ / =”, możesz po prostu użyć SecureRandom.hex (10) zamiast base64.
Min Ming Lo
16
SecureRandom.urlsafe_base64osiąga to samo.
iterion
7

możesz user has_secure_token https://github.com/robertomiranda/has_secure_token

jest naprawdę prosty w użyciu

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
user2627938
źródło
ładnie zapakowane! Dzięki: D
mswiszcz
1
Otrzymuję niezdefiniowaną zmienną lokalną „has_secure_token”. Jakieś pomysły, dlaczego?
Adrian Matteo
3
@AdrianMatteo Miałem ten sam problem. Z tego, co zrozumiałem, has_secure_tokenpochodzi z Rails 5, ale używałem 4.x. Postępowałem zgodnie z instrukcjami w tym artykule i teraz działa dla mnie.
Tamara Bernad
7

Spróbuj w ten sposób:

Od Ruby 1.9 generowanie uuid jest wbudowane. Użyj SecureRandom.uuidfunkcji.
Generowanie przewodników w Rubim

To było dla mnie pomocne

Nickolay Kondratenko
źródło
5

Aby utworzyć poprawny identyfikator GUID mysql, varchar 32

SecureRandom.uuid.gsub('-','').upcase
Aaron Henderson
źródło
Ponieważ próbujemy zastąpić pojedynczy znak „-”, możesz użyć tr zamiast gsub. SecureRandom.uuid.tr('-','').upcase. Sprawdź ten link, aby porównać tr i gsub.
Sree Raj
2
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
miosser
źródło
0

Myślę, że token powinien być traktowany tak jak hasło. Jako takie powinny być zaszyfrowane w DB.

Robię coś takiego, aby wygenerować unikalny nowy token dla modelu:

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
źródło