Gin 框架学习实录 · 第10篇:实现用户登录功能与 JWT Token 签发及中间件验证

前言

在之前的几篇文章中,我们已经完成了 Gin 框架的基本入门,涵盖了用户模块的增删改查、Redis 缓存使用、统一响应结构封装等重要功能模块。经过这些基础模块的搭建,我们的项目框架已经有了一个稳固的基础。

接下来,我们将通过实现一个 登录功能 ,引入 JWT(JSON Web Token) 来进行用户认证,确保用户身份的合法性。具体来说,我们会通过以下步骤来实现:

  1. 实现登录接口:用户通过提交用户名和密码进行身份验证。
  2. JWT Token 签发:登录成功后,服务器将生成一个 JWT Token,并返回给客户端,用于后续的身份验证。
  3. JWT 中间件验证:每次请求时,通过中间件验证请求中携带的 JWT Token,确保用户的身份合法。

通过这一系列的操作,我们能够实现一个简易的用户认证机制,为后续开发更加复杂的 API 接口和权限管理打下基础。

第一步:实现登录功能 + JWT Token 签发

我们先实现这个接口:

POST /login

json 复制代码
{
  "username": "admin",
  "password": "123456"
}

返回:

json 复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "token": "xxxxx.yyyyy.zzzzz"
  }
}

技术拆解 & 方法设计

方法设计清单

方法名 说明
GenerateToken(userID uint) 生成 JWT Token
ParseToken(tokenStr string) 校验 Token 并提取用户 ID
LoginService(username, password string) 登录业务逻辑:校验用户、生成 Token|

1. 安装依赖

首先,我们需要安装 github.com/golang-jwt/jwt/v5 这个库,用于生成和解析 JWT Token。

bash 复制代码
go get github.com/golang-jwt/jwt/v5

2. 创建工具包:utils/jwt.go

接下来,我们创建一个 jwt.go 工具包,封装生成和解析 JWT Token 的功能。在这个文件中,我们定义了一个结构体 Claims,它包括了用户的 ID 和 JWT 的标准字段(比如过期时间、签发时间等)。

go 复制代码
package utils

import (
	"github.com/golang-jwt/jwt/v5"
	"time"
	"errors"
)

var jwtSecret = []byte("my-secret-key") // 可写到 config.yaml

// 自定义声明结构体
type Claims struct {
	UserID uint `json:"user_id"`
	jwt.RegisteredClaims
}

// 生成 Token 的方法
func GenerateToken(userID uint) (string, error) {
	expirationTime := time.Now().Add(24 * time.Hour)

	claims := Claims{
		UserID: userID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			NotBefore: jwt.NewNumericDate(time.Now()),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

// 解析 Token 的方法
func ParseToken(tokenStr string) (*Claims, error) {
	token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})
	if err != nil {
		return nil, err
	}

	// 校验是否是合法的 Claims
	if claims, ok := token.Claims.(*Claims); ok && token.Valid {
		return claims, nil
	}
	return nil, errors.New("invalid token")
}

说明

  • GenerateToken 方法:根据用户 ID 生成一个 JWT Token,并设置过期时间为 24 小时。
  • ParseToken 方法:解析并验证传入的 JWT Token 是否有效,并返回解码后的 Claims。

3. 创建登录请求结构体(request/user_request.go

我们接下来需要定义一个登录请求的结构体,用户提交登录信息时,我们会从这个结构体中获取 usernamepassword,然后在服务器端进行验证。

go 复制代码
type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

4.实现登录逻辑

service/user_service.go 中新增登录逻辑

在服务层,我们新增一个 DoLogin 方法来处理用户登录。该方法根据传入的用户名和密码查找数据库中的用户记录。如果查找到用户并且密码正确,则使用之前封装的 GenerateToken 方法生成并返回 JWT Token。这里我们暂时使用明文密码进行校验,后续可以替换为加密(如 bcrypt)进行校验。

go 复制代码
package service

import (
	"gin-learn-notes/config"
	"gin-learn-notes/model"
	"gin-learn-notes/utils"
)

func DoLogin(username, password string) (string, error) {
	var user model.User
	// 查找数据库中的用户,验证用户名和密码
	if err := config.DB.Where("name= ? AND password= ?", username, password).First(&user).Error; err != nil {
		return "", err
	}

	// 调用封装的 token 生成方法
	token, err := utils.GenerateToken(user.ID)
	if err != nil {
		return "", err
	}

	return token, nil
}

说明:

  • DoLogin :该函数通过用户名和密码查找用户,如果找到并且密码正确,则返回一个 JWT Token。密码校验这里先不加密,后续可以加入 bcrypt 或其他加密方法来增强安全性。

controller/user.go 中新增登录接口

接着,在控制器层我们实现了 Login 方法,这个方法接收前端提交的用户名和密码,调用 DoLogin 逻辑进行验证。如果验证成功,返回 JWT Token,否则返回相应的错误信息。

go 复制代码
package controller

import (
	"gin-learn-notes/request"
	"gin-learn-notes/service"
	"gin-learn-notes/response"
	"github.com/gin-gonic/gin"
)

func Login(c *gin.Context) {
	var req request.LoginRequest
	// 绑定请求体中的参数
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Fail(c, response.ParamError, "参数错误")
		return
	}

	// 调用 service 层的登录逻辑
	token, err := service.DoLogin(req.Username, req.Password)
	if err != nil {
		// 用户名或密码错误
		response.Fail(c, response.Unauthorized, "用户名或密码错误")
		return
	}

	// 返回成功的响应,包含 token
	response.Success(c, gin.H{
		"token": token,
	})
}

说明:

  • Login :该函数接收前端传递的用户名和密码,通过 service.DoLogin 进行验证。如果登录成功,则返回 JWT Token,否则返回 "用户名或密码错误"

注册路由

router/router.go 中,我们添加了 /login 路由,使得客户端能够发送 POST 请求到该接口以进行登录。

go 复制代码
package router

import (
	"github.com/gin-gonic/gin"
	"gin-learn-notes/controller"
)

func InitRouter() *gin.Engine {
	r := gin.Default()

	// 登录路由
	r.POST("/login", controller.Login)

	// 其他路由...

	return r
}

5. 测试登录接口

我们已经实现了登录接口,并且通过 JWT Token 进行认证。接下来,我们进行接口的测试。

1. 启动项目

首先,确保项目已经正常启动。通过以下命令启动我们的 Gin 项目:

bash 复制代码
go run main.go

确认服务已在指定的端口启动。

2. 使用 Postman 或 Curl 测试登录接口

测试请求:

我们使用 Postman 或 Curl 发送一个 POST 请求到 /login 接口,提交 usernamepassword

请求示例:

json 复制代码
POST /login
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

测试响应:

如果用户名和密码正确,返回如下响应:

json 复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "token": "your-jwt-token-here"
  }
}

如果用户名或密码错误,返回如下错误信息:

json 复制代码
{
  "code": 10004,
  "msg": "用户名或密码错误",
  "data": null
}

我们之前已经成功实现了登录功能,并使用 JWT Token 进行身份验证。在测试登录接口时,返回的示例数据如下:

json 复制代码
{
  "code": 0,
  "msg": "success",
  "data": {
    "token": "your-jwt-token-here"
  }
}

这个响应返回了用户的 JWT Token。然而,在实际开发中,登录接口不仅仅返回用户的 Token ,可能还会返回其他的用户信息,比如 用户 ID用户名。因此,我们需要对现有的登录逻辑进行一些修改,以便返回更多用户信息。

如何修改登录返回结果?

为了在登录返回结果中不仅仅返回 token,还要返回 userIDusername,我们可以通过新增一个返回结构体专门用于登录接口的响应数据。这个结构体可以最初放在 user_service.go 文件中,但为了更好的代码结构和复用性,我们可以将其拆分到独立的文件中。

拆分结构体

为了避免结构体定义过于集中在业务逻辑中,并提升代码的可维护性,我们将 LoginResponse 结构体提取到一个单独的文件中。通常,这种结构体可以放在 response 包下。

原因:

考虑到后续项目接口越来越多,若将所有响应结构体放在一个 response.go 文件中会变得非常臃肿且不易维护。为了避免这种情况,建议按 模块功能 对响应结构体进行拆分管理。每个模块对应一个单独的响应文件,保持代码的清晰和结构化。

因此,我们可以按照模块划分,在 response 目录下创建 user_response.go 文件,并将登录返回的结构体 LoginResult 放入该文件中。

步骤:修改和拆分结构体

1. 创建 response/user_response.go 文件

response 文件夹下,我们新建 user_response.go 文件,用于存放与用户相关的所有响应结构体。

response/user_response.go

go 复制代码
package response

// LoginResult 是用户登录的返回结构
type LoginResult struct {
	Token    string `json:"token"`
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
}

2. 修改 user_service.go

service/user_service.go 中,我们调用新的 LoginResult 返回结构体,并修改 DoLogin 函数,使其返回用户信息以及 token

go 复制代码
package service

import (
	"gin-learn-notes/config"
	"gin-learn-notes/model"
	"gin-learn-notes/utils"
	"gin-learn-notes/response"
	"errors"
)

// 登录验证逻辑
func DoLogin(username, password string) (*response.LoginResult, error) {
	var user model.User
	if err := config.DB.Where("name = ? AND password = ?", username, password).First(&user).Error; err != nil {
		return nil, errors.New("用户名或密码错误")
	}

	// 生成 Token
	token, err := utils.GenerateToken(user.ID)
	if err != nil {
		return nil, err
	}

	// 返回 token 和用户信息
	return &response.LoginResult{
		Token:    token,
		UserID:   user.ID,
		Username: user.Name,
	}, nil
}

3. 修改控制器 controller/user.go

在控制器中,我们通过调用 DoLogin 返回的结构体,发送用户信息和 token 给前端。

go 复制代码
package controller

import (
	"gin-learn-notes/request"
	"gin-learn-notes/service"
	"gin-learn-notes/response"
	"github.com/gin-gonic/gin"
)

func Login(c *gin.Context) {
	var req request.LoginRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Fail(c, response.ParamError, "参数错误")
		return
	}

	// 调用 DoLogin 进行登录验证
	res, err := service.DoLogin(req.Username, req.Password)
	if err != nil {
		response.Fail(c, response.Unauthorized, err.Error())
		return
	}

	// 返回 token 和用户信息
	response.Success(c, gin.H{
		"token":    res.Token,
		"user_id":  res.UserID,
		"username": res.Username,
	})
}

4. 路由配置

确保在路由中添加 /login 接口:

go 复制代码
package router

import (
	"github.com/gin-gonic/gin"
	"gin-learn-notes/controller"
)

func InitRouter() *gin.Engine {
	r := gin.Default()

	// 登录路由
	r.POST("/login", controller.Login)

	// 其他路由...

	return r
}

然后我们再调用登陆接口就可以看到返回结果如下:

总结

  1. 拆分结构体 :为了更好的代码复用和维护性,我们将 LoginResponse 结构体从 service 中拆分出来,放入 response/user_response.go 文件中。
  2. 模块化管理响应结构体:考虑到后续接口会增多,我们将响应结构体按模块进行拆分管理,确保每个模块的响应结构体文件清晰明了,便于维护。
  3. 修改 DoLogin 和控制器 :在 service 层,我们修改了 DoLogin 函数,使其返回 token 和用户信息;在控制器中,我们相应地修改了 Login 函数,将 token 和用户信息一起返回给前端。

这样,我们的登录接口不仅返回了 token,还返回了用户的 ID 和用户名,同时通过模块化管理响应结构体,使得项目代码结构更加清晰。


第二步:JWT 鉴权中间件开发

在本步骤中,我们实现一个用于验证 JWT Token 的中间件 JWTAuthMiddleware,它的主要功能是:

  1. 从请求头提取 token :自动从请求的 Authorization 头中提取 Bearer <token>
  2. 解析 Token :调用 utils.ParseToken() 解析并验证 JWT 的有效性。
  3. 获取用户信息 :从解析出的 claims 中获取 userID
  4. 注入上下文 :将 userID 注入到 Gin 的上下文中,供后续的业务逻辑使用。
  5. 验证失败时返回 401:如果 Token 无效或过期,则返回 401 错误,提示无权限访问。

方法设计清单

方法名 作用
JWTAuthMiddleware() 返回一个 Gin 中间件,用于处理 Token 校验
ParseToken(token string) 解析 JWT,已封装在 utils/jwt.go
c.Set("userID", userID) userID 注入到 Gin 上下文,供后续业务使用

1. 创建中间件文件:middleware/jwt_auth.go

首先,我们在 middleware 目录下创建了 jwt_auth.go 文件,并实现了 JWTAuthMiddleware 中间件。这个中间件的主要功能是从请求的 Authorization 头部获取 JWT Token,验证其有效性,并将 userID 注入到 Gin 的上下文中。如果验证失败,则返回对应的错误信息和错误码。

go 复制代码
package middleware

import (
	"gin-learn-notes/core/response"
	"gin-learn-notes/utils"
	"github.com/gin-gonic/gin"
	"strings"
)

func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从 header 获取 Authorization
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			response.Fail(c, response.InvalidToken, "Token无效或者不存在")
			c.Abort()
			return
		}

		// 处理 "Bearer xxxxx" 格式
		parts := strings.SplitN(authHeader, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			response.Fail(c, response.TokenFormatError, "Token格式错误")
			c.Abort()
			return
		}

		// 解析 token
		claims, err := utils.ParseToken(parts[1])
		if err != nil {
			response.Fail(c, response.TokenExpired, "Token无效或已过期")
			c.Abort()
			return
		}

		// 注入 userID 到上下文
		c.Set("userID", claims.UserID)

		// 放行
		c.Next()
	}
}

中间件说明:

  • JWTAuthMiddleware:此中间件会从请求头中获取 Token,解析并校验 Token 的有效性。如果 Token 有效,则将 userID 注入到上下文 c.Set("userID", userID),供后续操作使用;如果验证失败,则返回相应的错误信息(如 Token 格式错误或已过期)。

新增错误码

core/response/code.go 中新增了以下三个错误码,以便在 Token 校验失败时进行响应:

go 复制代码
package response

const (
	Success      = 0
	ParamError   = 10001
	NotFound     = 10002
	DBError      = 10003
	Unauthorized = 10004
	ServerError  = 10005

	// 新增错误码
	InvalidToken    = 10006 // Token 无效或不存在
	TokenFormatError = 10007 // Token 格式错误
	TokenExpired     = 10008 // Token 已过期
)

2.注册路由中使用中间件

router 配置中,我们将受保护的接口放在一个新的路由组中,使用 JWTAuthMiddleware 中间件来进行保护,确保只有通过认证的用户才能访问。

go 复制代码
import (
	"gin-learn-notes/middleware"
)

func InitRouter() *gin.Engine {
	r := gin.Default()

	// 登录接口,无需 token
	r.POST("/login", controller.Login)

	// 需要登录的接口分组
	auth := r.Group("/api")
	auth.Use(middleware.JWTAuthMiddleware())
	{
	    auth.POST("/profile", controller.GetUserProfile)
	}

	return r
}

3.在业务中取出 userID

在中间件中,我们将 userID 注入到了 Gin 的上下文中。在后续的业务逻辑中,我们可以从上下文中提取 userID,进行权限验证或获取用户信息。

go 复制代码
userIDRaw, exists := c.Get("userID")
if !exists {
	response.Fail(c, 401, "用户未登录")
	return
}

userID := userIDRaw.(uint) // 类型断言

为了简化获取 userID 的过程,我们可以封装一个获取 userID 的方法,放在 utils/context.go 中:

go 复制代码
func GetUserID(c *gin.Context) uint {
	if userIDRaw, exists := c.Get("userID"); exists {
		if userID, ok := userIDRaw.(uint); ok {
			return userID
		}
	}
	return 0
}

调用时可以这样简洁:

go 复制代码
userID := utils.GetUserID(c)

4. 测试步骤

  1. 登录: 通过 /login 获取 token
  2. 请求受保护接口: 请求 /api/profile 接口时,附带 Authorization: Bearer <token>,中间件会自动拦截并校验 token
  3. 中间件校验: 中间件验证通过后,userID 被注入到上下文中,后续的处理可以直接从上下文中获取用户信息。

验证 /api/profile 接口:JWT 鉴权

为了验证 JWT 鉴权的功能,我们将修改 GetUserProfile 方法,使其能够从上下文中获取 userID,而不是通过请求结构体获取。这将确保只有通过 JWT 鉴权的用户才能访问该接口。

修改 GetUserProfile 方法

我们将在 controller/user.go 中修改 GetUserProfile 方法,改为从上下文中获取 userID,而不是请求结构体中的 ID。具体修改如下:

go 复制代码
package controller

import (
	"gin-learn-notes/response"
	"gin-learn-notes/service"
	"gin-learn-notes/utils"
	"github.com/gin-gonic/gin"
)

func GetUserProfile(c *gin.Context) {
	// 从上下文中获取用户ID
	userID := utils.GetUserID(c)
	if userID == 0 {
		// 如果用户ID为0,说明用户未登录或Token无效
		response.Fail(c, response.Unauthorized, "未登录或 token 无效")
		return
	}

	// 使用缓存获取用户信息
	user, err := service.GetUserProfileWithCache(userID)
	if err != nil {
		// 如果用户不存在
		response.Fail(c, response.NotFound, "用户不存在")
		return
	}

	// 返回用户信息
	response.Success(c, user)
}

说明:

  • userID := utils.GetUserID(c):从 Gin 上下文中获取 userID,它是在 JWTAuthMiddleware 中间件中注入的。
  • 如果 userID0,说明 Token 无效或者用户未登录,这时返回 Unauthorized 错误。
  • 通过 service.GetUserProfileWithCache(userID) 获取用户信息,并返回。

接口测试流程

  1. 登录获取 Token:

    • 首先,通过 /login 接口使用用户名和密码登录,获取返回的 Token。
    • 请求示例:
    bash 复制代码
    POST /login
    Content-Type: application/json
    
    {
        "username": "admin",
        "password": "123456"
    }
    • 响应示例:
    json 复制代码
    {
        "code": 0,
        "msg": "success",
        "data": {
            "token": "your-jwt-token-here"
             ...
        }
    }
  2. 请求 /api/profile 接口:

  • 获取到 Token 后,使用 Authorization 头部携带 Bearer <token> 请求 /api/profile 接口。
  • 请求示例:
bash 复制代码
POST /api/profile
Authorization: Bearer <your-jwt-token-here>
  • 响应示例(假设用户存在):
json 复制代码
{
    "code": 0,
    "msg": "success",
    "data": {
        "id": 1,
        "name": "user1",
        "age": 25
    }
}
  • 如果 Token 无效或者过期,接口会返回:
json 复制代码
{
    "code": 10006,
    "msg": "Token无效或者不存在",
    "data": null
}

总结

  1. 修改 GetUserProfile 方法 :我们将 GetUserProfile 方法从通过请求结构体获取用户 ID 改为从上下文中获取 userID,确保只有经过 JWT 鉴权的用户才能访问该接口。
  2. 中间件处理JWTAuthMiddleware 中间件会验证请求头中的 Token,确保请求是合法的。如果验证通过,将 userID 注入到上下文中,供后续的接口使用。
  3. 接口测试 :我们测试了登录获取 Token 后,如何通过带 Token 的请求访问受保护的 /api/profile 接口。并且验证了 Token 无效时,接口会返回相应的错误信息。

通过这一系列步骤,我们就已经简单实现了 JWT 鉴权和用户信息获取功能使用。


项目结构继续演进

go 复制代码
gin-learn-notes/
├── config/
│   ├── config.go        // 配置加载与管理
│   ├── database.go      // 数据库连接配置
│   └── redis.go         // Redis 配置
├── controller/
│   ├── hello.go         // 测试接口
│   ├── index.go         // 首页接口
│   └── user.go          // 用户模块接口
├── core/
│   └── response/        // 统一响应模块
│       ├── code.go      // 错误码定义
│       ├── page.go      // 分页响应结构
│       └── response.go  // 统一响应结构体
├── logger/
│   └── logger.go        // 日志配置与处理
├── logs/
│   └── app.log          // 日志文件
├── middleware/
│   └── jwt_auth.go      // JWT 鉴权中间件
├── model/
│   └── user.go          // 用户数据模型
├── request/
│   ├── page_request.go  // 分页请求参数
│   └── user_request.go  // 用户请求参数
├── response/
│   └── user_response.go // 用户模块响应结构
├── router/
│   └── router.go        // 路由配置
├── service/
│   └── user_service.go  // 用户模块服务层
├── utils/
│   ├── context.go       // 上下文工具
│   ├── jwt.go           // JWT 生成与解析工具
│   ├── paginate.go      // 分页工具
│   ├── redis.go         // Redis 工具
│   ├── response.go      // 通用响应工具
│   └── validator.go     // 验证工具
├── config.yaml          // 全局配置文件
├── go.mod               // Go Modules 配置
├── LICENSE              // 开源协议
├── main.go              // 项目入口
└── README.md            // 项目说明

本篇对应代码提交记录

commit: 51e0668b35603642683f0651067989f480df93cd

👉 GitHub 源码地址:github.com/luokakale-k...

相关推荐
宾燕哥哥8 小时前
Go 语言基础学习文档
go
Way2top9 小时前
Go语言动手写Web框架 - Gee第一天
go
探索云原生10 小时前
Buildah 简明教程:让镜像构建更轻量,告别 Docker 依赖
linux·docker·云原生·go·cicd
越千年11 小时前
工作中常用到的二进制运算
后端·go
踏浪无痕17 小时前
信不信?一天让你从Java工程师变成Go开发者
后端·go
卡尔特斯1 天前
Go 语言入门核心概念总结
go
代码扳手2 天前
从0到1揭秘!Go语言打造高性能API网关的核心设计与实现
后端·go·api
未来魔导2 天前
go语言中json操作总结(下)
数据分析·go·json
未来魔导2 天前
Go-qdrant-API开启客服系统新模式
go·api·qdrant
喵个咪3 天前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:Makefile 在后端开发中的应用与 Windows 环境配置
后端·go