Go 如何实现兼容 PHP 的密码加密解密

最近正在迁移自己的小项目,项目之前是基于 Laravel5.5 开发的。整个用户登陆也是基于框架的 Auth 包认证的。其中用户密码这块也是用到了 PHP 内置的函数 password_hash,用它进行密码加密。而且 PHP 默认使用的 PASSWORD_BCRYPT 算法。在使用 Go 的迁移过程中需要认证密码,还需要兼容 password_hash, 所以就把这个过程记录下来。使用下面的例子来说明如何使用 GO bcrypt 包来对你的密码进行 hash 和 salt 加密

对于这个例子,我将创建一个控制台应用程序,用于演示如何获取用户输入的密码并使用它生成 salt 哈希值。 完成此操作后,我将通过比较密码与其散列版本来验证密码是否正确。

获取用户输入的密码

开始我们先创建一个可以在控制台读取用户输入的的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func getPwd() []byte {
fmt.Println("Enter a password")
var pwd string

// 读取用户输入
_, err := fmt.Scan(&pwd)
if err != nil {
log.Println(err)
}
return []byte(pwd)

}
````

### Hash & Salt 用户的密码
现在我们可以使用 Go 的 [bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) 包提供的`GenerateFromPassword(password []byte, cost int)([]byte, error)`方法对用户的密码进行 hash 和 salt 加密了。
> GenerateFromPassword 方法以给定 cost 值返回密码的 Bcrypt 算法的 Hash 值,如果提供的 cost 值小于 Mincost 的话,将会默认使用 DefaultCost 代替
>
使用 `GenerateFromPassword` 函数的一个优势就是我们不需要自己来编写函数来生成 Salt,因为它会为我们自动生成一个 Salt。

下面的函数使用 `GenerateFromPassword` 生成 salted 哈希值,该哈希值作为字节切片返回。 然后我们将字节切片作为字符串返回,以便我们可以将 salted 哈希存储在数据库中作为用户密码。

func hashAndSalt(pwd []byte) string {
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
log.Println(err)
}
return string(hash)
}

1
2
3
4
### 目前我们做了什么
到目前为止,我们已经创建了一个接受来自控制台的用户输入并将其作为字节切片返回的函数。 然后,
我们再创建一个可以接收用户输入并返回 salted 哈希值的函数。下面就是代码事例。

package main

import (
“fmt”
“log”
“golang.org/x/crypto/bcrypt”
)

func main() {
for {
pwd := getPwd()
hash := hashAndSalt(pwd)
fmt.Println(“Salted Hash”, pwd)
}
}

1
2

如果你运行上面的代码,将会得到下面的结果

$ Enter a password
$ foobar
Salted Hash $2a$10$………..

1
2
3
4
5
6
7
8
这里需要的注意的是我使用 for 循环调用函数,直到我强制停止它。对于那些不熟悉 GO 的人来讲,这个就和其他语言的 `while (true){}`是一样的效果。
### 验证密码
最后一件事儿就是需要验证密码的正确性来登陆我们的系统,我们可以使用 [bcrypt](https://godoc.org/golang.org/x/crypto/bcrypt) 包提供的`CompareHashAndPassword(hashedPassword, password []byte) error`函数

> CompareHashAndPassword 将 bcrypt 哈希密码与其纯文本进行比较。 成功时返回nil,失败时返回错误
>
我们使用`CompareHashAndPassword`函数来创建另一个返回 bool 值的函数让我们知道密码是否匹配。

func comparePasswords(hashedPwd string, plainPwd []byte) bool {

byteHash := []byte(hashedPwd)
err := bcrypt.CompareHashAndPassword(byteHash, plainPwd)\
if err != nil {
    log.Println(err)
    return false
}
return true

}

1
2
3
4
5
6


### 更新 Main 函数
我们现在可以更新我们的主要功能,以便我们能够输入密码,获取其盐渍哈希,然后再次输入密码,并查明我们的第二个密码是否与我们输入的第一个密码相匹配。
我们现在修改一个 main 函数,当我们输入密码的时候,获取 salted 哈希值,然后再次输入密码,来检查我们的密码是否匹配。

func main() {
for {
pwd := getPwd()
hash := hashAndSalt(pwd)
pwd2 := getPwd()
pwdMatch := comparePasswords(hash, pwd2)
fmt.Println(“Passwords Match?”, pwd)
}
}

1
2
3

### 全部代码

package main

import (
“fmt”
“log”
“golang.org/x/crypto/bcrypt”
)

func main() {
for {
// 输入密码 获取 hash 值
pwd := getPwd()
hash := hashAndSalt(pwd)
// 再次输入密码验证
pwd2 := getPwd()
pwdMatch := comparePasswords(hash, pwd2)
fmt.Println(“Passwords Match?”, pwd)
}
}

func getPwd() []byte {
fmt.Println(“Enter a password”)
var pwd string
_, err := fmt.Scan(&pwd)
if err != nil {
log.Println(err)
}
return []byte(pwd)
}

func hashAndSalt(pwd []byte) string {
hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
if err != nil {
log.Println(err)
}
return string(hash)
}

func comparePasswords(hashedPwd string, plainPwd []byte) bool {
byteHash := []byte(hashedPwd)

err := bcrypt.CompareHashAndPassword(byteHash, plainPwd)
if err != nil {
    log.Println(err)
    return false
}
return true

}

以上便是 GO 转 php 的加密函数的过程,如果有任何错误或者不当的地方欢迎进行改进。