在 Rails 中创建唯一令牌的最佳方法?

发布于 2024-11-07 10:06:39 字数 611 浏览 5 评论 0 原文

这是我正在使用的。令牌不一定要听才能猜测,它更像是一个短 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,但因为这些是根据用户在应用程序中的操作自动批量创建的(它们放置了本质上是订购并购买代币),让应用程序抛出错误是不可行的。

我想,为了减少冲突的机会,在末尾附加另一个字符串,根据时间生成的东西或类似的东西,但我不希望令牌变得太长。

Here's what I'm using. The token doesn't necessarily have to be heard to guess, it's more like a short url identifier than anything else, and I want to keep it short. I've followed some examples I've found online and in the event of a collision, I think the code below will recreate the token, but I'm not real sure. I'm curious to see better suggestions, though, as this feels a little rough around the edges.

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

My database column for the token is a unique index and I'm also using validates_uniqueness_of :token on the model, but because these are created in batches automatically based on a user's actions in the app (they place an order and buy the tokens, essentially), it's not feasible to have the app throw an error.

I could also, I guess, to reduce the chance of collisions, append another string at the end, something generated based on the time or something like that, but I don't want the token to get too long.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

往日 2024-11-14 10:06:40

Ryan Bates 在他的Railscast 测试版邀请中使用了一些不错的代码。这会生成 40 个字符的字母数字字符串。

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

Ryan Bates uses a nice little bit of code in his Railscast on beta invitations. This produces a 40 character alphanumeric string.

Digest::SHA1.hexdigest([Time.now, rand].join)
月野兔 2024-11-14 10:06:40

这可能是一个较晚的响应,但为了避免使用循环,您也可以递归地调用该方法。对我来说,它看起来和感觉都稍微干净一些。

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

This might be a late response but in order to avoid using a loop you can also call the method recursively. It looks and feels slightly cleaner to me.

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
吝吻 2024-11-14 10:06:40

本文演示了一些非常巧妙的方法:

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"

There are some pretty slick ways of doing this demonstrated in this article:

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

My favorite listed is this:

rand(36**8).to_s(36)
=> "uur0cj2h"
汹涌人海 2024-11-14 10:06:40

如果你想要一些独特的东西,你可以使用类似这样的东西:

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

例如对于像 10000 这样的 id,生成的令牌将像“MTAwMDA=”(并且您可以轻松地解码它的 id,只需使

Base64::decode64(string)

If you want something that will be unique you can use something like this:

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

however this will generate string of 32 characters.

There is however other way:

require 'base64'

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

for example for id like 10000, generated token would be like "MTAwMDA=" (and you can easily decode it for id, just make

Base64::decode64(string)
野稚 2024-11-14 10:06:40

这可能会有所帮助:

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

如果您想删除任何特殊字符,而不是放入第一个参数“+/=”,并将任何字符放入第二个参数“0aZ”,并且 15 是此处的长度。

如果您想删除多余的空格和换行符,请添加以下内容:

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

希望这对任何人都有帮助。

This may be helpful :

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

If you want to remove any special character than put in first argument '+/=' and any character put in second argument '0aZ' and 15 is the length here .

And if you want to remove the extra spaces and new line character than add the things like :

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

Hope this will help to anybody.

牵强ㄟ 2024-11-14 10:06:40

尝试这种方式:

从 Ruby 1.9 开始,uuid 生成是内置的。使用 SecureRandom.uuid 函数。
在 Ruby 中生成指南

这对我很有帮助

Try this way:

As of Ruby 1.9, uuid generation is built-in. Use the SecureRandom.uuid function.
Generating Guids in Ruby

This was helpful for me

孤独陪着我 2024-11-14 10:06:40

你可以使用 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"

you can user has_secure_token https://github.com/robertomiranda/has_secure_token

is really simple to use

class User
  has_secure_token :token1, :token2
end

user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
千紇 2024-11-14 10:06:40

创建正确的 mysql、varchar 32 GUID

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

To create a proper, mysql, varchar 32 GUID

SecureRandom.uuid.gsub('-','').upcase
可是我不能没有你 2024-11-14 10:06:40

Rails 7 具有此功能内置 。请参阅下面的示例:

# Schema: User(token:string, auth_token:string)
class User < ActiveRecord::Base
  has_secure_token
  has_secure_token :auth_token, length: 36
end

user = User.new
user.save
user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
user.auth_token # => "tU9bLuZseefXQ4yQxQo8wjtBvsAfPc78os6R"
user.regenerate_token # => true
user.regenerate_auth_token # => true

Rails 7, has this functionality baked in. See the example below:

# Schema: User(token:string, auth_token:string)
class User < ActiveRecord::Base
  has_secure_token
  has_secure_token :auth_token, length: 36
end

user = User.new
user.save
user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
user.auth_token # => "tU9bLuZseefXQ4yQxQo8wjtBvsAfPc78os6R"
user.regenerate_token # => true
user.regenerate_auth_token # => true
水波映月 2024-11-14 10:06:40
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
╰沐子 2024-11-14 10:06:40

我认为令牌应该像密码一样处理。因此,它们应该在数据库中加密。

我正在做这样的事情来为模型生成一个独特的新令牌:

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

I think token should be handled just like password. As such, they should be encrypted in DB.

I'n doing something like this to generate a unique new token for a 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
猫弦 2024-11-14 10:06:39

-- 2022 年 EOY 更新 --

我回答这个问题已经有一段时间了。以至于我已经有七年没有看过这个答案了。我还看到许多依赖 Rails 来运行业务的组织中使用了这段代码。

说实话,这些天我不会认为我之前的解决方案,或者 Rails 如何实现它,是一个伟大的解决方案。它使用可以通过 PITA 进行调试的回调,本质上是悲观的

-- Update EOY 2022 --

It's been some time since I answered this. So much so that I've not even taken a look at this answer for ~7 years. I have also seen this code used in many organizations that rely on Rails to run their business.

TBH, these days I wouldn't consider my earlier solution, or how Rails implemented it, a great one. Its uses callbacks which can be PITA to debug and is pessimistic ???? in nature, even though there is a very low chance of collision for SecureRandom.urlsafe_base64. This holds true for both long and short-lived tokens.

What I would suggest as a potentially better approach is to be optimistic ???? about it. Set a unique constraint on the token in the database of choice and then just attempt to save it. If saving produces an exception, retry until it succeeds.

class ModelName < ActiveRecord::Base
  def persist_with_random_token!(attempts = 10)
    retries ||= 0
    self.token = SecureRandom.urlsafe_base64(nil, false)
    save!
  rescue ActiveRecord::RecordNotUnique => e
    raise if (retries += 1) > attempts

    Rails.logger.warn("random token, unlikely collision number #{retries}")
    retry
  end
end

What is the result of this?

  • One query less as we are not checking for the existence of the token beforehand.
  • Quite a bit faster, overall because of it.
  • Not using callbacks, which makes debugging easier.
  • There is a fallback mechanism if a collision happens.
  • A log trace (metric) if a collision does happen
    • Is it time to clean old tokens maybe,
    • or have we hit the unlikely number of records when we need to go to SecureRandom.urlsafe_base64(32, false)?).

-- Update --

As of January 9th, 2015. the solution is now implemented in Rails 5 ActiveRecord's secure token implementation.

-- Rails 4 & 3 --

Just for future reference, creating safe random token and ensuring it's uniqueness for the model (when using Ruby 1.9 and 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:

@kain suggested, and I agreed, to replace begin...end..while with loop do...break unless...end in this answer because previous implementation might get removed in the future.

Edit 2:

With Rails 4 and concerns, I would recommend moving this to concern.

# 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文