好吧,自从我上次写一篇文章以来已经有一段时间了,所以你知道我做了什么来获得动力,我修改了我的博客,或者我应该说我只是添加了一个黑暗的主题。

本博客是第一部分的延续,我们在其中创建了一个由 Mongo 数据库驱动的工作 Golang API。在开始阅读本文之前,请确保您阅读它,如果没有,这里是

TL;DR

  • 我们只处理基本用户身份验证

在这一部分中,我们将重点介绍:

  • 通过电子邮件验证用户帐户

  • 密码重置并通过电子邮件请求

  • 刷新令牌端点

我计划有 3 个其他部分,这些部分将重点关注以下重点内容:

  • 第 3 部分

  • 创建书签端点 > 我想构建一个 TODO API,但我认为这有点陈词滥调,所以我决定开发一个书签 API,它将允许用户保存指向他们喜欢的网站的链接,甚至通过预加载它们并保存元数据来保存元数据标签详情。

*能够创建书签

  • 预加载链接元详细信息并将其保存到集合中

*能够获取所有书签

*能够删除书签

  • 第 4 部分

  • 测试认证端点

  • 添加会话黑名单(阻止用户重复使用)

  • 将项目托管到heroku

  • 第 5 部分

  • 概述如何改进此 API

  • 您可以使用此 API 构建多少产品

简介

先决条件

你必须知道 Golang 的基础知识和一点点 Gin

入门

我们走吧

进球

  • 用户应该能够验证他们的帐户

  • 用户应该能够重置其帐户密码

  • 用户应该能够刷新令牌

设置

克隆项目

# SSH
$ git clone git@github.com:werickblog/golang_todo_api.git

# HTTP
$ git clone https://github.com/werickblog/golang_todo_api.git

进入全屏模式 退出全屏模式

确保项目位于您设置的$GOPATH/src目录中

接下来,使用您喜欢的编辑器打开项目并运行应用程序

$ go run app.go

进入全屏模式 退出全屏模式

这将自动安装所有缺少的软件包。

让我们破解

让破解

密码重置和请求

我们将从密码请求和重置控制器开始。

所以打开controllers/user.go并创建UserController结构体的ResetLink方法。


// ...
// ResetLink handles resending email to user to reset link
func (u *UserController) ResetLink(c *gin.Context) {
    // Defined schema for the request body
    var data forms.ResendCommand

    // Ensure the user provides all values from the request.body
    if (c.BindJSON(&data)) != nil {
        // Return 400 status if they don't provide the email
        c.JSON(400, gin.H{"message": "Provided all fields"})
        c.Abort()
        return
    }

    // Fetch the account from the database based on the email
    // provided
    result, err := userModel.GetUserByEmail(data.Email)

    // Return 404 status if an account was not found
    if result.Email == "" {
        c.JSON(404, gin.H{"message": "User account was not found"})
        c.Abort()
        return
    }

    // Return 500 status if something went wrong while fetching
    // account
    if err != nil {
        c.JSON(500, gin.H{"message": "Something wrong happened, try again later"})
        c.Abort()
        return
    }

    // Generate the token that will be used to reset the password
    resetToken, _ := services.GenerateNonAuthToken(result.Email)

    // The link to be clicked in order to perform a password reset
    link := "http://localhost:5000/api/v1/password-reset?reset_token=" + resetToken
    // Define the body of the email
    body := "Here is your reset <a href='" + link + "'>link</a>"
    html := "<strong>" + body + "</strong>"

    // Initialize email sendout
    email := services.SendMail("Reset Password", body, result.Email, html, result.Name)

    // If email was sent, return 200 status code
    if email == true {
        c.JSON(200, gin.H{"messsage": "Check mail"})
        c.Abort()
        return
    // Return 500 status when something wrong happened
    } else {
        c.JSON(500, gin.H{"message": "An issue occured sending you an email"})
        c.Abort()
        return
    }
}
// ...

进入全屏模式 退出全屏模式

以上是密码重置链接请求,如果你运行你的应用程序,它会失败,因为我们没有定义某些方法/变量/结构。这些是:

  • forms中定义的请求正文架构

  • 为密码请求生成令牌

  • 发送电子邮件

因此,让我们先来定义请求正文模式

密码重置请求架构

打开forms/user.go文件并添加以下行

// ..
// ResendCommand defines resend email payload
type ResendCommand struct {
    // We only need the email to initialize an email sendout
    Email string `json:"email" binding:"required"`
}
// ...

进入全屏模式 退出全屏模式

到下一个

代币生成

由于安全原因,我们不希望我们的用户使用他们从登录中获得的令牌来初始化重置密码,因此我们将不得不创建一种新方法来创建非身份验证令牌并对其进行解码。让我们跳进去

跳

打开services/jwt.go并添加以下方法

// ...

// Define its own secret key
var anotherJwtKey = []byte(os.Getenv("ANOTHER_SECRET_KEY"))

// GenerateNonAuthToken handles generation of a jwt code
// @returns string -> token and error -> err
func GenerateNonAuthToken(userID string) (string, error) {
    // Define token expiration time
    expirationTime := time.Now().Add(1440 * time.Minute)
    // Define the payload and exp time
    claims := &Claims{
        UserID: userID,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: expirationTime.Unix(),
        },
    }

    // Generate token
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // Sign token with secret key encoding
    tokenString, err := token.SignedString(anotherJwtKey)

    return tokenString, err
}

// DecodeNonAuthToken handles decoding a jwt token
func DecodeNonAuthToken(tkStr string) (string, error) {
    claims := &Claims{}

    // Decode token based on parameters provided, if it fails throw err
    tkn, err := jwt.ParseWithClaims(tkStr, claims, func(token *jwt.Token) (interface{}, error) {
        return anotherJwtKey, nil
    })

    if err != nil {
        if err == jwt.ErrSignatureInvalid {
            return "", err
        }
        return "", err
    }

    if !tkn.Valid {
        return "", err
    }

    // Return encoded email
    return claims.UserID, nil
}
// ...

进入全屏模式 退出全屏模式

处理电子邮件发送

我选择了 Sendgrid 电子邮件服务,因为创建帐户更容易(😂 不需要信用卡)而且设置您自己的自定义电子邮件域是可选的(意味着您可以使用您的 Gmail 帐户)。

谢谢 Sendgrid

创建一个Sendgrid帐户并生成一个 API 密钥,该密钥具有发送电子邮件的权限。

接下来我们将安装一个 Sendgrid 的 Go SDK,它将简化发送电子邮件的过程

$ go get github.com/sendgrid/sendgrid-go

进入全屏模式 退出全屏模式

创建一个新文件来保存我们发送电子邮件的方法。我们还将使其可重用,(DRY 代码,)

添加以下代码行

// Define the package
package services

// Import relevant dependecy
import (
    "fmt"
    "os"

    // Import Sendgrid Go library
    "github.com/sendgrid/sendgrid-go"
    "github.com/sendgrid/sendgrid-go/helpers/mail"
)

// EmailObject defines email payload data
type EmailObject struct {
    To      string
    Body    string
    Subject string
}


// SendMail method to send email to user
func SendMail(subject string, body string, to string, html string, name string) bool {
    fmt.Println(os.Getenv("SENDGRID_API_KEY"))

    // The first parameter is how your email name will be
    from := mail.NewEmail("Just Open it", os.Getenv("SENDGRID_FROM_MAIL"))
    // The recipient
    _to := mail.NewEmail(name, to)
    // Body in plain text
    plainTextContent := body
    // Body in html form(You can style a html document convert to string and make it look like the morning brew newsletter)
    htmlContent := html
    // Create message
    message := mail.NewSingleEmail(from, subject, _to, plainTextContent, htmlContent)
    // initialize client
    client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))
    _, err := client.Send(message)
    if err != nil {
        return false
    } else {
        return true
    }
}

进入全屏模式 退出全屏模式

密码重置请求

转到controllers/user.go,让我们添加密码重置请求。

定义密码重置控制器方法

// ResetLink handles resending email to user to reset link
func (u *UserController) ResetLink(c *gin.Context) {
    var data forms.ResendCommand

    // Ensure they provide all request body values
    if (c.BindJSON(&data)) != nil {
        c.JSON(400, gin.H{"message": "Provided all fields"})
        c.Abort()
        return
    }

    // Fetch the user in the database
    result, err := userModel.GetUserByEmail(data.Email)

    // If the user doesn't exist return 404 status code
    if result.Email == "" {
        c.JSON(404, gin.H{"message": "User account was not found"})
        c.Abort()
        return
    }

    // Something went wrong while fetching
    if err != nil {
        c.JSON(500, gin.H{"message": "Something wrong happened, try again later"})
        c.Abort()
        return
    }

    // Generate reset token to be used
    resetToken, _ := services.GenerateNonAuthToken(result.Email)

    // Define the email body
    link := "http://localhost:5000/api/v1/password-reset?reset_token=" + resetToken
    body := "Here is your reset <a href='" + link + "'>link</a>"
    html := "<strong>" + body + "</strong>"

    // Send the email
    email := services.SendMail("Reset Password", body, result.Email, html, result.Name)

    // If email is sent then return 200 HTTP status code
    if email == true {
        c.JSON(200, gin.H{"messsage": "Check mail"})
        c.Abort()
        return
    } else {
        // Else tell them something went down
        c.JSON(500, gin.H{"message": "An issue occured sending you an email"})
        c.Abort()
        return
    }
}

进入全屏模式 退出全屏模式

接下来我们需要定义将初始化请求的端点。

前往app.go并添加此行

// Send reset link
v1.PUT("/reset-link", user.ResetLink)

进入全屏模式 退出全屏模式

密码重置更改

接下来,我们必须添加新控制器以根据从 url 解码令牌获得的用户来处理密码更改。

让我们跳进去

前往controllers/user.go并添加控制器。

// PasswordReset handles user password request
func (u *UserController) PasswordReset(c *gin.Context) {
    var data forms.PasswordResetCommand

    // Ensure they provide data based on the schema
    if c.BindJSON(&data) != nil {
        c.JSON(406, gin.H{"message": "Provide relevant fields"})
        c.Abort()
        return
    }

    // Ensures that the password provided matches the confirm
    if data.Password != data.Confirm {
        c.JSON(400, gin.H{"message": "Passwords do not match"})
        c.Abort()
        return
    }

    // Get token from link query sent to your email
    resetToken, _ := c.GetQuery("reset_token")

    // Decode the token
    userID, _ := services.DecodeNonAuthToken(resetToken)

    // Fetch the user
    result, err := userModel.GetUserByEmail(userID)

    if err != nil {
        // Return response when we get an error while fetching user
        c.JSON(500, gin.H{"message": "Something wrong happened, try again later"})
        c.Abort()
        return
    }
    // Check if account exists
    if result.Email == "" {
        c.JSON(404, gin.H{"message": "User accoun was not found"})
        c.Abort()
        return
    }

    // Hash the new password
    newHashedPassword := helpers.GeneratePasswordHash([]byte(data.Password))

    // Update user account
    _err := userModel.UpdateUserPass(userID, newHashedPassword)

    if _err != nil {
        // Return response if we are not able to update user password
        c.JSON(500, gin.H{"message": "Somehting happened while updating your password try again"})
        c.Abort()
        return
    }

    c.JSON(201, gin.H{"message": "Password has been updated, log in"})
    c.Abort()
    return
}

进入全屏模式 退出全屏模式

接下来让我们添加一个新端点来初始化上面的控制器

转到app.go文件和以下行

// Password reset
v1.PUT("/password-reset", user.PasswordReset)

进入全屏模式 退出全屏模式

我们已成功处理用户帐户的密码重置,接下来我们将研究验证帐户。

账户验证

帐户验证允许开发人员验证特定帐户的用户,从而减少不存在电子邮件的虚拟创建。

我们将更新Signup控制器来处理发送验证电子邮件,并添加一个新控制器来处理重新发送电子邮件,最后是一个控制器来验证用户帐户。

转到controllers/user.go,让我们编辑Signup控制器。

// ...

// Generate token to hold users details
resetToken, _ := services.GenerateNonAuthToken(data.Email)

// link to be verify account
link := "http://localhost:5000/api/v1/verify-account?verify_token=" + resetToken
// Define email body
body := "Here is your reset <a href='" + link + "'>link</a>"
html := "<strong>" + body + "</strong>"

// initialize email send out
email := services.SendMail("Verify Account", body, data.Email, html, data.Name)

// If email fails while sending
if !email {
    c.JSON(500, gin.H{"message": "An issue occured sending you an email"})
    c.Abort()
    return
}
// ...

进入全屏模式 退出全屏模式

接下来让我们创建一个重新发送验证电子邮件控制器。还是在同一个文件上,加这个方法

// VerifyLink handles resending email to user to reset link
func (u *UserController) VerifyLink(c *gin.Context) {
    var data forms.ResendCommand

    // Ensure they provide all relevant fields in the request body
    if (c.BindJSON(&data)) != nil {
        c.JSON(400, gin.H{"message": "Provided all fields"})
        c.Abort()
        return
    }

    // Fetch account from database
    result, err := userModel.GetUserByEmail(data.Email)

    // Check if account exist return 404 if not
    if result.Email == "" {
        c.JSON(404, gin.H{"message": "User account was not found"})
        c.Abort()
        return
    }

    if err != nil {
        c.JSON(500, gin.H{"message": "Something wrong happened, try again later"})
        c.Abort()
        return
    }

    // Generate token to hold user details
    resetToken, _ := services.GenerateNonAuthToken(result.Email)

    // Define email body
    link := "http://localhost:5000/api/v1/verify-account?verify_token=" + resetToken
    body := "Here is your reset <a href='" + link + "'>link</a>"
    html := "<strong>" + body + "</strong>"

    // Initialize email sendout
    email := services.SendMail("Verify Account", body, result.Email, html, result.Name)

    // If email send 200 status code
    if email == true {
        c.JSON(200, gin.H{"messsage": "Check mail"})
        c.Abort()
        return
    } else {
        c.JSON(500, gin.H{"message": "An issue occured sending you an email"})
        c.Abort()
        return
    }
}

进入全屏模式 退出全屏模式

让我们添加一个端点来初始化上述控制器,转到app.go并添加以下行。

// Send verify link
v1.PUT("/verify-link", user.VerifyLink)

进入全屏模式 退出全屏模式

我们现在必须处理帐户控制器的验证,让我们寄希望于这一点。前往controllers/user.go并添加验证帐户控制器。

// VerifyAccount handles user password request
func (u *UserController) VerifyAccount(c *gin.Context) {
    // Get token from link query
    verifyToken, _ := c.GetQuery("verify_token")

    // Decode verify token
    userID, _ := services.DecodeNonAuthToken(verifyToken)

    // Fetch user based on details from decoded token
    result, err := userModel.GetUserByEmail(userID)

    if err != nil {
        // Return response when we get an error while fetching user
        c.JSON(500, gin.H{"message": "Something wrong happened, try again later"})
        c.Abort()
        return
    }

    if result.Email == "" {
        c.JSON(404, gin.H{"message": "User account was not found"})
        c.Abort()
        return
    }

    // Update user account
    _err := userModel.VerifyAccount(userID)

    if _err != nil {
        // Return response if we are not able to update user password
        c.JSON(500, gin.H{"message": "Something happened while verifying you account, try again"})
        c.Abort()
        return
    }

    c.JSON(201, gin.H{"message": "Account verified, log in"})
}

进入全屏模式 退出全屏模式

让我们添加一个端点来验证一个帐户

// Verify account
v1.PUT("/verify-account", user.VerifyAccount)

进入全屏模式 退出全屏模式

我们快完成了

我们留下了刷新令牌。如果访问令牌恰好过期,刷新令牌基本上是用于刷新用户会话的令牌。在此处阅读更多信息

前往controllers/user.go并添加我们的刷新令牌控制器

// RefreshToken handles refresh token
func (u *UserController) RefreshToken(c *gin.Context) {
    // Get refresh token from header
    refreshToken := c.Request.Header["Refreshtoken"]

    // Check if refresh token was provided
    if refreshToken == nil {
        c.JSON(403, gin.H{"message": "No refresh token provided"})
        c.Abort()
        return
    }

    // Decode token to get data
    email, err := services.DecodeRefreshToken(refreshToken[0])

    if err != nil {
        c.JSON(500, gin.H{"message": "Problem refreshing your session"})
        c.Abort()
        return
    }

    // Create new token
    accessToken, _refreshToken, _err := services.GenerateToken(email)

    if _err != nil {
        c.JSON(500, gin.H{"message": "Problem creating new session"})
        c.Abort()
        return
    }

    c.JSON(200, gin.H{"message": "Log in success", "token": accessToken, "refresh_token": _refreshToken})
}

进入全屏模式 退出全屏模式

现在让我们在app.go中添加一个端点

// Refresh token
v1.GET("/refresh", user.RefreshToken)

进入全屏模式 退出全屏模式

就是这样,

总结

  • 我们处理了密码重置请求和更改

  • 我们处理了帐户验证

  • 我们处理刷新令牌

其他

  • 回购链接这里

  • 在推特上关注我在这里

  • 加入 Discord 服务器以解决任何问题此处

Logo

MongoDB社区为您提供最前沿的新闻资讯和知识内容

更多推荐