【Go Web开发】发送用户激活tokens
上一篇文章我们实现了用户激活token创建,接下来是将激活token添加到用户注册处理程序registerUSerHandler中,这样在用户注册时生成一个激活token然后添加到欢迎邮件中发送给用户确认,类似如下:
Hi,
Thanks for signing up for a Greenlight account. We're excited to have you on board!
For future reference, your user ID number is 123.
Please send a request to the `PUT /v1/users/activated` endpoint with the following JSON body to activate your account:
{"token": "Y3QMGX3PJ3WLRL2YRTQGQ6KRHU"}
Please note that this is a one-time use token and it will expire in 3 days.
Thanks,
The Greenlight Team
邮件中最重要的部分是我们提示用户通过调用PUT请求到API服务激活账号,而不是点击一个包含token的链接。
让用户点击链接通过GET请求当然很方便,但对于API服务来说优缺点,尤其是:
- 这违反了HTTP的原则,GET方法只能用于检索资源的“安全”请求,而不能用于修改某些内容的请求(比如用户的激活状态)。
- 用户的网络浏览器或反病毒软件可能会在后台预取链接URL,无意中激活帐户。Stack Overflow很好地解释了这种风险:
这可能会导致这样一种情况:一个恶意的用户(Eve)想要使用别人的邮箱(Alice)创建一个帐户。Eve注册了,Alice收到了一封邮件。Alice打开邮件,因为她对一个她没请求的帐户感到好奇。她的浏览器(或反病毒软件)在后台请求URL,无意中激活了帐户。
最重要的是,你要确保任何改变应用程序状态的操作(包括激活用户)都只通过POST、PUT、PATCH或DELETE请求执行——而不是通过GET请求。
注意:如果你的API是一个网站的后端,那么你可以调整这封邮件,要求用户点击一个链接,把用户带到你的网站上的一个页面。然后,用户可以单击页面上的一个按钮来“确认激活”,这将对您的API服务执行PUT请求来激活用户。我们将在下一章中更详细地讨论这个模式。
但现在,我们先更新欢迎电子邮件模板,包括激活token,如下所示:
{{define "subject"}} Welcome to Greenlight!{{end}}
{{define "plainBody"}}
Hi,
Thanks for singing up for a Greenlight account. We're excited to have you on board!
for future reference, your use ID number is {{.userID}}.
please send a request to the `PUT /v1/users/activated` endpoint with flowing JSON
body to activate your account
{"token": "{{.activationToken}}"}
please note that this is a one-time use token and ti will expire in 3 days
Thanks,
The Greenlight Team
{{end}}
{{define "htmlBody"}}
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p>Hi,</p>
<p>Thanks for signing up for a Greenlight account. We're excited to have you on board!</p>
<p>For future reference, your user ID number is {{.userID}}.</p>
<p>Please send a request to the <code>PUT /v1/users/activated</code> endpoint with the
fllowing JSON body to activate your account:</p>
<pre><code>
{"token": {{.activationToken}}}
</code></pre>
<p>Please note that this is a one-time use token and it will expire in 3 days.</p>
<p>Thanks,</p>
<p>The Greenlight Team</p>
</body>
</html>
{{end}}
接下来需要更新registerUserHandler处理程序生成新的激活token并以动态数据写入到邮件模版中包括用户ID。如下所示:
File: cmd/api/user.go
package main
...
func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
...
//插入用户信息到数据库
err = app.models.Users.Insert(user)
if err != nil {
switch {
//如果错误是ErrDuplicateEmail,使用v.AddError()方法手动添加校验错误信息
case errors.Is(err, data.ErrDuplicateEmail):
v.AddError("email", "a user with this email address already exists")
app.failedValidationResponse(w, r, v.Errors)
default:
app.serverErrorResponse(w, r, err)
}
return
}
//用户数据插入表之后,为用户生成新的激活token
token, err := app.models.Tokens.New(user.ID, 3 * 24 * time.Hour, data.ScopeActivation)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
//使用background创建goroutine异步发送邮件
app.background(func() {
//现在要传入多个数据到邮件模版,我们创建一个map
data := map[string]interface{}{
"activationToken": token.Plaintext,
"userID": user.ID,
}
// 发送欢迎邮件,并传入map作为动态数据
err = app.mailer.Send(user.Email, "/user_welcome.tmpl", data)
if err != nil {
app.logger.Error(err, nil)
}
})
//将返回码改为202,表示客户端请求被接受,但处理没有完成。
err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
下面我们来看看是功能否正常。重启服务,使用faith@example.com邮箱注册一个新的用户。
$ BODY='{"name": "Faith Smith", "email": "faith@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/users
{
"user": {
"id": 1,
"create_at": "2022-01-03T14:10:11+08:00",
"name": "Faith Smith",
"email": "faith@example.com",
"activated": false
}
}
如果你查看Mailtrap收件箱,将看到新的欢迎邮件包含激活token,如下所示:
在上图中可以看到向邮箱faith@example.com发送了激活token为:7IYVRNWW2W3DXUM3S7Q3OVRAUU。如果你跟随本本系列文章操作,你的token应该是和我这里不同的26个字符串。出于兴趣,我们快速查看一下PostgreSQL数据库中的token表。同样,在你的数据库中存储的确切值是不同的,但它应该看起来像这样:
$ psql $GREENLIGHT_DB_DSN
psql (13.4)
Type "help" for help.
greenlight=> select * from tokens;
hash | user_id | expiry | scope
--------------------------------------------------------------------+---------+------------------------+------------
\x9290066d763e5a0b0b702273b64d36c12da0daef491807b781576dbff2d1a8c1 | 1 | 2022-01-06 14:10:11+08 | activation
(1 row)
我们看到这里存储的是激活token的哈希值:
9290066d763e5a0b0b702273b64d36c12da0daef491807b781576dbff2d1a8c1
正如前面提到的,psql总是将bytea列中的值显示为十六进制编码的字符串。因此,我们在这里看到的是明文7IYVRNWW2W3DXUM3S7Q3OVRAUU的SHA-256哈希值的十六进制编码,我们在欢迎邮件中发送了这个token。
注意:你可以在线验证激活token(7IYVRNWW2W3DXUM3S7Q3OVRAUU)的SHA-256的哈希值,和数据库中存储的16进制字符串是相匹配的。
注意邮件中的user_id是1(这里因为之前数据已删除),过期时间正确设置为3天之后,scope只为activation。
附加内容
生成tokens接口
您可能还想提供一个独立的接口来生成并向用户发送激活token。如果你需要重新发送激活token,例如当用户在3天的时间限制内没有激活他们的帐户,或他们从来没有收到欢迎电子邮件,生成token接口是有用的。
实现token生成接口模式和其他接口是差不多的,因此就不在这重复。