วิธีที่ดีที่สุดในการสร้างโทเค็นที่ไม่เหมือนใครใน 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วิธีการที่จะดำเนินการในขณะนี้ทางรถไฟ 5 การดำเนินงานโทเค็น ActiveRecord ปลอดภัย

- Rails 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

ไม่ได้ใช้เริ่มต้น / ในขณะที่การใช้ห่วง / ทำ
Kain

@kain เหตุผลใด ๆloop do("ในขณะที่ ... ทำ" ประเภทของการวนรอบ) ควรจะใช้ในกรณีนี้ (ที่วงจะต้องทำงานอย่างน้อยหนึ่งครั้ง) แทนที่จะเป็นbegin...while("do ... ในขณะที่" ประเภทของการวนรอบ)?
Krule

7
รหัสที่แน่นอนนี้จะไม่ทำงานเนื่องจาก random_token ถูกกำหนดขอบเขตภายในลูป
Jonathan Mui

1
@Krule ทีนี้เมื่อคุณเปลี่ยนมันให้เป็นข้อกังวลคุณไม่ควรที่จะกำจัดมันModelNameด้วยเหรอ? อาจแทนที่ด้วย self.classหรือไม่ มิฉะนั้นจะไม่สามารถใช้ซ้ำได้มากใช่ไหม
paracycle

1
วิธีแก้ปัญหานั้นไม่ได้รับการคัดค้าน Secure Token นำมาใช้ใน Rails 5 แต่ไม่สามารถใช้ใน Rails 4 หรือ Rails 3 (ซึ่งคำถามนี้เกี่ยวข้องกับ)
Aleks

52

Ryan Bates ใช้รหัสเล็กน้อยใน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 ตัวอักษรแรกของhexdigestIP ที่กำหนด
thekingoftruth

1
สำหรับที่อยู่ IP แบบ 32 บิตมันจะค่อนข้างง่ายที่จะมีตารางการค้นหาของ 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 เป็นค่าที่กำหนดดังนั้นหากคุณมีรหัสที่ไม่ซ้ำกันคุณจะมีโทเค็นที่ไม่ซ้ำกัน
Esse

ฉันไปกับrandom_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6]ที่ ID เป็น ID ของโทเค็น
Slick23

11
ดูเหมือนว่าฉันจะBase64::encode64(id.to_s)เอาชนะจุดประสงค์ของการใช้โทเค็น เป็นไปได้ว่าคุณใช้โทเค็นเพื่อปิดบัง ID และทำให้ทรัพยากรไม่สามารถเข้าถึงได้กับทุกคนที่ไม่มีโทเค็น อย่างไรก็ตามในกรณีนี้บางคนสามารถเรียกใช้Base64::encode64(<insert_id_here>)และพวกเขาจะมีโทเค็นทั้งหมดสำหรับทรัพยากรในเว็บไซต์ของคุณทันที
Jon Lemmon

จำเป็นต้องเปลี่ยนเป็นสิ่งนี้เพื่อทำงานstring = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")
Qasim

14

สิ่งนี้อาจมีประโยชน์:

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

หากคุณต้องการลบอักขระพิเศษใด ๆ ที่ไม่ใช่อาร์กิวเมนต์แรก '+ / =' และอักขระใด ๆ ที่ใส่อาร์กิวเมนต์ที่สอง '0aZ' และ 15 คือความยาวที่นี่

และถ้าคุณต้องการลบช่องว่างพิเศษและอักขระบรรทัดใหม่กว่าเพิ่มสิ่งที่ชอบ:

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

หวังว่านี่จะช่วยทุกคน


3
หากคุณไม่ต้องการตัวละครแปลก ๆ เช่น "+ / =" คุณสามารถใช้ SecureRandom.hex (10) แทน base64
Min Ming Lo

16
SecureRandom.urlsafe_base64ประสบความสำเร็จในสิ่งเดียวกันเช่นกัน
iterion

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' ความคิดใดทำไม
Adrian Matteo

3
@AdrianMatteo ฉันมีปัญหาเดียวกันนี้ จากสิ่งที่ฉันเข้าใจhas_secure_tokenมาพร้อมกับ Rails 5 แต่ฉันใช้ 4.x ฉันได้ทำตามขั้นตอนในบทความนี้แล้วและตอนนี้ก็ใช้ได้สำหรับฉัน
Tamara Bernad


5

เพื่อสร้าง mysql, varchar 32 GUID ที่เหมาะสม

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

เนื่องจากเราพยายามแทนที่ตัวอักษร '-' คุณสามารถใช้ tr แทน gsub SecureRandom.uuid.tr('-','').upcase. ตรวจสอบลิงค์นี้เพื่อเปรียบเทียบระหว่าง tr และ gsub
Sree Raj


0

ฉันคิดว่าโทเค็นควรได้รับการจัดการเช่นเดียวกับรหัสผ่าน ดังนั้นควรเข้ารหัสใน DB

ฉันกำลังทำสิ่งนี้เพื่อสร้างโทเค็นใหม่ที่ไม่เหมือนใครสำหรับโมเดล:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.