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