Day 9:用户认证与 JWT 实战

Table of Contents

Day 9:用户认证与 JWT 实战

在上一章中,我们用 Gin/Fiber 写了一个最基本的用户注册和登录接口。但当用户登录之后,我们需要在随后的 API 请求中知道 “这个用户是谁”,并验证用户是否有权限访问某些资源。这就是 用户认证 的核心。

本章我们将学习如何在 Go Web 服务中实现 JWT (JSON Web Token) 用户认证机制,并在任务管理系统中加上 登录态鉴权


1. 用户认证的常见方式

常见的 Web 服务认证方式有:

  • Session + Cookie:后端保存 session,前端通过 cookie 传递 session ID(状态化,依赖存储)。
  • Token(推荐):服务端生成 token,客户端每次请求时带上 token(无状态,适合微服务与移动端)。
  • JWT(JSON Web Token):一种特别的 Token 格式,包含了用户信息和签名,无需存储。

我们选择 JWT,因为它:

  • 无需服务端保存登录状态,适合微服务
  • 可以携带用户 ID、角色等信息。
  • 有过期时间,安全可控。

2. JWT 结构解析

一个 JWT 长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.   # Header
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IndhbHRlciIsImV4cCI6MTY5Nzg0NTYwMH0.   # Payload
kOk0Ex1uT_z9mGidZ6ABR5m2ndTvn2kTQmYTxg5zFvI   # Signature

三部分:

  1. Header:说明算法(如 HS256)
  2. Payload:用户数据(如 user_id=1exp=过期时间
  3. Signature:签名(防篡改)

3. 项目需求

我们要实现:

  • 用户注册(沿用 Day 8)
  • 用户登录 -> 返回 JWT
  • 用户访问任务接口时,必须带上 JWT
  • JWT 过期后,需要重新登录

4. 实战:Go + Gin + JWT

我们用 Gin 和 github.com/golang-jwt/jwt/v5 库。

4.1 安装依赖

go get github.com/gin-gonic/gin
go get github.com/golang-jwt/jwt/v5

4.2 定义用户和内存存储

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var userStore = map[string]string{} // 简单内存存储: username -> password
var jwtKey = []byte("my_secret_key") // JWT 密钥

type Claims struct {
    Username string `json:"username"`
    jwt.RegisteredClaims
}

4.3 用户注册 & 登录

// 注册
func register(c *gin.Context) {
    var req struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
        return
    }
    if _, exists := userStore[req.Username]; exists {
        c.JSON(http.StatusConflict, gin.H{"error": "user exists"})
        return
    }
    userStore[req.Username] = req.Password
    c.JSON(http.StatusOK, gin.H{"message": "register success"})
}

// 登录 -> 签发 JWT
func login(c *gin.Context) {
    var req struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
        return
    }
    if userStore[req.Username] != req.Password {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
        return
    }

    // 设置过期时间
    expirationTime := time.Now().Add(1 * time.Hour)
    claims := &Claims{
        Username: req.Username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expirationTime),
        },
    }

    // 生成 token
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(jwtKey)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create token"})
        return
    }

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

4.4 JWT 验证中间件

// 认证中间件
func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenString := c.GetHeader("Authorization")
        if tokenString == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
            c.Abort()
            return
        }

        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
            return jwtKey, nil
        })
        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            c.Abort()
            return
        }

        // 存储用户名到上下文
        c.Set("username", claims.Username)
        c.Next()
    }
}

4.5 受保护的任务接口

func taskList(c *gin.Context) {
    username := c.GetString("username")
    c.JSON(http.StatusOK, gin.H{
        "tasks": []string{
            "Task1 for " + username,
            "Task2 for " + username,
        },
    })
}

4.6 主程序

func main() {
    r := gin.Default()

    r.POST("/register", register)
    r.POST("/login", login)

    auth := r.Group("/api")
    auth.Use(authMiddleware())
    {
        auth.GET("/tasks", taskList)
    }

    r.Run(":8080")
}

5. 测试流程

  1. 注册:
curl -X POST http://localhost:8080/register \
  -H "Content-Type: application/json" \
  -d '{"username":"walter","password":"123"}'
  1. 登录获取 JWT:
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username":"walter","password":"123"}'

返回:

{"token":"eyJhbGciOi..."}
  1. 访问任务接口:
curl -X GET http://localhost:8080/api/tasks \
  -H "Authorization: eyJhbGciOi...JWT"

返回:

{"tasks":["Task1 for walter","Task2 for walter"]}

6. 小结

  • 了解了 JWT 的结构与原理
  • 使用 Gin 实现了 注册、登录、签发 Token、鉴权中间件
  • 将任务接口保护起来,只有登录后才能访问。

7. 思考与练习

  1. 修改 Token 过期时间为 5 分钟,过期后如何刷新 Token?
  2. 如何在 JWT 中加入用户角色字段,并在中间件中进行权限校验?
  3. 将用户存储从内存改为 数据库(Day 10 将实现)

Comments |0|

Legend *) Required fields are marked
**) You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Category: 似水流年