如何在Golang中生成唯一的随机字母数字令牌?

huangapple go评论76阅读模式
英文:

How to generate unique random alphanumeric tokens in Golang?

问题

对于一个RESTful后端API,我想生成唯一的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个字符的字母数字令牌的最佳方法是什么?

英文:

For a RESTful backend API, I want to generate unique url tokens to be used to authenticate users.

The unique data provided at registration to generate tokens are email addresses. But after generating tokens and sending that to the users, I don't need to decrypt received tokens to get email or other information. So the encryption can be one-way.

Initially I used bcrypt to do so:

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)

}

But since the tokens come as a url parameter (like /api/path/to/{token}) I can not use bcrypt because it generates tokens containing / like this:

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

which will break the routing.

So I'm wondering what is the best way to generate some unique 16-32 character alphanumeric tokens based on emails in Golang?

答案1

得分: 22

如前所述,你的做法是错误的,而且非常不安全。

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

As it was already mentioned you are doing it wrong and this is super insecure.

  1. Generate secure token using crypto package. This token completely random and not associated with any email.
func GenerateSecureToken(length int) string {
	b := make([]byte, length)
	if _, err := rand.Read(b); err != nil {
		return ""
	}
	return hex.EncodeToString(b)
}
  1. Create database table which maps this token to user identifier and during API request validate it.

答案2

得分: 7

选项1:对bcrypt输出进行md5哈希处理

感谢楼主在很大程度上回答了自己的问题 如何在Golang中生成唯一的随机字母数字令牌?

我认为这个方法可以满足你的需求(长度为32个字符):

package main

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

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

// GenerateToken 根据提供的电子邮件字符串生成唯一的令牌
func GenerateToken(email string) string {
	hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("要存储的哈希值:", string(hash))

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

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

$ go run main.go

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

令牌:90a514ab93e2c32fdd1072154b26a100


以下是我之前提供的两个建议,我怀疑它们对你来说可能不会更好,但对其他人可能有帮助。


选项2:base64

过去,我使用base64编码来在加密/哈希后使令牌更易于传输。这是一个可行的示例:

package main

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

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

// GenerateToken 根据提供的电子邮件字符串生成唯一的令牌
func GenerateToken(email string) string {
	hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("要存储的哈希值:", string(hash))

	return base64.StdEncoding.EncodeToString(hash)
}

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

$ go run main.go

要存储的哈希值:$2a$10$cbVMU9U665VSqpfwrNZWOeU5cIDOe5iBJ8ZVa2yJCTsnk9MEZHvRq

令牌:JDJhJDEwJGNiVk1VOVU2NjVWU3FwZndyTlpXT2VVNWNJRE9lNWlCSjhaVmEyeUpDVHNuazlNRVpIdlJx

如你所见,不幸的是,这种方法无法提供16-32个字符的长度。如果你可以接受长度为80个字符,那么这个方法可能适合你。


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

我还尝试了url.PathEscape和url.QueryEscape以确保全面。虽然它们与base64示例有相同的问题(长度较长,但稍微短一些),但至少它们“应该”在路径中起作用:

// GenerateToken 根据提供的电子邮件字符串生成唯一的令牌
func GenerateToken(email string) string {
	hash, err := bcrypt.GenerateFromPassword([]byte(email), bcrypt.DefaultCost)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("要存储的哈希值:", 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

希望对你有所帮助!

英文:

Option 1: md5 hash the bcrypt output

Props to OP for mostly answering his own question 如何在Golang中生成唯一的随机字母数字令牌?

I think it'll satisfy everything you're looking for (32-character length):

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"))
}

> $ go run main.go

> Hash to store: $2a$10$B23cv7lDpbY3iVvfZ7GYE.e4691ow8i7l6CQXkmz315fbg4jLzoue

> token: 90a514ab93e2c32fdd1072154b26a100


Below are 2 of my previous suggestions that I doubt will work better for you, but could be helpful for others to consider.


Option 2: base64

In the past, I've used base64 encoding to make tokens more portable after encryption/hashing. Here's a working example:

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"))
}

> $ go run main.go

> Hash to store: $2a$10$cbVMU9U665VSqpfwrNZWOeU5cIDOe5iBJ8ZVa2yJCTsnk9MEZHvRq

> token: JDJhJDEwJGNiVk1VOVU2NjVWU3FwZndyTlpXT2VVNWNJRE9lNWlCSjhaVmEyeUpDVHNuazlNRVpIdlJx

As you can see, this unfortunately doesn't provide you with a 16-32 character length. If you're okay with the length being 80 long, then this might work for you.


Option 3: url path/query escapes

I also tried url.PathEscape and url.QueryEscape to be thorough. While they have the same problem as the base64 example (length, though a bit shorter), at least they "should" work in the path:

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

答案3

得分: 6

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))
 }

哈希碰撞的可能性微乎其微(因此不能保证是唯一的),但在实际实现中,这并不是一个问题。

英文:

TL;DR

Please don't do this, it's not secure!. Use an existing authentication library or design a better approach.

Explaination

Authentication mechanisms can be tricky to implement properly.

Since these tokens are for authentication purposes, you don't just want them to be unique, you also need them to be unguessable. An attacker should not be able to calculate a users authentication token.

Your current implementation uses the users email address as the secret input for bcrypt. bcrypt was designed as a secure password hashing algorithm and is hence quite computationally expensive to run. Hence you probably don't want to be doing this in every request.

More importantly, your tokens are not secure. If I know your algorithm then I can generate a token for anyone by simply knowing their email address!

Also, with this approach, you cannot revoke or change a compromised token as it is calculated from the users email address. This is also a major security concern.

There are a few different approaches you could take, depending on whether you need stateless authentication or the ability to revoke tokens.

Additionally, as a matter as good practice, authentication/session tokens should not be placed in a URL as it is much easier for these to accidentally leak (e.g. cached, available to proxy servers, accidentally stored in browser history etc).

Identifiers Only?

If you aren't using your tokens for authentication then simply use a hash function on a users email address. For example, Gravatar that calculate the MD5 of a the users lowercase email address and use this to uniquely identify a user. For example:

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

There is an infinitesimal chance of a hash collision (and hence not guaranteed to be unique) but obviously in a real life implementation this hasn't been an issue.

huangapple
  • 本文由 发表于 2017年7月24日 00:02:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/45267125.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定