G01 Go 实战:Web 入门 - 邮箱验证
本贴文字大部分摘抄自 L01 Laravel 教程 - Web 开发实战入门 - 9.2 账户激活
账户激活
现在的登录逻辑是,用户一旦注册成功即可进行登录,本帖中我们要加入账号激活功能,只有当用户成功激活自己的账号时才能在网站上进行登录。为此,我们将需要为用户表新增三个字段用于保存用户的激活令牌、激活状态、激活时间。
整个激活流程如下:
- 用户注册成功后,自动生成激活令牌;
- 将激活令牌以链接的形式附带在注册邮件里面,并将邮件发送到用户的注册邮箱上;
- 用户点击注册链接跳到指定路由,路由收到激活令牌参数后映射给相关控制器动作处理;
- 控制器拿到激活令牌并进行验证,验证通过后对该用户进行激活,并将其激活状态设置为已激活;
- 用户激活成功,自动登录;
资源
添加字段
在用户的账号激活功能中,我们需要增加激活令牌 (activation_token
) 、激活状态 (activated
)、激活时间 (email_verified_at
) 三个字段。
app/models/user/user.go
.
.
.
type User struct {
models.BaseModel
Name string `gorm:"type:varchar(255);not null;unique" valid:"name"`
Email string `gorm:"type:varchar(255);unique;" valid:"email"`
Password string `gorm:"type:varchar(255)" valid:"password"`
ActivationToken string `gorm:"varchar(255)"`
Activated int `gorm:"type:int(10)"`
EmailVerifiedAt *time.Time
// gorm:"-" —— 设置 GORM 在读写时略过此字段,仅用于表单验证
PasswordConfirm string `gorm:"-" valid:"password_confirm"`
}
由于我们在前面已经配置过 GORM 的自动迁移,所以我们编辑之后将会自动生成对应字段
生成令牌
我们在前面说过,用户的激活令牌需要在用户创建(注册)之前就先生成好,这样当用户注册成功之后我们才可以将令牌附带到注册链接上,并通过邮件的形式发送给用户。
如果我们需要在模型被创建之前进行一些设置,则可以通过 GORM 模型钩子 BeforeCreate
方法来做到;不过在此之前我们需要先编写令牌生成算法。
创建 strings 包:
pkg/strings/string.go
package strings
import "math/rand"
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
//生成随机字符串,n 代表生成长度
func Rand(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
接下来我们在模型被创建之前进行令牌生成
app/models/user/hooks.go
package user
import (
.
.
.
"goblog/pkg/strings"
"gorm.io/gorm"
)
// BeforeCreate GORM 的模型钩子,创建模型前调用
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
.
.
.
u.ActivationToken = strings.Rand(10)
return
}
邮件程序
GoMail 包
GoMail 是一个简单而高效的 SMTP 电子邮件发送包;关于 SMTP 是什么请自行查阅资料。
Gomail 支持:
- 附件
- 嵌入图像
- HTML 和文本模板
- 特殊字符的自动编码
- SSL 和 TLS
- 使用相同的 SMTP 连接发送多封电子邮件
如何使用 Gomail 包?
- 首先我们将 GoMail 封装到新建的 mail 包中,并统一在这个包里做好错误处理;
- 注册成功时,调用我们封装好的 mail 包进行邮件发送。
创建 mail 包:
pkg/mail/mail.go
package mail
import (
"bytes"
"goblog/pkg/logger"
"goblog/pkg/route"
"gopkg.in/gomail.v2"
"html/template"
"path"
"strings"
)
var (
mailer = gomail.NewDialer("smtp.qq.com", 465, "your@qq.com", "yourpassword")
)
//邮件发送与解析模板
func SendWithTemplate(tplName string, data interface{}, to, subject string) error {
tplName = strings.Replace(tplName, ".", "/", -1)
tplFile := "resources/views/" + tplName + ".gohtml"
t, err := template.New(path.Base(tplFile)).Funcs(template.FuncMap{
"RouteName2URL": route.Name2URL,
}).ParseFiles(tplFile)
var tpl bytes.Buffer
if err = t.Execute(&tpl, data); err != nil {
logger.LogError(err)
return err
}
return Send(to, subject, tpl.String())
}
//邮件发送
func Send(to, subject, body string) (err error) {
m := gomail.NewMessage()
m.SetHeader("From", "your@qq.com")
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", body)
if err = mailer.DialAndSend(m); err != nil{
logger.LogError(err)
}
return err
}
注意:在这里我们使用 QQ邮箱
来进行邮件通信,关于 SMTP 开启请参考 这里
激活路由
我们需要为用户的激活功能设定好路由,该路由将附带用户生成的激活令牌,在用户点击链接进行激活之后,我们需要将激活令牌通过路由参数传给控制器的指定动作,最终生成的激活链接例子如下:
http://localhost:3000/auth/confirm/XVlBzgbaiC
由上面链接我们可以推导出路由的定义应该如下。
routes/web.go
.
.
.
au := new(controllers.AuthController)
r.HandleFunc("/auth/confirm/{token:[A-Za-z]+}", au.ConfirmEmail).Methods("GET").Name("auth.confirm_email")
我们使用视图来构建邮件模板,在用户查收邮件时,该模板将作为内容展示视图。接下来我们需要创建一个用于渲染注册邮件的 confirm_email
视图。
resources/views/auth/confirm_email.gohtml
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>注册确认链接</title>
</head>
<body>
<h1>感谢您在 Goblog App 网站进行注册!</h1>
<p>
请点击下面的链接完成注册:
<a href="http://localhost:3000{{ RouteName2URL "auth.confirm_email" "token" .ActivationToken }}">
http://localhost:3000{{ RouteName2URL "auth.confirm_email" "token" .ActivationToken }}
</a>
</p>
<p>
如果这不是您本人的操作,请忽略此邮件。
</p>
</body>
</html>
登录时检查是否已激活
在前面加入的登录操作中,用户即使没有激活也能够正常登录。接下来我们需要对之前的登录代码进行修改,当用户没有激活时,则视为认证失败,并显示消息提醒去引导用户查收邮件。
app/Http/Controllers/auth_controller.go
.
.
.
func (*AuthController) DoLogin(w http.ResponseWriter, r *http.Request) {
email := r.PostFormValue("email")
password := r.PostFormValue("password")
if err := auth.Attempt(email, password); err == nil {
if auth.User().Activated != 1 {
auth.Logout()
view.RenderSimple(w, view.D{
"Error": "你的账号未激活,请检查邮箱中的注册邮件进行激活。",
"Email": email,
"Password": password,
}, "auth.login")
} else {
// 登录成功
http.Redirect(w, r, "/", http.StatusFound)
}
} else {
view.RenderSimple(w, view.D{
"Error": err.Error(),
"Email": email,
"Password": password,
}, "auth.login")
}
}
发送邮件
我们还是利用 GORM 模型钩子来进行代码解耦,在用户模型创建之后发送确认邮件
app/models/user/hooks.go
import (
.
.
.
"goblog/pkg/mail"
)
.
.
.
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
mail.SendWithTemplate("auth.confirm_email", u, u.Email, "感谢注册 Goblog 应用!请确认你的邮箱。")
return
}
激活功能
现在的邮箱发送功能已经能够正常使用,接下来让我们完成前面定义的 confirm_email
路由对应的控制器方法 ConfirmEmail
,来完成用户的激活操作。
app/http/controllers/auth_controller.go
func (*AuthController) ConfirmEmail(w http.ResponseWriter, r *http.Request) {
token := route.GetRouteVariable("token", r)
user, err := user.GetByActivationToken(token)
if err != nil {
if err == gorm.ErrRecordNotFound {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "404 未找到相关用户")
} else {
logger.LogError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
}
}
user.Activated = 1
user.ActivationToken = ""
now := time.Now()
user.EmailVerifiedAt = &now
rowsAffected, err := user.Update()
if err != nil {
// 数据库错误
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 服务器内部错误")
return
}
if rowsAffected > 0 {
auth.Login(user)
http.Redirect(w, r, "/", http.StatusFound)
} else {
fmt.Fprint(w, "What happened?")
}
}
我们还需要在 user 模型中增加 GetByActivationToken
方法
app/models/user/crud.go
func GetByActivationToken(activationToken string) (User, error) {
var user User
if err := model.DB.Where("activation_token = ?", activationToken).First(&user).Error; err != nil {
return user, err
}
return user, nil
}
至此我们的邮箱验证功能已经完成啦~
Git 代码版本控制
接着让我们将本次更改纳入版本控制中:
$ git add -A
$ git commit -m "邮箱验证"
推荐文章: