Cách tốt nhất để tạo mã thông báo duy nhất trong Rails?


156

Đây là những gì tôi đang sử dụng. Mã thông báo không nhất thiết phải được nghe để đoán, nó giống như một mã định danh url ngắn hơn bất kỳ thứ gì khác và tôi muốn giữ nó ngắn. Tôi đã theo dõi một số ví dụ tôi đã tìm thấy trực tuyến và trong trường hợp xảy ra va chạm, tôi nghĩ mã dưới đây sẽ tạo lại mã thông báo, nhưng tôi không chắc lắm. Tôi tò mò muốn xem các đề xuất tốt hơn, mặc dù, vì điều này cảm thấy một chút xung quanh các cạnh.

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

Cột cơ sở dữ liệu của tôi cho mã thông báo là một chỉ mục duy nhất và tôi cũng đang sử dụng validates_uniqueness_of :tokentrên mô hình, nhưng vì chúng được tạo theo lô tự động dựa trên hành động của người dùng trong ứng dụng (về cơ bản họ đặt hàng và mua mã thông báo) không khả thi để ứng dụng ném lỗi.

Tôi cũng có thể, tôi đoán, để giảm cơ hội va chạm, nối thêm một chuỗi khác vào cuối, thứ gì đó được tạo ra dựa trên thời gian hoặc thứ gì đó tương tự, nhưng tôi không muốn mã thông báo quá dài.

Câu trả lời:


333

- Cập nhật -

Kể từ ngày 9 tháng 1 năm 2015. giải pháp hiện được triển khai trong triển khai mã thông báo bảo mật của Rails 5 ActiveRecord .

- Đường ray 4 & 3 -

Chỉ để tham khảo trong tương lai, tạo mã thông báo ngẫu nhiên an toàn và đảm bảo tính duy nhất cho mô hình (khi sử dụng Ruby 1.9 và 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

Biên tập:

@kain đề xuất, và tôi đã đồng ý, thay thế begin...end..whilebằng loop do...break unless...endcâu trả lời này vì việc thực hiện trước đó có thể bị xóa trong tương lai.

Chỉnh sửa 2:

Với Rails 4 và mối quan tâm, tôi sẽ khuyên bạn nên chuyển vấn đề này sang mối quan tâm.

# 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

không sử dụng bắt đầu / trong khi sử dụng loop / do
kain

@kain Bất kỳ lý do nào loop do(loại vòng lặp "while ... do") nên được sử dụng trong trường hợp này (trong đó vòng lặp được yêu cầu chạy ít nhất một lần) thay vì begin...whileloại vòng lặp "do ... while")?
Krule

7
mã chính xác này sẽ không hoạt động vì Random_token nằm trong phạm vi vòng lặp.
Jonathan Mũi

1
@Krule Bây giờ bạn đã biến điều này thành Mối quan tâm, bạn cũng không nên loại bỏ ModelNamephương thức này chứ? Có thể thay thế nó bằng self.classthay thế? Nếu không, nó không phải là rất tái sử dụng, phải không?
paracycl

1
Giải pháp không bị phản đối, Mã thông báo an toàn được triển khai đơn giản trong Rails 5, nhưng nó không thể được sử dụng trong Rails 4 hoặc Rails 3 (câu hỏi này liên quan đến)
Aleks

52

Ryan Bates sử dụng một ít mã đẹp trong Railscast của mình trên các lời mời beta . Điều này tạo ra một chuỗi ký tự gồm 40 ký tự.

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

3
Vâng, điều đó không tệ. Tôi thường tìm kiếm các chuỗi ngắn hơn nhiều, để sử dụng như một phần của URL.
Slick23

Vâng, điều này ít nhất là dễ đọc và dễ hiểu. 40 ký tự là tốt trong một số tình huống (như lời mời beta) và điều này vẫn hoạt động tốt với tôi cho đến nay.
Chim Nate

12
@ Slick23 Bạn luôn có thể lấy một phần của chuỗi:Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]
bijan

Tôi sử dụng điều này để làm xáo trộn địa chỉ IP khi gửi "id khách hàng" đến giao thức đo lường của Google Analytics. Nó được coi là một UUID, nhưng tôi chỉ lấy 32 ký tự đầu tiên hexdigestcho bất kỳ IP nào.
thekingoftruth

1
Đối với địa chỉ IP 32 bit, sẽ khá dễ dàng để có một bảng tra cứu tất cả các hexdigest có thể được tạo bởi @thekingoftruth, vì vậy đừng ai nghĩ rằng ngay cả một chuỗi con của hàm băm sẽ không thể đảo ngược.
mwfearnley

32

Đây có thể là phản hồi muộn nhưng để tránh sử dụng vòng lặp, bạn cũng có thể gọi phương thức theo cách đệ quy. Nó trông và cảm thấy sạch sẽ hơn với tôi.

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

Có một số cách khá hay để làm điều này được thể hiện trong bài viết này:

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

Danh sách yêu thích của tôi là đây:

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

Có vẻ như phương pháp đầu tiên tương tự như những gì tôi đang làm, nhưng tôi nghĩ rand không phải là cơ sở dữ liệu?
Slick23

Và tôi không chắc chắn mình làm theo điều này: if self.new_record? and self.access_token.nil?... đó có phải là kiểm tra để đảm bảo mã thông báo chưa được lưu trữ không?
Slick23

4
Bạn sẽ luôn cần kiểm tra bổ sung đối với các mã thông báo hiện có. Tôi đã không nhận ra rằng điều này là không rõ ràng. Chỉ cần thêm validates_uniqueness_of :tokenvà thêm một chỉ mục duy nhất vào bảng với một di chuyển.
coreyward

6
tác giả của bài viết blog ở đây! Có: Tôi luôn thêm một ràng buộc db hoặc tương tự để khẳng định tính đơn nhất trong trường hợp này.
Thibaut Barrère

1
Đối với những người tìm kiếm các bài (mà không tồn tại nữa) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/...
King'ori Maina

17

Nếu bạn muốn thứ gì đó độc đáo, bạn có thể sử dụng thứ như thế này:

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

tuy nhiên điều này sẽ tạo ra chuỗi 32 ký tự.

Tuy nhiên có một cách khác:

require 'base64'

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

ví dụ: đối với id như 10000, mã thông báo được tạo sẽ giống như "MTAwMDA =" (và bạn có thể dễ dàng giải mã nó cho id, chỉ cần thực hiện

Base64::decode64(string)

Tôi quan tâm hơn đến việc đảm bảo rằng giá trị được tạo sẽ không xung đột với các giá trị đã được tạo và lưu trữ, thay vì các phương thức để tạo các chuỗi duy nhất.
Slick23

giá trị được tạo sẽ không xung đột với các giá trị đã được tạo - base64 mang tính xác định, vì vậy nếu bạn có id duy nhất, bạn sẽ có mã thông báo duy nhất.
Esse

Tôi đã đi với random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]nơi ID là ID của mã thông báo.
Slick23

11
Dường như với tôi rằng Base64::encode64(id.to_s)đánh bại mục đích sử dụng mã thông báo. Nhiều khả năng bạn đang sử dụng mã thông báo để che khuất id và làm cho tài nguyên không thể truy cập được đối với bất kỳ ai không có mã thông báo. Tuy nhiên, trong trường hợp này, ai đó chỉ có thể chạy Base64::encode64(<insert_id_here>)và họ sẽ ngay lập tức có tất cả các mã thông báo cho mọi tài nguyên trên trang web của bạn.
Jon Lemmon

Cần phải thay đổi để làm việc nàystring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim

14

Điều này có thể hữu ích:

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

Nếu bạn muốn xóa bất kỳ ký tự đặc biệt nào hơn là đặt trong đối số thứ nhất '+ / =' và bất kỳ ký tự nào đặt trong đối số thứ hai '0aZ' và 15 là độ dài ở đây.

Và nếu bạn muốn xóa các khoảng trắng thừa và ký tự dòng mới hơn hãy thêm những thứ như:

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

Hy vọng điều này sẽ giúp cho bất cứ ai.


3
Nếu bạn không muốn các ký tự lạ như "+ / =", bạn chỉ có thể sử dụng SecureRandom.hex (10) thay vì base64.
Min Ming Lo

16
SecureRandom.urlsafe_base64đạt được điều tương tự là tốt.
lặp lại

7

bạn có thể sử dụng has_secure_token https://github.com/robertomiranda/has_secure_token

thực sự đơn giản để sử dụng

class User
  has_secure_token :token1, :token2
end

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

bọc độc đáo! Cảm ơn: D
mswiszcz

1
Tôi nhận được biến cục bộ không xác định 'has_secure_token'. Bất cứ ý tưởng tại sao?
Adrian Matteo

3
@AdrianMatteo Tôi cũng gặp vấn đề tương tự. Từ những gì tôi đã hiểu has_secure_tokenđi kèm với Rails 5, nhưng tôi đã sử dụng 4.x. Tôi đã làm theo các bước trên bài viết này và bây giờ nó hoạt động với tôi.
Tamara Bernad 10/07/2015


5

Để tạo một mysql thích hợp, varchar 32 GUID

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

Vì chúng tôi đang cố gắng thay thế một ký tự '-', bạn có thể sử dụng tr thay vì gsub. SecureRandom.uuid.tr('-','').upcase. Kiểm tra liên kết này để so sánh giữa tr và gsub.
Sree Raj

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

0

Tôi nghĩ mã thông báo nên được xử lý giống như mật khẩu. Như vậy, chúng nên được mã hóa trong DB.

Tôi đang làm một cái gì đó như thế này để tạo mã thông báo mới độc đáo cho một mô hình:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.