• 10-Email Support
    • 第三方库支持
    • 加入mail支持
    • 请求重置密码
      • goroutine
      • jwt
    • 重置密码
    • Links

    10-Email Support

    这是Go-Mega系列的第十部分,本章我将告诉你,应用如何向你的用户发送电子邮件,以及如何在电子邮件支持之上构建密码重置功能。

    现在,应用在数据库方面做得相当不错,所以在本章中,我想抛开这个主题,开始添加发送电子邮件的功能,这是大多数Web应用必需的另一个重要部分。

    为什么应用需要发送电子邮件给用户? 原因很多,但其中一个常见的原因是解决与认证相关的问题。 在本章中,我将为忘记密码的用户添加密码重置功能。 当用户请求重置密码时,应用将发送包含特制链接的电子邮件。 用户然后需要点击该链接才能访问设置新密码的表单。

    本章的GitHub链接为: Source, Diff, Zip

    第三方库支持

    本章我们需要两个第三方插件 gomail 以及 jwt-go

    1. # gomail
    2. $ go get gopkg.in/gomail.v2
    3. # jwt-go
    4. $ go get github.com/dgrijalva/jwt-go

    加入mail支持

    在 config 中增加 mail 设置

    config.yml

    1. mysql:
    2. charset: utf8
    3. db: dbname
    4. host: localhost
    5. password: password
    6. user: root
    7. mail:
    8. smtp: smtp-server
    9. smtp-port: 587
    10. user: user
    11. password: pwd

    这里的 smtp server, 请查看你的邮件提供商的文档,比如 zoho mail 的 smtp 是 smtp.zoho.com

    config/g.go

    1. ...
    2. // GetSMTPConfig func
    3. func GetSMTPConfig() (server string, port int, user, pwd string) {
    4. server = viper.GetString("mail.smtp")
    5. port = viper.GetInt("mail.smtp-port")
    6. user = viper.GetString("mail.user")
    7. pwd = viper.GetString("mail.password")
    8. return
    9. }

    在 controller/utils.go 封装 sendMail 函数,方便调用

    controller/utils.go

    1. ...
    2. // Email
    3. // sendEmail func
    4. func sendEmail(target, subject, content string) {
    5. server, port, usr, pwd := config.GetSMTPConfig()
    6. d := gomail.NewDialer(server, port, usr, pwd)
    7. d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
    8. m := gomail.NewMessage()
    9. m.SetHeader("From", usr)
    10. m.SetHeader("To", target)
    11. m.SetAddressHeader("Cc", usr, "admin")
    12. m.SetHeader("Subject", subject)
    13. m.SetBody("text/html", content)
    14. if err := d.DialAndSend(m); err != nil {
    15. log.Println("Email Error:", err)
    16. return
    17. }
    18. }

    本小节 Diff

    请求重置密码

    我上面提到过,用户有权利重置密码。因此我将在登录页面提供一个链接:

    templates/content/login.html

    1. ...
    2. <p>New User? <a href="/register">Click to Register!</a></p>
    3. <p>Forget Password? <a href="/reset_password_request">Click to Reset Password!</a></p>
    4. ...

    10-01

    当用户点击链接时,会出现一个新的Web表单,要求用户输入注册的电子邮件地址,以启动密码重置过程。

    vm/reset_password_request.go

    1. package vm
    2. import (
    3. "log"
    4. "github.com/bonfy/go-mega-code/model"
    5. )
    6. // ResetPasswordRequestViewModel struct
    7. type ResetPasswordRequestViewModel struct {
    8. LoginViewModel
    9. }
    10. // ResetPasswordRequestViewModelOp struct
    11. type ResetPasswordRequestViewModelOp struct{}
    12. // GetVM func
    13. func (ResetPasswordRequestViewModelOp) GetVM() ResetPasswordRequestViewModel {
    14. v := ResetPasswordRequestViewModel{}
    15. v.SetTitle("Forget Password")
    16. return v
    17. }
    18. // CheckEmailExist func
    19. func CheckEmailExist(email string) bool {
    20. _, err := model.GetUserByEmail(email)
    21. if err != nil {
    22. log.Println("Can not find email:", email)
    23. return false
    24. }
    25. return true
    26. }

    templates/content/reset_password_request.html

    1. {{define "content"}}
    2. <h1>Input your email address:</h1>
    3. <form action="/reset_password_request" method="post" name="reset_password_request">
    4. <p><input type="text" class="form-control" name="email" value="" placeholder="Email"></p>
    5. <p><input type="submit" class="btn btn-outline-primary" name="submit" value="Submit" ></p>
    6. </form>
    7. {{if .Errs}}
    8. <ul>
    9. {{range .Errs}}
    10. <li>{{.}}</li>
    11. {{end}}
    12. </ul>
    13. {{end}}
    14. {{end}}

    controller/home.go

    1. ...
    2. r.HandleFunc("/reset_password_request", resetPasswordRequestHandler)
    3. ...
    4. func resetPasswordRequestHandler(w http.ResponseWriter, r *http.Request) {
    5. tpName := "reset_password_request.html"
    6. vop := vm.ResetPasswordRequestViewModelOp{}
    7. v := vop.GetVM()
    8. if r.Method == http.MethodGet {
    9. templates[tpName].Execute(w, &v)
    10. }
    11. if r.Method == http.MethodPost {
    12. r.ParseForm()
    13. email := r.Form.Get("email")
    14. errs := checkResetPasswordRequest(email)
    15. v.AddError(errs...)
    16. if len(v.Errs) > 0 {
    17. templates[tpName].Execute(w, &v)
    18. } else {
    19. log.Println("Send mail to", email)
    20. vopEmail := vm.EmailViewModelOp{}
    21. vEmail := vopEmail.GetVM(email)
    22. var contentByte bytes.Buffer
    23. tpl, _ := template.ParseFiles("templates/email.html")
    24. if err := tpl.Execute(&contentByte, &vEmail); err != nil {
    25. log.Println("Get Parse Template:", err)
    26. w.Write([]byte("Error send email"))
    27. return
    28. }
    29. content := contentByte.String()
    30. go sendEmail(email, "Reset Password", content)
    31. http.Redirect(w, r, "/login", http.StatusSeeOther)
    32. }
    33. }
    34. }

    10-02

    controller 里涉及到了 email 的template

    vm/email.go

    1. package vm
    2. import (
    3. "github.com/bonfy/go-mega-code/config"
    4. "github.com/bonfy/go-mega-code/model"
    5. )
    6. // EmailViewModel struct
    7. type EmailViewModel struct {
    8. Username string
    9. Token string
    10. Server string
    11. }
    12. // EmailViewModelOp struct
    13. type EmailViewModelOp struct{}
    14. // GetVM func
    15. func (EmailViewModelOp) GetVM(email string) EmailViewModel {
    16. v := EmailViewModel{}
    17. u, _ := model.GetUserByEmail(email)
    18. v.Username = u.Username
    19. v.Token, _ = u.GenerateToken()
    20. v.Server = config.GetServerURL()
    21. return v
    22. }

    templates/email.html

    1. <p>Dear {{.Username}},</p>
    2. <p>
    3. To reset your password
    4. <a href="{{.Server}}/reset_password/{{.Token}}">
    5. click here
    6. </a>.
    7. </p>
    8. <p>Alternatively, you can paste the following link in your browser's address bar:</p>
    9. <p>{{.Server}}/reset_password/{{.Token}}</p>
    10. <p>If you have not requested a password reset simply ignore this message.</p>
    11. <p>This Email will expire in 2 hours.</p>
    12. <p>Sincerely,</p>
    13. <p>BONFY</p>

    10-03

    上面简单的一封邮件,其实蕴含着两个 非常重要的 知识点:

    goroutine

    原来 Flask-Mega 里面非常复杂的多线程操作,这里只用了一个 go function() 完成了

    goroutine 可以说是 Go 这个语言的特色之一了,当然资料也比较多,大家可以深入了解下

    这里我简单说下,就是 function 前面加个 go 关键字,就实现了 协程,用于高并发,而且性能非常好,是不是很cool!

    jwt

    具体可以通过 jwt.io了解下

    或者中文的可以通过这片文章具体了解下 直通车

    这里的邮件可以说是 JWT 的一个非常典型的应用场景,jwt 加密后的 URL 就是图中的一长串,其中其实隐含着 2 hour 过期时间

    我们通过在 user.go 里加入两个function 就能实现, 密钥 secret 这里直接写在代码里,其实更优还是通过配置文件配置,又偷懒了

    model/user.go

    1. ...
    2. // GenerateToken func
    3. func (u *User) GenerateToken() (string, error) {
    4. token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    5. "username": u.Username,
    6. "exp": time.Now().Add(time.Hour * 2).Unix(), // 可以添加过期时间
    7. })
    8. return token.SignedString([]byte("secret"))
    9. }
    10. // CheckToken func
    11. func CheckToken(tokenString string) (string, error) {
    12. token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    13. // Don't forget to validate the alg is what you expect:
    14. if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
    15. return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
    16. }
    17. // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
    18. return []byte("secret"), nil
    19. })
    20. if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
    21. return claims["username"].(string), nil
    22. } else {
    23. return "", err
    24. }
    25. }
    26. ...

    本小节 Diff

    重置密码

    目前我们点击邮件中的邮件是不能操作的,因为我们其实并没有 /reset_password 这个handler,那这里我们来完成它

    其实密码重置就是一个简单的表单, 两个密码框,检验输入一致以及符合密码规范就可以了,操作起来不算太复杂。

    vm/reset_password.go

    1. package vm
    2. import (
    3. "github.com/bonfy/go-mega-code/model"
    4. )
    5. // ResetPasswordViewModel struct
    6. type ResetPasswordViewModel struct {
    7. LoginViewModel
    8. Token string
    9. }
    10. // ResetPasswordViewModelOp struct
    11. type ResetPasswordViewModelOp struct{}
    12. // GetVM func
    13. func (ResetPasswordViewModelOp) GetVM(token string) ResetPasswordViewModel {
    14. v := ResetPasswordViewModel{}
    15. v.SetTitle("Reset Password")
    16. v.Token = token
    17. return v
    18. }
    19. // CheckToken func
    20. func CheckToken(tokenString string) (string, error) {
    21. return model.CheckToken(tokenString)
    22. }
    23. // ResetUserPassword func
    24. func ResetUserPassword(username, password string) error {
    25. return model.UpdatePassword(username, password)
    26. }

    templates/content/reset_password.html

    1. {{define "content"}}
    2. <h1>Register</h1>
    3. <form action="/reset_password/{{.Token}}" method="post" name="reset_password">
    4. <p><input type="password" name="pwd1" value="" placeholder="Password"></p>
    5. <p><input type="password" name="pwd2" value="" placeholder="Confirm Password"></p>
    6. <p><input type="submit" name="submit" value="Reset"></p>
    7. </form>
    8. {{if .Errs}}
    9. <ul>
    10. {{range .Errs}}
    11. <li>{{.}}</li>
    12. {{end}}
    13. </ul>
    14. {{end}}
    15. {{end}}

    controller/home.go

    1. ...
    2. r.HandleFunc("/reset_password/{token}", resetPasswordHandler)
    3. ...
    4. func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
    5. vars := mux.Vars(r)
    6. token := vars["token"]
    7. username, err := vm.CheckToken(token)
    8. if err != nil {
    9. w.Write([]byte("The token is no longer valid, please go to the login page."))
    10. }
    11. tpName := "reset_password.html"
    12. vop := vm.ResetPasswordViewModelOp{}
    13. v := vop.GetVM(token)
    14. if r.Method == http.MethodGet {
    15. templates[tpName].Execute(w, &v)
    16. }
    17. if r.Method == http.MethodPost {
    18. log.Println("Reset password for ", username)
    19. r.ParseForm()
    20. pwd1 := r.Form.Get("pwd1")
    21. pwd2 := r.Form.Get("pwd2")
    22. errs := checkResetPassword(pwd1, pwd2)
    23. v.AddError(errs...)
    24. if len(v.Errs) > 0 {
    25. templates[tpName].Execute(w, &v)
    26. } else {
    27. if err := vm.ResetUserPassword(username, pwd1); err != nil {
    28. log.Println("reset User password error:", err)
    29. w.Write([]byte("Error update user password in database"))
    30. return
    31. }
    32. http.Redirect(w, r, "/login", http.StatusSeeOther)
    33. }
    34. }
    35. }

    10-04

    就这样我们完成了重置密码的功能,虽然设计到的页面比较多,不过只要我们思路清晰,一步一个脚印,还是能非常容易就能实现了。

    现在输入新的密码,保存后,就可以用新密码登陆了。

    本小节 Diff

    Notice: 本章还涉及到一些后端验证,在这里没有一一列举,大家还是请看下源码diff,可以更完整的了解代码

    • 目录
    • 上一节: 09-Pagination
    • 下一节: 11-Facelift