GO学习笔记 | 第七章节 JWT、Redis 入门与 K8s 部署实战| Session 深入与 JWT 入门
代码仓库地址:Darling-123456/go_learning: go学习过程记录
核心内容:多实例 Session 共享、面向接口编程实践、Session 刷新机制、JWT 结构原理与登录校验
前置知识:Gin Session 中间件、Cookie/Session 机制、分层架构
一、Debug 技巧:分层断点定位
1.1 请求经过的完整链路
1 2 3 4 5 6
| 浏览器请求 → 日志确认到达 → Middleware 1 → Middleware 2 → Middleware 3 → handler(业务入口) → service(业务逻辑) → repository(存储抽象) → dao(数据库操作)
|
1.2 定位问题的策略
- 先看 Gin 日志:确认后端是否收到请求(有请求一定有日志)
- 在 handler 入口打断点:看是否到达业务逻辑(没到就是中间件拦截了)
- 逐层打断点:web → service → repository → dao,每层入口一个断点,看哪层没进去
- 分支全覆盖:每个 if/else 分支都打断点,确认走了哪个分支
定位问题本质上是经验——踩坑多了,看错误信息就知道在哪。亲手写的代码定位更快。
二、Session Store:数据存哪?

2.1 回顾 Session 的存储结构

1 2 3 4 5
| Cookie(my_session = abc123) ↓ 指向 Store(真正存数据的地方) ↓ 存着 业务数据(如 userId=123)
|
Cookie 里永远只放 sessionID,真正的数据在 Store 里。
2.2 五种 Store 实现

Gin Session 插件提供了多种 Store:
| Store |
数据存哪 |
适用场景 |
cookie |
Cookie 本身 |
数据不敏感、量很小(基本不用) |
memstore |
进程内存 |
开发 / 测试 / 单实例 |
redis |
Redis |
生产多实例(最常用) |
gorm |
数据库(MySQL/PG 等) |
没有 Redis 时的备选 |
mongodb / postgres |
对应数据库 |
特殊场景 |

2.3 Memstore:单实例可用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/memstore" )
store := memstore.NewStore( []byte("32-or-64-byte-encryption-key"), []byte("32-or-64-byte-authentication-key"), ) store.Options(sessions.Options{ HttpOnly: true, Secure: false, MaxAge: 3600, }) server.Use(sessions.Sessions("mysession", store))
|
密钥用随机生成器生成,越长越安全。公网生成也没关系——别人不知道你用在哪。
2.4 多实例部署的问题

1 2
| 实例A(登录了,memstore 有 session) 实例B(没有 session,判定未登录)
|
Memstore 的数据只在当前进程内存里,实例 A 和实例 B 不互通。 用户请求被负载均衡到实例 B 时,就变成「未登录」。
解决方案:用 Redis 做集中式 Session 存储——所有实例共享同一个 Redis。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| version: '3.0'
services: mysql8: image: mysql:8.0 restart: always command: --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: 040725ge volumes: - ./script/mysql/:/docker-entrypoint-initdb.d/ ports: - "13316:3306"
redis: image: 'redis:latest' environment: - ALLOW_EMPTY_PASSWORD=yes ports: - '6379:6379'
|

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| func initWebServer() *gin.Engine { server := gin.Default() server.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:3000"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, AllowCredentials: true, MaxAge: 12 * time.Hour, })) store, err := redis.NewStore(16, "tcp", "localhost:6379", "root", "", []byte("qOYZLAuWmwkxAKG6bijwru9ghNNS9rHc"), []byte("8Mv11Olt6x3DX97rUE1exp9XISEMSZJl")) if err != nil { panic(err) } server.Use(sessions.Sessions("mysession", store)) server.Use(middleware.NewLoginMiddlewareBuilder(). IgnorePaths("/users/signup"). IgnorePaths("/users/login").Build())
return server }
|
把步骤一的cookie实现的session换成了redis
1 2 3 4 5 6
| docker exec -it webook-redis-1 redis-cli
docker ps
monitor
|

这是登录过程中的redis的样子的体现
三、面向接口编程:第一次真正感受到好处

Store 是接口,可以随便换
1 2 3 4 5 6 7 8 9 10 11
|
store := memstore.NewStore(key, authKey)
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", key, authKey)
server.Use(sessions.Sessions("mysession", store))
|
换了 Store 之后:
- handler 代码:不用改
- service 代码:不用改
- 登录校验中间件:不用改
- 只在
main.go 的初始化里换一行
这就是面向接口编程的核心价值——依赖接口而非具体实现,底层可以随意替换。
四、Session 配置详解

4.1 Options 控制的是 Cookie
里面的字段都是cookie的字段
1 2 3 4 5 6 7 8
| sess.Options(sessions.Options{ Path: "/", Domain: "", MaxAge: 3600, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode, })
|
关键提醒:
Secure: true 开启后,HTTP 协议下 Cookie 不会生效——开发环境先用 false
MaxAge 设为负数 → Cookie 立即过期 → 实现退出登录
4.2 退出登录
把maxage设置为负数就是退出登录
1 2 3 4 5 6 7
| func (u *UserHandler) Logout(ctx *gin.Context) { sess := sessions.Default(ctx) sess.Options(sessions.Options{MaxAge: -1}) sess.Save() ctx.String(http.StatusOK, "退出登录") }
|
五、Session 刷新(Sliding Expiration)


5.1 需求



- 登录态保持 30 分钟
- 用户活跃使用时自动续期
- 实现思路:在登录校验中间件里检测上次刷新时间,超过阈值就刷新
5.2 实现方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| package middleware
import ( "net/http" "time"
"github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" )
type LoginMiddlewareBuilder struct { paths []string }
func NewLoginMiddlewareBuilder() *LoginMiddlewareBuilder { return &LoginMiddlewareBuilder{} }
func (l *LoginMiddlewareBuilder) IgnorePaths(path string) *LoginMiddlewareBuilder { l.paths = append(l.paths, path) return l }
func (l *LoginMiddlewareBuilder) Build() gin.HandlerFunc { return func(ctx *gin.Context) { for _, path := range l.paths { if ctx.Request.URL.Path == path { return } }
sess := sessions.Default(ctx)
id := sess.Get("userId") if id == nil { ctx.AbortWithStatus(http.StatusUnauthorized) return }
updateTime := sess.Get("update_time") sess.Set("userId", id) sess.Options(sessions.Options{ MaxAge: 60 * 30, }) now := time.Now().UnixMilli() if updateTime == nil { sess.Set("update_time", now) sess.Save() return }
updateTimeVal, ok := updateTime.(int64) if !ok { ctx.AbortWithStatus(http.StatusInternalServerError) return } if now-updateTimeVal > 60*1000 { sess.Set("update_time", now) sess.Save() return } } }
|
要点:
- 刷新不是登录本身的概念,是管理 Session 的概念——放在中间件里而非登录逻辑里
- 每次刷新要重新保存所有数据(
userId 也要重新放回去)
- 刷新时要重新设置
MaxAge
六、JWT(JSON Web Token)

6.1 为什么要学 JWT?
Session 需要服务端存储(内存/Redis),JWT 是无状态的——数据直接编码在 Token 里,服务端不需要存。
6.2 JWT 的结构

1 2 3 4
| eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEyM30.签名部分 ↓ ↓ ↓ Header Payload Signature (Base64编码) (Base64编码数据) (防篡改签名)
|
以 . 分割的三段:
| 部分 |
内容 |
作用 |
| Header(红色) |
签名算法类型(如 HS512) |
告诉如何验证 |
| Payload(紫色) |
真正的数据(如 userId:123) |
业务数据 |
| Signature(蓝色) |
用密钥对前两段签名 |
防篡改 |
防篡改原理:攻击者改了 Payload → 重新算签名 → 和蓝色段对不上 → 服务端直接拒绝。没有密钥就算不出正确的签名。
6.3 JWT 库选择

Go 的标准 JWT 库:github.com/golang-jwt/jwt/v5
老师不建议用 Gin 的 JWT 插件——封装过于复杂,屏蔽了 JWT 核心理解。直接用原始 API 更清晰。

1. Raw string
- 含义:原始的 JWT 字符串。
- 对应标准:完整的 JWT,即类似
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3T1WWT51SzEqFRI 这种格式。
- 用途:通常用于调试、日志记录,或者当需要对 Token 做特殊字符串处理时使用(不过正常情况下不需要用到它)。
2. Method SigningMethod
- 含义:代表签名算法的接口对象。
- 对应标准:JWT 头部的
alg(算法)字段。
- 用途:包含了用于生成和验证签名的加密方法。常见的实现有
jwt.SigningMethodHS256(HMAC-SHA256)、jwt.SigningMethodRS256(RSA-SHA256)等。
- 开发注意:在解析 Token 时,这个字段在验证前是空的,验证后库会根据 JWT 的头部自动赋予正确的值。
- 含义:JWT 的头部数据,是一个 Go 的 Map。
- 对应标准:JWT 的第一部分。
- 用途:包含
alg(加密算法)和 typ(类型,通常是 "JWT"),以及你也可以在此放入自定义的头部信息。
4. Claims Claims
- 含义:JWT 的载荷(Payload)数据。这里的
Claims 是一个接口类型。
- 对应标准:JWT 的第二部分(加密前的 JSON 数据)。
- 用途:这是业务核心。开发者需要自己定义结构体来实现这个
Claims 接口(比如包含 UserId、Username 以及标准时间 exp 等)。你验证完 Token 后,业务数据就从这里获取。
5. Signature []byte
- 含义:签名数据,以
[]byte 字节切片的形式存储。
- 对应标准:JWT 的第三部分(加密后的二进制数据)。
- 用途:这是 JWT 防篡改的核心。当解析 Token 时,库会拿着你的密钥,用
Method 里的算法,把 Header 和 Claims 重新加密算出一个签名,然后对比这个 Signature 是否一致。
6. Valid bool
- 含义:代表当前 Token 是否有效。
- 用途:
- 如果 JWT 解析成功,签名验证通过,并且没有过期,该字段为
true。
- 如果签名对不上、或者 Token 过期、或者解析失败,该字段为
false。
- 开发注意:通常你在拦截器(Middleware)里拿到 Token 对象后,必须检查
token.Valid 是否为 true,才能允许后续的请求通过。
6.4 签名算法选择
1 2 3 4 5 6 7 8 9
| jwt.SigningMethodHS256 jwt.SigningMethodHS384 jwt.SigningMethodHS512
jwt.SigningMethodRS256 jwt.SigningMethodES256 jwt.SigningMethodEdDSA
|
- HS 系列:密钥相同,简单;用 HS512 即可
- RS/ES 系列:公私钥分离,更安全但更复杂
6.5 登录时生成 JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| func (u *UserHandler) LoginJWT(ctx *gin.Context) { type LoginReq struct { Email string `json:"email"` Password string `json:"password"` } var req LoginReq if err := ctx.Bind(&req); err != nil { return } user, err := u.svc.Login(ctx, req.Email, req.Password) if err == service.ErrInvalidUserPassword { ctx.String(http.StatusOK, "用户名或密码不对") return } if err != nil { ctx.String(http.StatusOK, "系统错误") return }
token := jwt.New(jwt.SigningMethodHS512) tokenStr, err := token.SignedString([]byte("qOYZLAuWmwkxAKG6bijwru9ghNNS9rHc")) if err != nil { ctx.String(http.StatusInternalServerError, "系统错误") return } ctx.Header("x-jwt-token", tokenStr) fmt.Println(tokenStr) fmt.Println(user) ctx.String(http.StatusOK, "登录成功") }
|
6.6 JWT 登录校验中间件
这个还没有携带uid等数据,只是完成了简单登录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package middleware
import ( "net/http" "strings"
"github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" )
type LoginJWTMiddlewareBuilder struct { paths []string }
func NewLoginJWTMiddlewareBuilder() *LoginJWTMiddlewareBuilder { return &LoginJWTMiddlewareBuilder{} }
func (l *LoginJWTMiddlewareBuilder) IgnorePaths(path string) *LoginJWTMiddlewareBuilder { l.paths = append(l.paths, path) return l }
func (l *LoginJWTMiddlewareBuilder) Build() gin.HandlerFunc { return func(ctx *gin.Context) { for _, path := range l.paths { if ctx.Request.URL.Path == path { return } } tokenHeader := ctx.GetHeader("Authorization") if tokenHeader == "" { ctx.AbortWithStatus(http.StatusUnauthorized) return } segs := strings.Split(tokenHeader, ".") if len(segs) != 2 { ctx.AbortWithStatus(http.StatusUnauthorized) return } tokenStr := segs[1] token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { return []byte("qOYZLAuWmwkxAKG6bijwru9ghNNS9rHc"), nil }) if err != nil { ctx.AbortWithStatus(http.StatusUnauthorized) return } if token == nil || !token.Valid { ctx.AbortWithStatus(http.StatusUnauthorized) return } } }
|
6.7 JWT携带数据
前置:定义 Claims 结构体
为什么需要自定义 Claims?
用 jwt.MapClaims 存 userId,但它是 map[string]interface{}——取值时需要知道 key 和类型,没有编译期检查。用自定义结构体解决。
1 2 3 4
| type UserClaims struct { jwt.RegisteredClaims Uid int64 }
|
jwt.RegisteredClaims 提供了 ExpiresAt,Uid 是自己要携带的业务数据。
前置:改造 CORS 中间件
之前 CORS 配置里没允许 Authorization 头,需要加上:
1 2 3 4 5 6 7
| server.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:3000"}, AllowMethods: []string{"POST", "GET", "OPTIONS"}, AllowHeaders: []string{"Content-Type", "Authorization"}, AllowCredentials: true, MaxAge: 12 * time.Hour, }))
|
步骤一:登录时签发 —— 把 userId 塞进 token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func (u *UserHandler) LoginJWT(ctx *gin.Context) { user, err := u.svc.Login(ctx, req.Email, req.Password)
claims := UserClaims{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute)), }, Uid: user.Id, }
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) tokenStr, _ := token.SignedString([]byte("密钥"))
ctx.Header("x-jwt-token", tokenStr) ctx.String(http.StatusOK, "登录成功") }
|
token 解码后长这样:
1 2 3
| Header: {"alg":"HS512","typ":"JWT"} Payload: {"Uid":1,"exp":1719480000} ← userId 就在里面 Signature: 密钥签名
|
和 Session 的核心区别:Session 版是把 userId 存 Redis,JWT 版是直接写在 token 里给前端。
步骤二:中间件验签 —— 从 token 里取出 userId
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func (l *LoginJWTMiddlewareBuilder) Build() gin.HandlerFunc { return func(ctx *gin.Context) { tokenHeader := ctx.GetHeader("Authorization") segs := strings.SplitN(tokenHeader, " ", 2) tokenStr := segs[1]
claims := &web.UserClaims{} token, _ := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) { return []byte("密钥"), nil }, )
ctx.Set("claims", claims) ctx.Set("userId", claims.Uid) } }
|
验签通过后,claims.Uid 就是登录时塞进去的 user.Id。
步骤三:Handler 使用 —— 直接取 claims
1 2 3 4 5 6
| func (u *UserHandler) ProfileJWT(ctx *gin.Context) { c, _ := ctx.Get("claims") claims := c.(*UserClaims) println(claims.Uid) }
|
不用查 Redis,不用查 Session,中间件已经把 userId 放好了,直接拿。
完整链路
1 2 3 4 5 6 7 8 9 10 11
| 登录:POST /users/login → 查数据库验密码 → claims{Uid: 1, ExpiresAt: 1分钟后} → 签名 → token 字符串 → x-jwt-token 响应头返回
后续请求:GET /users/profile → Authorization: Bearer eyJhbG... → 中间件验签 → claims{Uid: 1} → ctx.Set("userId", 1) → ProfileJWT → ctx.Get("claims") → 拿到 userId
|
JWT 刷新
1 2 3 4 5 6 7 8 9
| if claims.ExpiresAt.Sub(now) < time.Second*50 { claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute)) tokenStr, _ = token.SignedString([]byte("密钥")) if err!=nil{ log.println("jwt token刷新失败") } ctx.Header("x-jwt-token", tokenStr) }
|
前端从响应头拿新 token,替换旧的,实现”滑动过期”。
和 Session 版的关键区别:JWT 的 userId 在 token 里(客户端自证),不需要 Redis 存状态。
6.8 JWT vs Session
|
Session |
JWT |
| 状态 |
有状态(服务端存数据) |
无状态(数据在 Token 里) |
| 扩展性 |
需要共享存储(Redis) |
天然支持多实例 |
| 安全性 |
Session ID 被盗 = 身份被盗 |
Token 被盗 = 身份被盗(有效期短可缓解) |
| 数据量 |
随意存 |
不适合大量数据(Token 会很长) |
| 吊销 |
服务端删掉即可 |
需要黑名单机制 |
实际项目中经常混用:JWT 存基本信息 + Session 存敏感数据。

6.9 JWT登录全流程



七、核心概念速查
| 概念 |
一句话解释 |
| Store 接口 |
Session 数据的存储抽象,内存/Redis/数据库都能实现 |
| Memstore |
基于进程内存,单实例可用,多实例数据不互通 |
| 面向接口 |
依赖接口不依赖实现,换 Store 只改一行初始化代码 |
| MaxAge |
Cookie/Session 过期时间,设负数即删除 |
| 滑动刷新 |
用户活跃时自动延长 Session 有效期 |
| JWT 三段式 |
Header.Payload.Signature,. 分割 |
| Signature |
用密钥对前两段签名,保证数据未被篡改 |
Bearer 前缀 |
JWT 放在 Authorization 头的标准格式 |