Go + Gin实现双Token管理员登录的示例代码

Written by

in

文章目录
  • 传统单 Token(JWT)架构下,“续签”与“强制失效” 是一对矛盾: 单 Token 痛点 双 Token 解法 Token 过期后需重新登录,体验差 Access-Token 失效后,用 Refresh-Token 无感刷新 Token 一旦泄露,在有效期内无法撤销 Refresh-Token 存 Redis,可一键踢人 续签逻辑侵入业务代码 续签、校验、踢人全部封装在 Auth 中间件 本文将用 Gin + GORM + Redis 带你落地一套生产可用的「管理员双 Token」登录体系。
  • ┌────────────┐ ┌──────────────┐ ┌──────────┐ │ Web │──────►│ Handler │──────►│ Logic │ └────────────┘ └──────────────┘ └──────────┘ ▲ ▲ │ │ │ ▼ │ ┌──────────────┐ ┌──────────┐ │ │ Response │ │ Repo │ └────────────┘ Helper │ └──────────┘ │ Redis / MySQL Handler 负责参数校验、鉴权前置检查 Logic 处理核心业务:登录、刷新、退出、获取用户信息 Repo 封装数据访问:MySQL 查管理员、Redis 存 Refresh-Token Response 统一封装返回格式,避免样板代码
  • 前端收到 401 Unauthorized 后,携带 Refresh-Token 调 /admin/refresh: // 伪代码(Handler 略) refreshToken := c.GetHeader(“X-Refresh-Token”) adminID, err := usersignutil.ParseRefreshToken(refreshToken) if err != nil { /* 无效 Refresh-Token */ } // 与 Redis 比对 saved, _ := repo.GetRefreshToken(ctx, adminID) if saved != refreshToken { /* 已被踢出 */ } // 重新颁发 newAccess, _ := usersignutil.GenerateAccessToken(adminID)
  • 细节 实现 密码加密 bcrypt 哈希,不可逆 Token 签名 使用独立 jwtSecret,区分 Access/Refresh Refresh-Token 存储 Redis + TTL,支持热踢人 SQL 注入 GORM 占位符自动防注入 并发登录 Redis Key 覆盖即可实现「后者踢前者」
  • 本文用 200 行核心代码展示了: 分层架构:Handler → Logic → Repo 双 Token:Access-Token(短)+ Refresh-Token(长) 统一响应:封装 response.OK / Fail 消除样板 安全退出:Redis 删除 Refresh-Token 即踢人 到此这篇关于Go + Gin实现双Token管理员登录的示例代码的文章就介绍到这了,更多相关Go Gin 双Token管理员登录内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!  您可能感兴趣的文章: Go语言中双Token登录系统的思路与实现详解
  • 目录
    • 一、为什么要做「双 Token」
    • 二、整体架构
    • 三、核心代码走读
      • 3.1 路由层(Handler)
      • 3.2 业务层(Logic)
      • 3.3 数据访问层(Repo)
    • 四、如何无感刷新 Access-Token
      • 五、安全细节
        • 六、总结

          传统单 Token(JWT)架构下,“续签”与“强制失效” 是一对矛盾:

          单 Token 痛点 双 Token 解法
          Token 过期后需重新登录,体验差 Access-Token 失效后,用 Refresh-Token 无感刷新
          Token 一旦泄露,在有效期内无法撤销 Refresh-Token 存 Redis,可一键踢人
          续签逻辑侵入业务代码 续签、校验、踢人全部封装在 Auth 中间件

          本文将用 Gin + GORM + Redis 带你落地一套生产可用的「管理员双 Token」登录体系。

          ┌────────────┐       ┌──────────────┐       ┌──────────┐
          │   Web      │──────►│  Handler     │──────►│  Logic   │
          └────────────┘       └──────────────┘       └──────────┘
                   ▲                    ▲                      │
                   │                    │                      ▼
                   │            ┌──────────────┐       ┌──────────┐
                   │            │  Response    │       │   Repo   │
                   └────────────┘  Helper      │       └──────────┘
                                               │
                                        Redis / MySQL
          
          • Handler 负责参数校验、鉴权前置检查
          • Logic 处理核心业务:登录、刷新、退出、获取用户信息
          • Repo 封装数据访问:MySQL 查管理员、Redis 存 Refresh-Token
          • Response 统一封装返回格式,避免样板代码

          // NewAdminLoginHandler 管理员登录
          func NewAdminLoginHandler() gin.HandlerFunc {
              return func(c *gin.Context) {
                  var req requests.AdminLoginReq
                  if err := c.ShouldBindJSON(&req); err != nil {
                      response.Fail(c, "参数不合法")
                      return
                  }
          
                  repo := repositories.NewAdminLoginRepository(globals.DB, globals.RDB)
                  logic := logics.NewAdminLoginLogic(repo)
          
                  resp, err := logic.Login(c.Request.Context(), &req)
                  if err != nil {
                      response.Fail(c, fmt.Sprintf("登录失败: %v", err))
                      return
                  }
                  response.OK(c, "登录成功", resp)
              }
          }
          
          • 只做「绑定参数 + 调用逻辑 + 包装返回」
          • 不掺杂任何业务判断,保持 单一职责

          3.2.1 登录

          func (l *AdminLoginLogic) Login(ctx context.Context, req *requests.AdminLoginReq) (*responses.AdminLoginResp, error) {
              admin, err := l.findAdminByAccount(ctx, req.Account)
              if err != nil || admin == nil {
                  return nil, errors.New("账号不存在")
              }
              if admin.Status != 1 {
                  return nil, errors.New("账号已被禁用")
              }
              if !usersignutil.CheckPasswordHash(req.Password, admin.Password) {
                  return nil, errors.New("密码错误")
              }
          
              // 更新最后登录时间
              _ = l.adminRepo.UpdateLastLoginTime(ctx, admin.ID)
          
              // 生成双 Token
              return l.generateTokensAndBuildResponse(ctx, admin)
          }
          

          3.2.2 生成双 Token

          func (l *AdminLoginLogic) generateTokensAndBuildResponse(
              ctx context.Context,
              admin *models.Admin,
          ) (*responses.AdminLoginResp, error) {
          
              accessToken, _ := usersignutil.GenerateAccessToken(admin.ID)
              refreshToken, _ := usersignutil.GenerateRefreshToken(admin.ID)
          
              // 保存 Refresh-Token 到 Redis,设置过期时间
              err := l.adminRepo.SaveRefreshToken(
                  ctx,
                  admin.ID,
                  refreshToken,
                  globals.AppConfig.JWT.RefreshTokenExpiry,
              )
              if err != nil {
                  // 记录日志但不阻断登录
                  globals.Log.Error("SaveRefreshToken err:", err)
              }
          
              resp := (&responses.AdminLoginResp{}).ToResponse(admin)
              resp.AccessToken = accessToken
              resp.RefreshToken = refreshToken
              return resp, nil
          }
          

          3.2.3 获取管理员信息

          func (l *AdminLoginLogic) GetAdminInfo(ctx context.Context, adminID uint) (*responses.AdminInfoResp, error) {
              admin, err := l.adminRepo.FindByIDWithDetails(ctx, adminID)
              if err != nil || admin == nil {
                  return nil, errors.New("管理员不存在")
              }
              if admin.Status != 1 {
                  return nil, errors.New("账号已被禁用")
              }
              return (&responses.AdminInfoResp{}).ToResponse(admin), nil
          }
          

          3.2.4 登出

          func (l *AdminLoginLogic) Logout(ctx context.Context, adminID uint) error {
              return l.adminRepo.DeleteRefreshToken(ctx, adminID)
          }
          

          仅展示关键函数,完整代码已在文章开头给出。

          • FindByPhone / FindByAccountID / FindByIDWithDetails
            利用 GORM 的 Preload 一把连表查,减少 N+1

          • SaveRefreshToken / DeleteRefreshToken
            使用 admin:refresh_token:{id} 作为 Redis Key,天然支持「单设备登录」或「多端互踢」

          前端收到 401 Unauthorized 后,携带 Refresh-Token 调 /admin/refresh

          // 伪代码(Handler 略)
          refreshToken := c.GetHeader("X-Refresh-Token")
          adminID, err := usersignutil.ParseRefreshToken(refreshToken)
          if err != nil { /* 无效 Refresh-Token */ }
          
          // 与 Redis 比对
          saved, _ := repo.GetRefreshToken(ctx, adminID)
          if saved != refreshToken { /* 已被踢出 */ }
          
          // 重新颁发
          newAccess, _ := usersignutil.GenerateAccessToken(adminID)
          

          细节 实现
          密码加密 bcrypt 哈希,不可逆
          Token 签名 使用独立 jwtSecret,区分 Access/Refresh
          Refresh-Token 存储 Redis + TTL,支持热踢人
          SQL 注入 GORM 占位符自动防注入
          并发登录 Redis Key 覆盖即可实现「后者踢前者」

          本文用 200 行核心代码展示了:

          1. 分层架构:Handler → Logic → Repo
          2. 双 Token:Access-Token(短)+ Refresh-Token(长)
          3. 统一响应:封装 response.OK / Fail 消除样板
          4. 安全退出:Redis 删除 Refresh-Token 即踢人

          到此这篇关于Go + Gin实现双Token管理员登录的示例代码的文章就介绍到这了,更多相关Go Gin 双Token管理员登录内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客! 

          您可能感兴趣的文章:

          • Go语言中双Token登录系统的思路与实现详解

          站内搜索