Go语言使用Bcrypt实现加密或验证登录密码
Bcrypt 就是一款加密工具,它生成的密文是60位的,而且每次加密生成的值是不一样的。 MD5 加密后的值是32位的,且每次加密后的密文都是一样的。 保存密码,一般我们都推荐使用 Bcrypt 进行加密,而不使用 MD5.
Bcrypt 加密后的值举例:
# 比如加密 admin, 两次结果不一样,但都以 $2a 开头
$2a$10$cL3WHWi3/x96MII1pwm4NOMRESxbAHnImp.tV5AMIJCneIkp2IAF2
$2a$10$P1zZnMm8/KYVseSkkfh0T.i2cVwydZ5L/5rZEALWCo3f9TmVLmM9q
# 加密 123456:
$2a$10$wtJie2Wc93SqCCri5u/f4uZX7ATSSyMxlrCTEkPmNHLl9Oa0QdLim
MD5 加密后的值举例:
# 比如加密 admin, MD5两次结果都一样
21232f297a57a5a743894a0e4a801fc3
21232f297a57a5a743894a0e4a801fc3
# 加密 123456:
e10adc3949ba59abbe56e057f20f883e
初始化项目
(1) 创建一个 pwd_demo 的目录。
$ mkdir pwd_demo
(2) 使用 go mod 命令来管理包,设置项目的包名为 pwd_demo
:
$ mkdir pwd_demo
$ go mod init pwd_demo
go: creating new go.mod: module pwd_demo
go: to add module requirements and sums:
go mod tidy
该命令会创建一个 go.mod 的文件,用于记录引入的包版本,类似于 node 的 package.json 或 php 的 composer.json 文件。
(3) 创建一个 main.go 文件:
package main
import (
"fmt"
)
func main() {
fmt.Println("=> hi password!")
}
(4) 运行代码
$ go run main.go
=> hi password!
启动正常。
编写 utils 包
在 go 语言中,一个包就是一个目录,即同一个目录下的同级的所有go文件应该属于一个包。
这里我们创建一个 utils 目录,并将包名命名为 utils (即和目录名相同,不相同也可以)。
(1) 创建一个 utils 目录, 并创建一个 pwd.go 文件:
$ mkdir utils
$ cd utils
$ touch pwd.go
文件 pwd.go 代码如下:
package utils
import "golang.org/x/crypto/bcrypt"
// 密码加密: pwdHash 同PHP函数 password_hash()
func PasswordHash(pwd string) (string, error) {
bytes, err := bcrypt.GenerateFrompwd([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bytes), err
}
// 密码验证: pwdVerify 同PHP函数 password_verify()
func PasswordVerify(pwd, hash string) bool {
err := bcrypt.CompareHashAndpwd([]byte(hash), []byte(pwd))
return err == nil
}
执行测试用例:
$ go test -v utils/pwd_test.go utils/pwd.go
utils/pwd.go:3:8:
no required module provides package golang.org/x/crypto/bcrypt;
to add it:
go get golang.org/x/crypto/bcrypt
按照提示,安装 crypto/bcrypt 包:
$ go get golang.org/x/crypto/bcrypt
go: downloading golang.org/x/crypto
v0.0.0-20210711020723-a769d52b0f97
go get: added golang.org/x/crypto
v0.0.0-20210711020723-a769d52b0f97
像 md5 加密方法,在Go自带的包里都有,即 "crypto/md5" 。 但是自带的包里没有 bcrypt 机密库,在其 golang.org/x/crypto 加密扩展包里有,所以需要手动下载。
## 语言自带的
import "crypto/md5"
## 扩展子模块里,需要自己下载
import "golang.org/x/crypto/bcrypt"
如果不能下载,请使用 goproxy.io
:
# 配置 GOPROXY 环境变量
export GOPROXY=https://goproxy.io,direct
执行测试代码
go test 命令用于执行测试文件,它会自动读取源码目录下面名为 _test.go
的文件,并执行测试用例。
测试文件中的测试用例要以 func Test
开始,比如:func TestXXXX(t *testing.T)
。
测试用的参数有且只有一个,即 t *testing.T
。
测试文件 pwd_test.go 代码如下:
// pwd_test.go
package utils
import (
"testing"
)
// go test -v utils/pwd_test.go utils/pwd.go
func TestA(t *testing.T) {
t.Log("TestA")
}
func TestPasswordHash(t *testing.T) {
t.Log("--> TestHelloWorld ")
pwd := "admin123"
hash, _ := PasswordHash(pwd)
t.Log("--> 输入密码:", pwd)
t.Log("--> 生成hash:", hash)
// $2a$10$lRewHvjtPrYZK4TQy.TWDemBMqwIEy/.IVoB7x/2KfqrjzZJNP2ia
// $2a$10$KEl9ZHfD4gAPu/hgXAjAm.TLgWi5ce7EzBgTdhBfW5IOimtOSfin2
match := PasswordVerify(pwd, hash)
t.Log("--> 验证结果:", match)
}
func TestPasswordVerify(t *testing.T) {
t.Log("--> TestpwdVerify ")
pwd := "admin2"
// PHP 生成的密码为 $2y$ 开头 (PHP实现 $2a$ 时有bug,修复时改成了 $2y$)
hash := "$2y$10$f7ZKW1ZOR4UzGM37.GTmTuY6RmJHknfSwhBacki.Yro1I1kIddEiu"
match := PasswordVerify(pwd, hash)
t.Log("--> TestpwdVerify 验证结果:", match)
if match == false {
t.Errorf("TestpwdVerify failed. Got false, expected true.")
}
}
执行测试代码(只返回结果):
$ go test utils/pwd_test.go utils/pwd.go
ok command-line-arguments 0.813s
如果想查看执行过程和测试代码里的日志,使用 -v
参数即可。
$ go test -v utils/pwd_test.go utils/pwd.go
=== RUN TestA
pwd_test.go:9: TestA
--- PASS: TestA (0.00s)
=== RUN Testpwd
pwd_test.go:13: --> TestHelloWorld
pwd_test.go:18: --> 输入密码: admin123
pwd_test.go:19: --> 生成hash: $2a$10$mqS/IPrw.
pwd_test.go:24: --> 验证结果: true
--- PASS: Testpwd (0.17s)
=== RUN TestpwdVerify
pwd_test.go:28: --> TestpwdVerify
pwd_test.go:35: --> TestpwdVerify 验证结果: true
--- PASS: TestpwdVerify (0.08s)
PASS
ok command-line-arguments 0.843s
其他地方使用
若有在其他包中使用自定义的 PasswordHash 和 PasswordVerify 方法,直接使用 import 引入 utils 即可。
在 main.go 中使用示例如下:
package main
import (
"fmt"
"password_demo/utils"
)
func checkLogin(loginPwd string, hash string) bool {
return utils.PasswordVerify(loginPwd, hash)
}
func main() {
fmt.Println("=> hi password!")
// loginPwd := "123456"
loginPwd := "admin"
dbPwd := "$2a$10$ND3xmjbf27oBfjh9AcHxxuLViQubQzVwR/DEYcfjB9RwwR.8h8j6W"
if checkLogin(loginPwd, dbPwd) {
fmt.Println("=> 密码输入正确!")
} else {
fmt.Println("=> failed 密码错误,请重新输入!")
}
}
$ go run main.go
=> hi password!
=> 密码输入正确!
拓展内容
(1) PHP 语言中的相关函数:
md5() — 计算字符串的 MD5 散列值
crypt() — 单向字符串散列
password_hash() — 创建密码的散列(hash)
password_verify() — 验证密码是否和散列值匹配
password_hash() 使用了一个强的哈希算法,来产生足够强的盐值,并且会自动进行合适的轮次。 password_hash() 是 crypt() 的一个简单封装,并且完全与现有的密码哈希兼容。 推荐使用 password_hash()。
(2) go test 执行测试报错:
使用 go test 执行单个测试文件报错。
$ go test utils/pwd_test.go
# command-line-arguments [command-line-arguments.test]
utils/pwd_test.go:16:13: undefined: pwdHash
utils/pwd_test.go:23:11: undefined: pwdVerify
utils/pwd_test.go:34:11: undefined: pwdVerify
FAIL command-line-arguments [build failed]
FAIL
这是因为在测试用例代码里并没有定义使用的方法,需要报 pwd.go 加上即可。
$ go test utils/pwd_test.go utils/pwd.go
ok command-line-arguments 0.847s
参考内容
https://www.twle.cn/t/877
https://stackoverflow.com/questions/15733196/where-2x-prefix-are-used-in-bcrypt
https://blog.csdn.net/mrtwenty/article/details/91952707
https://zhuanlan.zhihu.com/p/92845975
https://www.jianshu.com/p/fc910a1f7c8d/
https://www.php.net/manual/zh/function.pwd-hash.php