前言

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

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

实现登录接口:用户通过提交用户名和密码进行身份验证。

JWT Token 签发:登录成功后,服务器将生成一个 JWT Token,并返回给客户端,用于后续的身份验证。

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)

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

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 接口,提交 username 和 password。

请求示例:

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,还要返回 userID 和 username,我们可以通过新增一个返回结构体专门用于登录接口的响应数据。这个结构体可以最初放在 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

}

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

总结

拆分结构体 :为了更好的代码复用和维护性,我们将 LoginResponse 结构体从 service 中拆分出来,放入 response/user_response.go 文件中。

模块化管理响应结构体:考虑到后续接口会增多,我们将响应结构体按模块进行拆分管理,确保每个模块的响应结构体文件清晰明了,便于维护。

修改 DoLogin 和控制器 :在 service 层,我们修改了 DoLogin 函数,使其返回 token 和用户信息;在控制器中,我们相应地修改了 Login 函数,将 token 和用户信息一起返回给前端。

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

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

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

从请求头提取 token :自动从请求的 Authorization 头中提取 Bearer

解析 Token :调用 utils.ParseToken() 解析并验证 JWT 的有效性。

获取用户信息 :从解析出的 claims 中获取 userID。

注入上下文 :将 userID 注入到 Gin 的上下文中,供后续的业务逻辑使用。

验证失败时返回 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. 测试步骤

登录: 通过 /login 获取 token。

请求受保护接口: 请求 /api/profile 接口时,附带 Authorization: Bearer ,中间件会自动拦截并校验 token。

中间件校验: 中间件验证通过后,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 中间件中注入的。

如果 userID 为 0,说明 Token 无效或者用户未登录,这时返回 Unauthorized 错误。

通过 service.GetUserProfileWithCache(userID) 获取用户信息,并返回。

接口测试流程

登录获取 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"

...

}

}

请求 /api/profile 接口:

获取到 Token 后,使用 Authorization 头部携带 Bearer 请求 /api/profile 接口。

请求示例:

bash

复制代码

POST /api/profile

Authorization: Bearer

响应示例(假设用户存在):

json

复制代码

{

"code": 0,

"msg": "success",

"data": {

"id": 1,

"name": "user1",

"age": 25

}

}

如果 Token 无效或者过期,接口会返回:

json

复制代码

{

"code": 10006,

"msg": "Token无效或者不存在",

"data": null

}

总结

修改 GetUserProfile 方法 :我们将 GetUserProfile 方法从通过请求结构体获取用户 ID 改为从上下文中获取 userID,确保只有经过 JWT 鉴权的用户才能访问该接口。

中间件处理 :JWTAuthMiddleware 中间件会验证请求头中的 Token,确保请求是合法的。如果验证通过,将 userID 注入到上下文中,供后续的接口使用。

接口测试 :我们测试了登录获取 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...