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.

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

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)

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

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"

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)

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.


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"

ł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


5

Aby utworzyć poprawny identyfikator GUID mysql, varchar 32

SecureRandom.uuid.gsub('-','').upcase

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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.