前言
在之前的几篇文章中,我们已经完成了 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>。 - 解析 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>,中间件会自动拦截并校验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。 - 请求示例:
bashPOST /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 <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
}
总结
- 修改
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...