【问题标题】:How to generate unique random alphanumeric tokens in Golang?如何在 Golang 中生成唯一的随机字母数字标记?
【发布时间】:2017-12-29 05:52:25
【问题描述】:

对于 RESTful 后端 API,我想生成 unique url 令牌以用于对用户进行身份验证。

注册时提供的用于生成令牌的唯一数据是电子邮件地址。但是在生成令牌并将其发送给用户之后,我不需要解密收到的令牌来获取电子邮件或其他信息。所以加密可以是单向的。

最初我使用 bcrypt 这样做:

func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    return string(hash)

}

但由于令牌作为 url 参数(如 /api/path/to/{token})我不能使用 bcrypt,因为它会生成包含 / 的令牌,如下所示:

"$2a$10$NebCQ8BD7xOa82nkzRGA9OEh./zhBOPcuV98vpOKBKK6ZTFuHtqlK"

这会破坏路由。

所以我想知道在 Golang 中基于电子邮件生成一些独特的 16-32 字符字母数字令牌的最佳方法是什么?

【问题讨论】:

  • @AlexK。不同之处在于我希望令牌不仅是随机的,而且是唯一的。
  • 您可以对任何随机数据使用 URL 安全的 Base64 编码。请参阅导出的 encoding/base64/URLEncoding 变量。使用它的输出,您可以安全地将其包含在 URL 中。
  • 如果您可以为每个用户生成一个唯一编号,那么您可以使用 base 64 的 URL 安全版本。
  • 难道不能简单地对该字符串进行urlencode吗?任何人都不应该使用该算法为其他人生成令牌吗?

标签: go token


【解决方案1】:

正如已经提到的那样,您做错了,这是非常不安全的。

  1. 使用加密包生成安全令牌。此令牌完全随机,不与任何电子邮件关联。
func GenerateSecureToken(length int) string {
    b := make([]byte, length)
    if _, err := rand.Read(b); err != nil {
        return ""
    }
    return hex.EncodeToString(b)
}
  1. 创建将此令牌映射到用户标识符的数据库表,并在 API 请求期间对其进行验证。

【讨论】:

  • 但这非常昂贵,因为您需要存储所有数据......
  • 为什么这超级不安全?可预测性?
  • 应该多长时间?
【解决方案2】:

TL;DR

请不要这样做,它不安全!。使用现有的身份验证库或设计更好的方法。

说明

身份验证机制可能难以正确实施。

由于这些令牌用于身份验证,您不仅希望它们唯一,还需要它们不可猜测。攻击者应该无法计算用户身份验证令牌。

您当前的实现使用用户电子邮件地址作为 bcrypt 的秘密输入。 bcrypt 被设计为一种安全的密码散列算法,因此运行起来非常昂贵。因此,您可能不想在每个请求中都这样做。

更重要的是,您的令牌不安全。如果我知道你的算法,那么我只需知道他们的电子邮件地址就可以为任何人生成一个令牌!

此外,使用这种方法,您无法撤销或更改受损令牌,因为它是根据用户电子邮件地址计算得出的。这也是一个主要的安全问题。

您可以采用几种不同的方法,具体取决于您是否需要无状态身份验证或撤销令牌的能力。

此外,作为一种良好做法,身份验证/会话令牌不应放置在 URL 中,因为这些令牌更容易意外泄漏(例如缓存、代理服务器可用、意外存储在浏览器历史等中)。

仅标识符?

如果您不使用令牌进行身份验证,那么只需在用户电子邮件地址上使用哈希函数即可。例如,Gravatar 计算用户小写电子邮件地址的MD5 并使用它来唯一标识用户。例如:

func GravatarMD5(email string) string {
    h := md5.New()
    h.Write([]byte(strings.ToLower(email)))
    return hex.EncodeToString(h.Sum(nil))
 }

哈希冲突的可能性很小(因此不保证是唯一的)但显然在现实生活中这不是问题。

【讨论】:

  • 这当然是安全的——它只是一种创建唯一电子邮件验证令牌的昂贵方式,不是吗?它不是在每个请求上发送的身份验证承载令牌。
  • 原问题询问如何生成令牌来验证用户,而不是创建电子邮件验证令牌。
【解决方案3】:

选项 1:md5 散列 bcrypt 输出

支持 OP 主要回答他自己的问题 :)

我认为它会满足您正在寻找的一切(32 个字符长度):

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "log"

    "golang.org/x/crypto/bcrypt"
)

// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    hasher := md5.New()
    hasher.Write(hash)
    return hex.EncodeToString(hasher.Sum(nil))
}

func main() {
    fmt.Println("token:", GenerateToken("bob@webserver.com"))
}

$ 运行 main.go

哈希存储:$2a$10$B23cv7lDpbY3iVvfZ7GYE.e4691ow8i7l6CQXkmz315fbg4jLzoue

令牌:90a514ab93e2c32fdd1072154b26a100


以下是我之前提出的 2 条建议,我怀疑它们是否对您更有效,但可能对其他人有所帮助。


选项 2:base64

过去,我使用 base64 编码使令牌在加密/散列后更易于移植。这是一个工作示例:

package main

import (
    "encoding/base64"
    "fmt"
    "log"

    "golang.org/x/crypto/bcrypt"
)

// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    return base64.StdEncoding.EncodeToString(hash)
}

func main() {
    fmt.Println("token:", GenerateToken("bob@webserver.com"))
}

$ 运行 main.go

哈希存储:$2a$10$cbVMU9U665VSqpfwrNZWOeU5cIDOe5iBJ8ZVa2yJCTsnk9MEZHvRq

代币:JDJhJDEwJGNiVk1VOVU2NjVWU3FwZndyTlpXT2VVNWNJRE9lNWlCSjhaVmEyeUpDVHNuazlNRVpIdlJx

如您所见,不幸的是,并没有为您提供 16-32 个字符的长度。如果您可以接受 80 长的长度,那么这可能适合您。


选项 3:url 路径/查询转义

我也试过 url.PathEscape 和 url.QueryEscape 来彻底。虽然它们与 base64 示例存在相同的问题(长度,虽然有点短),但至少它们“应该”在路径中工作:

// GenerateToken returns a unique token based on the provided email string
func GenerateToken(email string) string {
    hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Hash to store:", string(hash))

    fmt.Println("url.PathEscape:", url.PathEscape(string(hash)))
    fmt.Println("url.QueryEscape:", url.QueryEscape(string(hash)))

    return base64.StdEncoding.EncodeToString(hash)
}

url.PathEscape: $2a$10$wx1UL6%2F7RD6sFq7Bzpgcc.ibFSW114Tf6A521wRh9rVy8dp%2Fa82x2 url.QueryEscape: %242a%2410%24wx1UL6%2F7RD6sFq7Bzpgcc.ibFSW114Tf6A521wRh9rVy8dp%2Fa82x2

【讨论】:

  • 如何将结果输入 MD5 以缩短生成的令牌,如下所示:hasher := md5.New(); hasher.Write([]byte(token)); return hex.EncodeToString(hasher.Sum(nil))?
  • 好主意,@Karlom - 我也会尝试一下
  • 考虑添加时间跨度有效性github.com/anacrolix/dht/blob/master/tokens.go
  • 关于选项2:有什么理由不存储base64编码的哈希?
猜你喜欢
  • 2011-08-18
  • 2015-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-17
  • 1970-01-01
  • 1970-01-01
  • 2017-06-17
相关资源
最近更新 更多