在 Golang 中使用 Gin 创建 API 第 2 部分
好吧,自从我上次写一篇文章以来已经有一段时间了,所以你知道我做了什么来获得动力,我修改了我的博客,或者我应该说我只是添加了一个黑暗的主题。
本博客是第一部分的延续,我们在其中创建了一个由 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 服务器以解决任何问题此处

更多推荐
所有评论(0)