优秀的编程知识分享平台

网站首页 > 技术文章 正文

7.Go语言编写个人博客 用户登录(go语言实战项目)

nanyue 2024-09-09 04:58:46 技术文章 9 ℃

上一节,我们简单介绍了JWT。本节让我们使用它完成用户登录的代码编写。

还是先从models/user.go文件开始编写,写入一个结构体JwtClaims:

// JwtClaims用于生成Jwt token
type JwtClaims struct {
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
	jwt.StandardClaims
}

以上代码定义了一个名为 JwtClaims 的结构体,该结构体通常用于在 Go 应用程序中生成 JSON Web 令牌 (JWT)

  • User、Username 这两个字段就不再赘述了。
  • jwt.StandardClaims: 这行代码嵌入了来自 jwt 包(github.com/dgrijalva/jwt-go)的 StandardClaims 结构体。嵌入允许您在自己的结构体内重用另一个结构体的字段和方法。

总的来说 JwtClaims 结构体旨在保存生成 JWT 相关的信息。它包括用户身份验证详细信息(UserID 和 Username),也包括来自 jwt 包定义的标准声明。

再写入一个验证密码的函数CheckPassword:

// 验证密码
func CheckPassword(hashedPassword, plainPassword string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
	return err == nil
}

CheckPassword 的函数接收两个字符串参数:

  • hashedPassword: 存储的哈希密码 (来自数据库)
  • plainPassword: 用户输入的密码 (未加密)

函数返回一个布尔值 (bool),表示密码匹配 (true) 或不匹配 (false)。

err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))这行代码使用 bcrypt 包的 CompareHashAndPassword 函数进行密码比较。 它将两个切片 ([]byte) 作为参数:

  • 第一个切片 ([]byte(hashedPassword)) 将存储的哈希密码转换为字节切片。
  • 第二个切片 ([]byte(plainPassword)) 将用户输入的密码转换为字节切片。

CompareHashAndPassword 函数比较两个密码的哈希值之后将其结果存储在 err 变量中。

return err == nil 通过比较 err 是否为 nil 来判断密码是否匹配,并返回相应的布尔值 (true 或 false)。

当有了存储JWT的结构体和验证密码的函数后,我们就可以去写生成用户令牌和用户登录的的代码了。

在utils目录中新建文件jwt.go,并写入如下内容:

package utils

import (
	"time"
	"xblog/models"

	"github.com/dgrijalva/jwt-go"
)

var jwtSecret = []byte("my_secret_key")

// GenerateToken生成用户令牌
func GenerateToken(userID uint, username string) (string, error) {
	now := time.Now()
	claims := models.JwtClaims{
		UserID:   userID,
		Username: username,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: now.Add(time.Hour * 24 * 7).Unix(), // 一周有效期
			Issuer:    "xblog",
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

此文件目前只包含了一个函数(GenerateToken),也是我要重点介绍的。

  1. var jwtSecret = []byte("my_secret_key") 定义了一个名为 jwtSecret 的全局变量,用于存储 JWT 签名使用的密钥。注意,这是一个示例密钥,请勿将其用于生产环境中。
  2. func GenerateToken(userID uint, username string) (string, error) 定义了一个名为 GenerateToken 的函数。函数接收两个参数:userID: 用户 ID (类型为 uint) username: 用户名 (类型为 string)。该函数返回两个值:生成的 JWT 令牌 (类型为 string) 发生错误时返回的错误对象 (类型为 error)
  3. now := time.Now(): 获取当前时间。
  4. claims := models.JwtClaims{ ... } 创建一个 models.JwtClaims 结构体的实例 (models.JwtClaims 已经在 xblog/models 包中完成)。设置结构体字段的值: UserID: 使用传入的 userID 参数。Username: 使用传入的 username 参数。StandardClaims: 嵌入 jwt.StandardClaims 结构体,并设置以下字段: ExpiresAt: 令牌的过期时间为当前时间加上 7 天 (使用 time.Hour * 24 * 7)。 Issuer: 令牌的颁发者为 "xblog"。
  5. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims): 使用 jwt-go 库创建新的 JWT 令牌。第一个参数指定签名方法为 HMAC SHA-256 ( jwt.SigningMethodHS256 )。第二个参数是之前创建的 claims 结构体。
  6. return token.SignedString(jwtSecret): 使用预先定义的 jwtSecret 密钥对令牌进行签名,并返回签名的字符串

总的来说 GenerateToken 函数用于生成一个包含用户 ID、用户名、过期时间和颁发者信息的 JWT 令牌。这个令牌可以用来验证用户身份并授予访问权限。

在utils目录中新建文件auth.go,也许只看文件名你就能想到它的用途。写入如下内容:

package utils

import (
	"errors"
	"net/http"
	"strings"
	"xblog/models"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

// Login 用户登陆
func Login(username, password string) (user *models.User, err error) {
	user = &models.User{}

	// 直接查询用户。如果存在,则加载详细信息;如果不存在,则返回错误信息
	result := models.DB.Where("username = ?", username).First(user)
	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		return nil, errors.New("用户名或密码错误") // 返回通用错误信息
	} else if result.Error != nil {
		return nil, result.Error // 返回其它数据库查询错误
	}

	// 验证密码
	if !models.CheckPassword(user.Password, password) {
		return nil, errors.New("用户名或密码错误") // 返回通用错误信息
	}

	return user, nil
}

// RequireAuth是验证Jwt的中间件
func RequireAuth(c *gin.Context) {
	authToken := c.GetHeader("Authorization")
	if authToken == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
		c.Abort()
		return
	}

	parts := strings.Fields(authToken)
	if len(parts) < 2 || parts[0] != "Bearer" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌格式错误"})
		c.Abort()
		return
	}

	tokenStr := parts[1]
	claims := &models.JwtClaims{}

	token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})

	if err != nil || !token.Valid {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "认证令牌无效"})
		c.Abort()
		return
	}

	// 验证通过后,将用户信息设置到Context中,供后续处理函数使用
	c.Set("currentUser", claims)
	c.Next()
}

Login 该函数用于用户登录,验证用户名和密码是否正确,并返回用户信息。它接收两个参数:username: 用户名 password: 密码 (类型是 string)。同样返回两值:user: 登录成功后返回的用户信息 (类型为 *models.User) err: 登录失败时返回的错误信息 (类型为 error)

函数内部逻辑:

  1. 使用 models.DB 查询是否存在指定用户名的用户。
  2. 如果用户不存在,则返回 errors.New("用户名或密码错误") 错误。
  3. 如果用户存在,则加载用户详细信息。
  4. 使用 models.CheckPassword 函数验证用户输入的密码是否正确。
  5. 如果密码错误,则返回 errors.New("用户名或密码错误") 错误。
  6. 如果验证通过,则返回用户信息和 nil 错误。

RequireAuth 该函数用于验证用户是否携带并拥有有效的 JWT 令牌。它只接收一个类型为 *gin.Context的参数(Gin 框架的上下文)

函数内部逻辑:

  1. 从请求头中获取 Authorization 字段的值,即 JWT 令牌。
  2. 如果 Authorization 字段为空,则返回 http.StatusUnauthorized 状态码和错误信息 "未提供认证令牌"。
  3. 将 Authorization 字段的值分割成两部分,并检查格式是否正确。
  4. 如果格式不正确,则返回 http.StatusUnauthorized 状态码和错误信息 "认证令牌格式错误"。
  5. 使用 jwt.ParseWithClaims 函数解析 JWT 令牌,并将其结果存储在 claims 结构体中。
  6. 如果解析失败或令牌无效,则返回 http.StatusUnauthorized 状态码和错误信息 "认证令牌无效"。
  7. 如果验证通过,则将用户信息设置到 Gin 上下文中,供后续处理函数使用。

总结:这两个函数共同实现了用户认证功能,确保只有拥有有效令牌的用户才能访问需要权限的资源。

接下来就是API接口和路由了。

打开api/user.go文件,编写如下代码:

func UserLogin(c *gin.Context) {
	var u models.User
	if err := c.ShouldBindJSON(&u); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}

	user, err := utils.Login(u.Username, u.Password)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{
			"error": err.Error(),
		})
		return
	}

	token, err := utils.GenerateToken(user.ID, user.Username)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"token": token,
	})
}

UserLogin 该函数用于处理用户登录请求。让我们来详细分解一下:

1. var u models.User

  • 声明了一个名变量 u,类型为 models.User。

2. if err := c.ShouldBindJSON(&u); err != nil {

  • 尝试从请求体中解析 JSON 数据并将其绑定到变量 u 上。
  • 如果解析过程中出现错误 (err != nil),则执行后续代码块。

3. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})

  • 解析 JSON 数据出错时执行到这里。
  • 向客户端返回 JSON 响应,状态码为 http.StatusBadRequest (请求格式错误)。
  • 响应体包含 "error" 键,值为解析错误信息 err.Error()。

4. user, err := utils.Login(u.Username, u.Password)

  • 调用 utils.Login 函数尝试登录用户,使用 u.Username 和 u.Password 进行验证。
  • 登录成功后,user 变量会存储返回的用户信息 (类型为 models.User)。

5. if err != nil {

  • 检查 utils.Login 函数返回的错误 (err)。
  • 如果登录失败 (err != nil) 执行后续代码块。

6. c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})

  • 向客户端返回 JSON 响应,状态码为 http.StatusUnauthorized (未授权)。
  • 响应体包含 "error" 键,值为登录失败信息 err.Error()。

7. token, err := utils.GenerateToken(user.ID, user.Username)

  • 如果登录成功,调用 utils.GenerateToken 函数生成 JWT 令牌。
  • 使用登录成功返回的用户信息 (user.ID 和 user.Username) 作为参数生成令牌。
  • 生成的 JWT 令牌会存储在 token 变量中。

8. if err != nil {

  • 检查 utils.GenerateToken 函数返回的错误 (err)。
  • 生成令牌失败 (err != nil) 时执行后续代码块。

9. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

  • 向客户端返回 JSON 响应,状态码为 http.StatusInternalServerError (服务器内部错误)。
  • 响应体包含 "error" 键,值为生成令牌失败信息 err.Error()。

10. c.JSON(http.StatusOK, gin.H{"token": token})

  • 如果登录和生成令牌都成功,使用 c.JSON 方法向客户端返回 JSON 响应。
  • 状态码为 http.StatusOK (请求成功)。
  • 响应体包含 "token" 键,值为生成的 JWT 令牌 (token)。

完成上述代码后保存,如果IDE不能自动添加"xblog/utils",我们在import中手动添加它。

最后一步。在router.go文件(路由)中写入用户登录的路由:

	public := r.Group("api/v1")
	{
		public.POST("user/add", api.UserAdd)
		public.POST("user/login", api.UserLogin)
	}

好,接下来我们使用如下CURL测试一下:

# 测试Login
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username":"user1","password":"passwd123"}' \
http://10.0.0.185:8000/api/v1/user/login; echo

由图片可知,程序成功返回用户登录后的token。

Tags:

最近发表
标签列表