问题
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:
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.
回答2:
Option 1: md5 hash the bcrypt output
Props to OP for mostly answering his own question :)
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
来源:https://stackoverflow.com/questions/45267125/how-to-generate-unique-random-alphanumeric-tokens-in-golang