Cara terbaik untuk membuat token unik di Rails?


156

Inilah yang saya gunakan. Token tidak harus didengar untuk menebak, ini lebih seperti pengidentifikasi url pendek daripada yang lainnya, dan saya ingin tetap singkat. Saya telah mengikuti beberapa contoh yang saya temukan online dan jika terjadi tabrakan, saya pikir kode di bawah ini akan membuat ulang token, tapi saya tidak begitu yakin. Saya ingin tahu melihat saran yang lebih baik, karena ini terasa agak kasar di tepinya.

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

Kolom basis data saya untuk token adalah indeks unik dan saya juga menggunakan validates_uniqueness_of :tokenpada model, tetapi karena ini dibuat dalam batch secara otomatis berdasarkan tindakan pengguna di app (mereka melakukan pemesanan dan membeli token, pada dasarnya), itu tidak layak untuk membuat aplikasi melempar kesalahan.

Saya juga bisa, saya kira, untuk mengurangi kemungkinan tabrakan, menambahkan string lain pada akhirnya, sesuatu yang dihasilkan berdasarkan waktu atau sesuatu seperti itu, tetapi saya tidak ingin token terlalu lama.

Jawaban:


334

- Perbarui -

Pada 9 Januari 2015. solusinya sekarang diimplementasikan dalam implementasi token aman Rails 5 ActiveRecord .

- Rel 4 & 3 -

Hanya untuk referensi di masa mendatang, membuat token acak yang aman dan memastikan keunikannya untuk model (saat menggunakan Ruby 1.9 dan 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

Edit:

@ain menyarankan, dan saya setuju, untuk menggantikanbegin...end..while dengan loop do...break unless...enddalam jawaban ini karena implementasi sebelumnya mungkin dihapus di masa depan.

Edit 2:

Dengan Rails 4 dan masalah, saya akan merekomendasikan memindahkan ini ke masalah.

# 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

jangan gunakan begin / while, gunakan loop / do
kain

@ain Alasan apa pun loop do("while ... do" jenis loop) harus digunakan dalam kasus ini (di mana loop diperlukan untuk menjalankan setidaknya satu kali) alih-alih begin...while("do ... while" tipe loop)?
Krule

7
kode persis ini tidak akan berfungsi karena random_token dicakup dalam loop.
Jonathan Mui

1
@ Krule Sekarang setelah Anda mengubah ini menjadi Masalah, bukankah Anda juga harus menyingkirkan ModelNamemetode ini? Mungkin menggantinya dengan self.class? Kalau tidak, itu tidak bisa digunakan kembali, bukan?
paracycle

1
Solusinya tidak ditinggalkan, Secure Token itu hanya diimplementasikan dalam Rails 5, tetapi tidak dapat digunakan dalam Rails 4 atau Rails 3 (yang berhubungan dengan pertanyaan ini)
Aleks

52

Ryan Bates menggunakan sedikit kode yang bagus di Railscast- nya pada undangan beta . Ini menghasilkan string alfanumerik 40 karakter.

Digest::SHA1.hexdigest([Time.now, rand].join)

3
Ya, itu tidak buruk. Saya biasanya mencari string yang lebih pendek, untuk digunakan sebagai bagian dari URL.
Slick23

Ya, ini setidaknya mudah dibaca dan dimengerti. 40 karakter bagus dalam beberapa situasi (seperti undangan beta) dan ini bekerja dengan baik untuk saya sejauh ini.
Nate Bird

12
@ Slick23 Anda selalu dapat mengambil sebagian dari string juga:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
Bijan

Saya menggunakan ini untuk mengaburkan alamat IP saat mengirim "id klien" ke protokol pengukuran Google Analytics. Seharusnya UUID, tapi saya hanya mengambil 32 karakter pertama hexdigestuntuk setiap IP yang diberikan.
thekingoftruth

1
Untuk alamat IP 32-bit, akan cukup mudah untuk memiliki tabel pencarian dari semua hexdigest yang mungkin dihasilkan oleh @thekingoftruth, jadi jangan ada yang berpikir bahwa substring dari hash tidak akan dapat diubah.
mwfearnley

32

Ini mungkin respons yang terlambat tetapi untuk menghindari penggunaan loop Anda juga dapat memanggil metode secara rekursif. Terlihat dan terasa sedikit lebih bersih bagi saya.

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

Ada beberapa cara yang cukup apik untuk melakukan hal ini dalam artikel ini:

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

Favorit saya yang terdaftar adalah ini:

rand(36**8).to_s(36)
=> "uur0cj2h"

Sepertinya metode pertama mirip dengan apa yang saya lakukan, tapi saya pikir rand bukan database agnostik?
Slick23

Dan saya tidak yakin saya mengikuti ini: if self.new_record? and self.access_token.nil?... apakah itu yang memeriksa untuk memastikan token belum disimpan?
Slick23

4
Anda akan selalu memerlukan pemeriksaan tambahan terhadap token yang ada. Saya tidak menyadari bahwa ini tidak jelas. Cukup tambahkan validates_uniqueness_of :tokendan tambahkan indeks unik ke tabel dengan migrasi.
coreyward

6
penulis posting blog di sini! Ya: Saya selalu menambahkan batasan db atau serupa untuk menyatakan ketidakpaduan dalam kasus ini.
Thibaut Barrère

1
Bagi mereka yang mencari posting (yang tidak ada lagi) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/…
King'ori Maina

17

Jika Anda menginginkan sesuatu yang unik, Anda dapat menggunakan sesuatu seperti ini:

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

namun ini akan menghasilkan string 32 karakter.

Namun ada cara lain:

require 'base64'

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

misalnya untuk id seperti 10000, token yang dihasilkan akan seperti "MTAwMDA =" (dan Anda dapat dengan mudah mendekodekannya untuk id, cukup buat

Base64::decode64(string)

Saya lebih tertarik untuk memastikan bahwa nilai yang dihasilkan tidak akan bertabrakan dengan nilai-nilai yang sudah dihasilkan dan disimpan, daripada metode untuk membuat string unik.
Slick23

nilai yang dihasilkan tidak akan bertabrakan dengan nilai yang telah dihasilkan - base64 bersifat deterministik, jadi jika Anda memiliki id unik, Anda akan memiliki token unik.
Esse

Saya pergi dengan random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]ID tempat adalah ID token.
Slick23

11
Sepertinya saya yang Base64::encode64(id.to_s)mengalahkan tujuan menggunakan token. Kemungkinan besar Anda menggunakan token untuk mengaburkan id dan membuat sumber daya tidak dapat diakses oleh siapa pun yang tidak memiliki token. Namun, dalam hal ini, seseorang dapat menjalankannya Base64::encode64(<insert_id_here>)dan mereka akan langsung memiliki semua token untuk setiap sumber daya di situs Anda.
Jon Lemmon

Perlu diubah agar ini berfungsistring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim

14

Ini mungkin bermanfaat:

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

Jika Anda ingin menghapus karakter khusus daripada memasukkan argumen pertama '+ / =' dan karakter apa pun dimasukkan ke argumen kedua '0aZ' dan 15 adalah panjangnya di sini.

Dan jika Anda ingin menghapus spasi tambahan dan karakter baris baru daripada menambahkan hal-hal seperti:

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

Semoga ini bisa membantu siapa saja.


3
Jika Anda tidak ingin karakter aneh seperti "+ / =", Anda bisa menggunakan SecureRandom.hex (10) sebagai ganti base64.
Min Ming Lo

16
SecureRandom.urlsafe_base64mencapai hal yang sama juga.
iterion

7

Anda dapat pengguna has_secure_token https://github.com/robertomiranda/has_secure_token

sangat mudah digunakan

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"

dibungkus dengan baik! Terima kasih: D
mswiszcz

1
Saya mendapatkan variabel lokal yang tidak terdefinisi 'has_secure_token'. Ada ide mengapa?
Adrian Matteo

3
@AdrianMatteo Saya punya masalah yang sama. Dari apa yang saya pahami has_secure_tokendilengkapi dengan Rails 5, tapi saya menggunakan 4.x. Saya telah mengikuti langkah-langkah pada artikel ini dan sekarang berfungsi untuk saya.
Tamara Bernad


5

Untuk membuat yang benar, mysql, varchar 32 GUID

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

Karena kami mencoba mengganti satu karakter '-', Anda dapat menggunakan tr daripada gsub. SecureRandom.uuid.tr('-','').upcase. Periksa tautan ini untuk perbandingan antara tr dan gsub.
Sree Raj

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

0

Saya pikir token harus ditangani seperti kata sandi. Dengan demikian, mereka harus dienkripsi dalam DB.

Saya melakukan sesuatu seperti ini untuk menghasilkan token baru yang unik untuk model:

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
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.