本文将解释如何在 Go 中使用基于密码的身份验证来注册和登录用户。
任何存储密码的应用程序都必须确保密码被安全地存储。您不能只将密码存储为纯文本,理想情况下,即使您可以访问用户的数据,你也不可能猜到您的用户的密码。
在这篇文章中,我们将介绍在生产环境中使用密码存储方法,并在 Go 中构建一个 Web 应用程序以在实践中进行演示。
概述
存储密码的正确方法
如前所述,我们不应该在收到用户密码时直接存储它们。
我们需要对每一个密码进行加密转换,使其易于验证,但不易被猜到。我们使用单向散列函数(在本例中为 bcrypt 算法)来实现这一点。
bcrypt 是由 Niels Provos 和 David Mazières 设计的密码散列函数,基于 Blowfish 密码,并于 1999 年在 USENIX 上提出。 除了加入盐来防止彩虹表攻击外,bcrypt 还是一种自适应函数:随着时间的推移,可以增加迭代次数以使其变慢,因此即使计算能力增加,它仍然可以抵抗暴力搜索攻击。
bcrypt 函数的输入是密码字符串(最多 72 字节)、数字成本和 16 字节(128 位)盐值。 盐通常是随机值。 bcrypt 函数使用这些输入来计算 24 字节(192 位)的散列。 bcrypt 函数的最终输出是以下形式的字符串:
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]`
bcrypt 函数是 OpenBSD 的默认密码哈希算法,也是某些 Linux 发行版(例如 SUSE Linux)的默认值。
将一段文本转换为哈希很容易,但几乎不可能根据哈希猜出这段文本。
例如,输入密码 abc123xyz、成本 12 和随机盐,bcrypt 的输出是字符串:
$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash
在登录期间,我们可以通过检索该用户名的密码哈希值来检查给定用户名的密码是否正确,并且必须使用 bcrypt 比较函数来验证给定密码与哈希值:
加密的密码 ------- [bcrypt compare] -------> true/false
^
|
登录密码hash ------------------
如果只是简单的拿当前登录密码再进行一次同样的加密,再去与原始的加密结果进行比较是无法成功的,两次加密结果是不一样的,必须使用 bcrypt 的比较函数进行校验。
现在,让我们看看如何使用 HTTP Web 服务器实现注册和登录。
HTTP 服务器实现
我们将构建一个具有两条路由的 HTTP 服务器:/signup
和 /signin
,为了方便演示,我们使用 SQLite 数据库存储用户凭据。
/signup
将接受用户凭据,并将它们安全地存储在我们的数据库中。/signin
将接受用户凭据,并通过将它们与数据库中的条目进行比较来验证它们。
user ---username,password--> (/signup) --> [bcrypt hash] ---store hash--> db
user ---username,password--> (/signin) --> [bcrypt compare] <--retrieve hash-- db
初始化 Web 应用程序
在我们实现密码存储之前,让我们创建我们的数据库并初始化我们的 HTTP 服务器:
创建我们的数据库
使用 sqlite3 命令在我们的项目目录中创建一个新数据库 mydb
cd blogpost-demo-bcrypt-auth
sqlite3 ./mydb
然后,创建用户表,其中包含用户名和密码列:
create table users (
username text primary key,
password text
);
初始化 HTTP 服务器
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/glebarez/go-sqlite"
)
// “db”将保存对我们数据库实例的引用
var db *sql.DB
func main() {
// "Signin" 和 "Signup" 是我们要实现的处理函数
http.HandleFunc("/signin", Signin)
http.HandleFunc("/signup", Signup)
// 初始化我们的数据库连接
initDB()
// 在端口 8000 上启动服务器
log.Fatal(http.ListenAndServe(":8000", nil))
}
func initDB() {
var err error
// 连接到 sqlite 数据库
db, err = sql.Open("sqlite", "./mydb")
if err != nil {
panic(err)
}
}
实现用户注册(Signup)
为了创建用户或注册用户,我们将编写一个接受 POST 请求的处理函数,其请求 JSON 主体为以下形式:
{
"username": "axiaoxin",
"password": "666888"
}
如果用户已成功注册,处理函数将返回 200 状态:
// 创建一个对用户模型
type Credentials struct {
Password string `json:"password", db:"password"`
Username string `json:"username", db:"username"`
}
func Signup(w http.ResponseWriter, r *http.Request) {
// 将请求主体解析并解码为 `Credentials` 实例
creds := &Credentials{}
err := json.NewDecoder(r.Body).Decode(creds)
if err != nil {
// 如果请求体有问题,返回 400 状态
w.WriteHeader(http.StatusBadRequest)
return
}
// 使用 bcrypt 算法对密码进行加盐和哈希散列处理
// 第二个参数是散列的成本,我们设置为 8(这个值可以任意设置,可以多也可以少,取决于你希望利用的计算能力)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(creds.Password), 8)
// 接下来,将用户名和哈希处理后的密码插入数据库
if _, err = db.Exec("insert into users values (?, ?)", creds.Username, string(hashedPassword)); err != nil {
// 如果插入数据库有任何问题,返回 500 错误
w.WriteHeader(http.StatusInternalServerError)
return
}
// 到此,我们完成了数据库的保存,将默认返回状态 200
}
此时,我们可以启动服务器并尝试发送请求来存储一些凭据:
curl -i -XPOST http://localhost:8000/signup \
-H 'Content-Type: application/json' \
-d '{"username":"axiaoxin","password":"666888"}'
如果我们现在检查我们的数据库,我们可以看到密码字段不包含我们刚才发送的密码:
sqlite> select * from users;
axiaoxin|$2a$08$4Ba1Tt7R5DBafKkcFsVJWeaqyf.tPoS8q32fhpq2t5GPBProuOB8S
sqlite>
一旦使用 bcrypt 对密码进行哈希处理,我们就无法逆转哈希。本质上,即使我们可以查看用户数据表,我们自己也不可能知道用户的原始密码。
实现用户登录(Signin)
现在编写登录处理函数,根据我们数据库中用户名和密码对用户进行身份验证。
func Signin(w http.ResponseWriter, r *http.Request) {
// 将请求体解析为 `Credentials` 实例
creds := &Credentials{}
err := json.NewDecoder(r.Body).Decode(creds)
if err != nil {
// 如果请求体有问题,返回 400 状态
w.WriteHeader(http.StatusBadRequest)
return
}
// 根据请求用户名获取数据库中对应的用户
result := db.QueryRow("select password from users where username=?", creds.Username)
if err != nil {
// 如果数据库有问题,返回 500 错误
w.WriteHeader(http.StatusInternalServerError)
return
}
// 我们创建另一个 `Credentials` 实例来存储我们从数据库中获取的凭据
storedCreds := &Credentials{}
// 将获得的密码存储在 storedCreds 中
err = result.Scan(&storedCreds.Password)
if err != nil {
// 如果登录用户名不存在,则发送“未授权”(401) 状态
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusUnauthorized)
return
}
// 如果错误是任何其他类型,则发送 500 状态
w.WriteHeader(http.StatusInternalServerError)
return
}
// 将数据库中的哈希密码与用户当前登录的密码的哈希版本进行比较
if err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(creds.Password)); err != nil {
// 如果两个密码不匹配,返回 401 状态
w.WriteHeader(http.StatusUnauthorized)
return
}
// 到达这里,就意味着用户密码是正确的,并且他们被授权
// 默认将返回200状态
}
现在,我们可以尝试通过向 /signin
路由发出 POST 请求来登录:
curl -i -XPOST http://localhost:8000/signin \
-H 'Content-Type: application/json' \
-d '{"username":"axiaoxin","password":"666888"}'
HTTP/1.1 200 OK
这会给你一个 200 状态码。如果我们使用不正确的密码或不存在的用户名发出请求,我们将获得 401 状态代码:
curl -i -XPOST http://localhost:8000/signin \
-H 'Content-Type: application/json' \
-d '{"username":"axiaoxin","password":"666xxx"}'
HTTP/1.1 401 Unauthorized
如果你想运行本示例的服务器,你可以在这里获取源代码在本地运行。
持久化用户登录
在此示例中,我们简单的演示了用户注册和登录。然而,在现实世界中,大多数用户都希望继续使用您的网站。
在这种情况下,即使他们转到我们网站上的另一个页面,我们也需要知道这个用户已经登录过了。否则,用户将不得不一遍又一遍地登录。
我们可以使用 session 以及 cookie 来实现这一点。 “cookie”是保存在您的用户浏览器中的一段数据,使我们能够在整个网站上识别他们。
后续我们将会有相关文章的学习。