日南葵
发布于

Go 实战:Web 入门 - 邮箱验证

账户激活

现在的登录逻辑是,用户一旦注册成功即可进行登录,本帖中我们要加入账号激活功能,只有当用户成功激活自己的账号时才能在网站上进行登录。为此,我们将需要为用户表新增三个字段用于保存用户的激活令牌、激活状态、激活时间。

整个激活流程如下:

  1. 用户注册成功后,自动生成激活令牌;
  2. 将激活令牌以链接的形式附带在注册邮件里面,并将邮件发送到用户的注册邮箱上;
  3. 用户点击注册链接跳到指定路由,路由收到激活令牌参数后映射给相关控制器动作处理;
  4. 控制器拿到激活令牌并进行验证,验证通过后对该用户进行激活,并将其激活状态设置为已激活;
  5. 用户激活成功,自动登录;

资源

添加字段

在用户的账号激活功能中,我们需要增加激活令牌 (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 的自动迁移,所以我们编辑之后将会自动生成对应字段

G01 Go 实战:Web 入门 - 邮箱验证

生成令牌

我们在前面说过,用户的激活令牌需要在用户创建(注册)之前就先生成好,这样当用户注册成功之后我们才可以将令牌附带到注册链接上,并通过邮件的形式发送给用户。

如果我们需要在模型被创建之前进行一些设置,则可以通过 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 包?

  1. 首先我们将 GoMail 封装到新建的 mail 包中,并统一在这个包里做好错误处理;
  2. 注册成功时,调用我们封装好的 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 "邮箱验证"

原文作者 lidongyoo,如有侵权,请联系删除。

评论