GinFast 开源后台管理框架的 Token 管理业务逻辑及源码解析

项目地址

概述

本文档详细分析GinFast项目中的Token管理系统,涵盖前后端JWT令牌的生成、验证、刷新、撤销等核心业务逻辑,以及相关的源码实现。包括后端Go服务和前端Vue应用的完整Token管理流程。

系统架构

后端核心组件

  1. TokenService - 令牌服务核心实现
  2. JWTAuthMiddleware - JWT认证中间件
  3. AuthController - 认证控制器
  4. TokenHelper - 令牌工具类

前端核心组件

  1. Http拦截器 - 自动Token管理和刷新
  2. Auth工具类 - Token存储和格式化
  3. 用户状态管理 - Token状态维护

技术栈

后端技术

  • JWT (JSON Web Token) - 用于身份认证
  • Redis/Memory - 可配置的缓存后端,用于令牌缓存和撤销管理
  • Gin - Web框架
  • jwt-go - JWT库

前端技术

  • Axios - HTTP客户端
  • Vue 3 + TypeScript - 前端框架
  • js-cookie - Cookie管理
  • Pinia - 状态管理

核心业务逻辑

1. 令牌类型

Access Token

  • 用途: API访问认证
  • 过期时间: 可配置 (默认12小时/43200秒)
  • 存储方式: 可选缓存模式 (通过配置控制)
  • 格式: JWT
  • 前端存储: Cookie (gin-fast-access-token)

Refresh Token

  • 用途: 刷新Access Token
  • 过期时间: 可配置 (默认30天/2592000秒)
  • 存储方式: 一定会缓存 (Redis或内存缓存)
  • 格式: JWT
  • 前端存储: Cookie (gin-fast-refresh-token)

2. 认证流程

登录流程

// 后端流程
// 1. 用户提交用户名、密码、验证码
// 2. 验证用户身份和租户权限
// 3. 生成Access Token和Refresh Token
// 4. 返回令牌对给客户端
// 前端流程
// 1. 用户填写登录表单
// 2. 调用/login接口获取令牌对
// 3. 将令牌存储到Cookie
// 4. 更新用户状态

令牌刷新流程

// 后端流程
// 1. 客户端提交Refresh Token
// 2. 验证Refresh Token有效性
// 3. 生成新的Access Token
// 4. 轮换Refresh Token(保持剩余有效期)
// 5. 返回新的令牌对
// 前端流程
// 1. 检测Access Token即将过期(剩余3秒)
// 2. 自动调用/refreshToken接口
// 3. 更新Cookie中的令牌信息
// 4. 重试待处理的请求

登出流程

// 后端流程
// 1. 撤销当前Access Token
// 2. 撤销Refresh Token
// 3. 清除相关缓存
// 前端流程
// 1. 调用登出接口
// 2. 清除Cookie中的令牌
// 3. 清除用户状态
// 4. 跳转到登录页

源码解析

1. 后端TokenService实现

核心结构体

type TokenService struct {
    // Ctx 上下文,用于缓存操作
    Ctx            context.Context
    
    // RedisHelper 缓存接口,支持Redis和内存两种实现
    // 通过配置 server.cachetype 控制使用哪种缓存后端
    RedisHelper    app.CacheInterf
    
    // JWTSecret JWT签名密钥,用于Token的加密和解密
    // 通过配置 token.jwttokensignkey 设置
    JWTSecret      string
    
    // TokenExpire Access Token过期时间,单位秒
    // 通过配置 token.jwttokenexpire 设置,默认12小时(43200秒)
    TokenExpire    time.Duration
    
    // RefreshExpire Refresh Token过期时间,单位秒
    // 通过配置 token.jwttokenrefreshexpire 设置,默认30天(2592000秒)
    RefreshExpire  time.Duration
    
    // CacheKeyPrefix 缓存键前缀,用于区分不同应用的缓存
    // 通过配置 token.cachekeyprefix 设置,默认"gin-fast:"
    CacheKeyPrefix string
    
    // IsCache 是否开启Access Token缓存
    // 通过配置 token.isCache 控制:
    // - true: 开启缓存,支持Token立即撤销,安全性更高
    // - false: 关闭缓存,性能更高,但无法立即撤销Token
    IsCache        bool
}

主要方法

GenerateTokenWithCache - 生成带缓存的Access Token

func (s *TokenService) GenerateTokenWithCache(user *app.ClaimsUser) (string, error) {
    // 1. 生成JWT令牌
    tokenString, err := s.GenerateToken(user)
    // 2. 如果开启缓存,存储到Redis
    if s.IsCache {
        tokenInfo := &app.TokenInfo{
            UserID:    user.UserID,
            Token:     tokenString,
            ExpiresAt: time.Now().Add(s.TokenExpire * time.Second),
            CreatedAt: time.Now(),
        }
        err = s.storeTokenWithCache(tokenInfo)
    }
    return tokenString, nil
}

ValidateTokenWithCache - 验证带缓存的Token

func (s *TokenService) ValidateTokenWithCache(tokenString string) (*app.Claims, error) {
    // 1. 验证JWT签名
    claims, err := s.ParseToken(tokenString)
    // 2. 检查缓存中是否存在该token(白名单模式)
    if s.IsCache {
        key := s.getTokenKeyWithCache(claims.UserID, tokenString)
        exists, err := s.RedisHelper.Exists(s.Ctx, key)
        if err != nil || exists == 0 {
            return nil, errors.New("token not found")
        }
    }
    return claims, nil
}

RotateRefreshToken - 轮换Refresh Token

func (s *TokenService) RotateRefreshToken(oldRefreshToken string) (string, error) {
    // 1. 验证旧的refresh token
    claims, err := s.ValidateRefreshToken(oldRefreshToken)
    // 2. 计算剩余有效时间
    remainingDuration := claims.ExpiresAt.Time.Sub(now)
    // 3. 撤销旧的refresh token
    s.RevokeRefreshToken(claims.UserID)
    // 4. 生成新的refresh token,使用剩余的有效时间
    expirationTime := now.Add(remainingDuration)
    // 5. 存储新的refresh token
    s.storeRefreshToken(newRefreshTokenInfo)
    return newTokenString, nil
}

2. 前端Http拦截器实现

请求拦截器 - 自动Token刷新

private httpInterceptorsRequest(): void {
    Http.axiosInstance.interceptors.request.use(
        async (config: HttpRequestConfig): Promise<any> => {
            // 请求白名单,不需要token的接口
            const whiteList = ["/refreshToken", "/login", "/captcha/id", "/captcha/image"];
            return whiteList.some(url => config.url?.includes(url))
                ? config
                : new Promise(resolve => {
                    const data = getAccessToken();
                    const refreshTokenData = getRefreshToken();
                    // 过期时间小于3秒时即认为过期
                    const expired = !data || data.accessTokenExpires * 1000 - Date.now() <= 3000;
                    if (expired && refreshTokenData) {
                        // 过期处理 - 自动刷新Token
                        if (!Http.isRefreshing) {
                            Http.isRefreshing = true;
                            // token过期刷新
                            useUserStoreHook()
                                .handRefreshToken(refreshTokenData.refreshToken)
                                .then((res: any) => {
                                    const token = res.data.accessToken;
                                    if (config.headers) {
                                        config.headers["Authorization"] = formatToken(token);
                                    }
                                    Http.requests.forEach(cb => cb(token));
                                    Http.requests = [];
                                })
                                .finally(() => {
                                    Http.isRefreshing = false;
                                });
                        }
                        resolve(Http.retryOriginalRequest(config));
                    } else {
                        // 未过期 - 正常添加Token
                        if (config.headers && data?.accessToken) {
                            config.headers["Authorization"] = formatToken(data.accessToken);
                        }
                        resolve(config);
                    }
                });
        }
    );
}

响应拦截器 - 错误处理

private httpInterceptorsResponse(): void {
    instance.interceptors.response.use(
        (response: HttpResponse) => {
            return response.data;
        },
        (error: HttpError) => {
            const status = error.response?.status;
            const config = error.config;
            // 刷新token的API报错时提示跳转登录页
            if (config?.url?.includes("/refreshToken")) {
                throttledModalConfirm({
                    title: '提示',
                    content: '登录状态已过期,请重新登录',
                    onOk: () => {
                        Http.redirectLoginPage();
                    }
                });
            } else if (status === 401) {
                // 401错误时,提示用户刷新页面重试
                throttledModalConfirm({
                    title: '提示',
                    content: '登录状态已过期,请刷新页面重试',
                    onOk: () => {
                        window.location.reload();
                    }
                });
            }
            return Promise.reject(error);
        }
    );
}

3. 前端Auth工具类

Token存储管理

export interface AccessTokenData {
    accessToken: string;
    accessTokenExpires: number; // 时间戳
}

export interface RefreshTokenData {
    refreshToken: string;
    refreshTokenExpires: number; // 时间戳
}

// Token存储到Cookie
export function setAccessToken(accessToken: string, accessTokenExpires: number) {
    const cookieString = JSON.stringify({
        accessToken: accessToken,
        accessTokenExpires
    });
    Cookies.set(AccessTokenKey, cookieString, {
        expires: (accessTokenExpires * 1000 - Date.now()) / 86400000
    });
}

export function setRefreshToken(refreshToken: string, refreshTokenExpires: number) {
    const cookieString = JSON.stringify({
        refreshToken: refreshToken,
        refreshTokenExpires
    });
    Cookies.set(RefreshTokenKey, cookieString, {
        expires: (refreshTokenExpires * 1000 - Date.now()) / 86400000
    });
}

// Token格式化
export const formatToken = (token: string): string => {
    return "Bearer " + token;
};

4. 后端JWT认证中间件

JWTAuthMiddleware

func JWTAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从Header获取Access Token
        tokenString, err := common.GetAccessToken(c)
        // 2. 验证Token有效性
        claims, err := app.TokenService.ValidateTokenWithCache(tokenString)
        // 3. 将用户信息存储到上下文
        c.Set(consts.BindContextKeyName, claims)
        // 4. 继续处理请求
        c.Next()
    }
}

5. 后端认证控制器

Login方法

func (ac *AuthController) Login(c *gin.Context) {
    // 1. 验证请求参数
    var req models.LoginRequest
    req.Validate(c)
    
    // 2. 查询用户信息
    user := models.NewUser()
    user.Find(c, func(d *gorm.DB) *gorm.DB {
        return d.Where("username = ?", req.Username).Preload("Tenant")
    })
    
    // 3. 验证租户权限
    tenantID, tenantCode := ac.validateTenant(user, req.TenantCode)
    
    // 4. 密码验证和登录锁定机制
    ac.validatePassword(user, req.Password, req.Username)
    
    // 5. 生成令牌对
    token, _ := app.TokenService.GenerateTokenWithCache(&app.ClaimsUser{
        UserID:     user.ID,
        Username:   user.Username,
        TenantID:   tenantID,
        TenantCode: tenantCode,
    })
    
    refreshToken, _ := app.TokenService.GenerateRefreshToken(user.ID)
    
    // 6. 返回响应
    ac.Success(c, gin.H{
        "accessToken":         token,
        "accessTokenExpires":  claims.ExpiresAt.Unix(),
        "refreshToken":        refreshToken,
        "refreshTokenExpires": claims1.ExpiresAt.Unix(),
    })
}

RefreshToken方法

func (ac *AuthController) RefreshToken(c *gin.Context) {
    // 1. 获取Refresh Token
    refreshToken := c.GetHeader("RefreshToken")
    // 2. 解析Refresh Token获取用户ID
    claims, err := app.TokenService.ParseRefreshToken(refreshToken)
    // 3. 查询用户信息
    var user models.User
    app.DB().WithContext(c).First(&user, claims.UserID)
    // 4. 刷新Access Token
    newAccessToken, err := app.TokenService.RefreshAccessTokenWithCache(refreshToken, &app.ClaimsUser{
        UserID:   user.ID,
        Username: user.Username,
    })
    // 5. 轮换Refresh Token
    newRefreshToken, err := app.TokenService.RotateRefreshToken(refreshToken)
    // 6. 返回新的令牌对
    ac.Success(c, gin.H{
        "accessToken":         newAccessToken,
        "accessTokenExpires":  claims1.ExpiresAt.Unix(),
        "refreshToken":        newRefreshToken,
        "refreshTokenExpires": newRefreshClaims.ExpiresAt.Unix(),
    })
}

配置管理

Token相关配置

token:
  jwttokensignkey: "gin-fast"           # JWT签名密钥
  jwttokenexpire: 43200                 # Access Token过期时间(12小时),可配置
  jwttokenrefreshexpire: 2592000        # Refresh Token过期时间(30天),可配置
  cachekeyprefix: "gin-fast:"           # 缓存前缀
  isCache: false                        # 是否开启Access Token缓存

缓存配置

server:
  cachetype: "redis" # 缓存模式 memory OR redis

缓存策略

1. 缓存后端配置

系统支持两种缓存后端:

Redis缓存

  • 特点: 分布式、持久化、适合生产环境
  • 配置: 通过Redis配置连接
  • 适用场景: 多实例部署、高可用环境

内存缓存

  • 特点: 高性能、单机、重启丢失
  • 配置: 无需额外配置
  • 适用场景: 开发环境、单机部署

2. Token缓存策略

Access Token缓存

  • 可配置: 通过 isCache 配置控制是否缓存
  • 缓存模式: 白名单模式,缓存中存在的Token才有效
  • 优势: 支持立即Token撤销
  • 性能: 缓存模式下性能略低,但安全性更高

Refresh Token缓存

  • 强制缓存: Refresh Token一定会被缓存
  • 存储方式: 每个用户只有一个有效的Refresh Token
  • 优势: 支持Token轮换和立即撤销

3. 缓存键设计

Access Token Key

func (s *TokenService) getTokenKeyWithCache(userID uint, tokenString string) string {
    hash := md5.Sum([]byte(tokenString))
    tokenHash := fmt.Sprintf("%x", hash)[:8]
    return fmt.Sprintf("%stoken:%d:%s", s.CacheKeyPrefix, userID, tokenHash)
}

Refresh Token Key

func (s *TokenService) getRefreshTokenKey(userID uint) string {
    return fmt.Sprintf("%srefresh_token:%d", s.CacheKeyPrefix, userID)
}

安全特性

1. 登录锁定机制

safe:
  loginlockthreshold: 3    # 密码错误锁定阈值
  loginlockexpire: 60      # 失败次数记录缓存时间
  loginlockduration: 600   # 账号锁定时长

2. Token安全

  • JWT签名: 使用HS256算法
  • 密钥配置: 通过配置文件管理
  • 过期时间: 合理的过期时间设置
  • Token轮换: Refresh Token轮换机制

3. 租户隔离

Token中包含租户信息,实现多租户系统的权限隔离:

type ClaimsUser struct {
    UserID     uint   `json:"userId"`
    Username   string `json:"username"`
    TenantID   uint   `json:"tenantId,omitempty"`
    TenantCode string `json:"tenantCode,omitempty"`
}

TokenService使用灵活性

1. 独立调用模式

TokenService设计为独立的服务组件,开发者可以根据需要选择使用方式:

仅使用基础Token功能

// 仅生成和验证Access Token,不使用Refresh Token机制
token, err := app.TokenService.GenerateToken(user)
claims, err := app.TokenService.ValidateToken(tokenString)

使用完整Token管理

// 使用完整的Token管理,包括Refresh Token
token, err := app.TokenService.GenerateTokenWithCache(user)
refreshToken, err := app.TokenService.GenerateRefreshToken(user.ID)
newToken, err := app.TokenService.RefreshAccessTokenWithCache(refreshToken, user)

2. 缓存配置灵活性

Access Token缓存可选

  • 开启缓存: 支持立即Token撤销,安全性更高
  • 关闭缓存: 性能更高,但无法立即撤销Token
  • 配置控制: 通过 isCache 配置项控制

Refresh Token强制缓存

  • 必须缓存: 确保Refresh Token的安全管理
  • 单用户单Token: 每个用户只有一个有效的Refresh Token
  • 自动轮换: 支持Token轮换保持安全性

路由配置

公开路由(无需认证)

public := api.Group("")
public.POST("/login", middleware.CaptchaMiddleware(), authControllers.Login)
public.POST("/refreshToken", authControllers.RefreshToken)

受保护路由(需要JWT认证)

protected := api.Group("")
protected.Use(middleware.JWTAuthMiddleware())
protected.Use(middleware.DemoAccountMiddleware())
protected.Use(middleware.CasbinMiddleware())

前后端交互流程

1. 完整的Token生命周期

登录过程

Redis后端前端用户Redis后端前端用户输入用户名密码POST /login验证用户身份生成Access Token生成Refresh Token存储Refresh Token返回令牌对存储到Cookie登录成功

Token自动刷新过程

Redis后端前端Redis后端前端检测Token即将过期(3秒)POST /refreshToken验证Refresh Token生成新Access Token轮换Refresh Token更新Refresh Token返回新令牌对更新Cookie重试待处理请求

2. 错误处理机制

 

前端错误处理策略

  • Token刷新失败: 跳转登录页
  • 401错误: 提示刷新页面
  • 网络错误: 友好提示
  • 重复刷新保护: 防止并发刷新

后端错误处理策略

  • Token无效: 返回401状态码
  • Refresh Token过期: 要求重新登录
  • 租户权限不足: 返回403状态码
  • 登录锁定: 返回429状态码

最佳实践

1. Token管理

  • 前后端协同: 前端自动刷新,后端安全验证
  • 双Token机制: Access Token(12h) + Refresh Token(30d)
  • 智能过期检测: 前端提前3秒检测Token过期
  • Token轮换: 保持Refresh Token剩余有效期

2. 安全性

  • 登录锁定: 防止暴力破解
  • 多租户隔离: Token包含租户信息
  • Token撤销: 支持立即失效
  • 白名单机制: 防止Token过期死循环

3. 性能优化

  • 缓存模式可选: 平衡安全性和性能
  • 请求队列: 防止重复刷新
  • 节流控制: 防止频繁跳转登录页
  • 高效验证: JWT签名验证 + 缓存检查

4. 用户体验

  • 无感刷新: 用户无需手动操作
  • 友好提示: 清晰的错误信息
  • 状态保持: 刷新页面后保持登录状态
  • 重试机制: 自动重试待处理请求

总结

GinFast项目的Token管理系统设计完善,具备以下特点:

后端优势

  1. 安全性高: 支持Token撤销、登录锁定、多租户隔离
  2. 灵活性好: 支持可配置的缓存模式(Redis/内存)
  3. 扩展性强: 清晰的接口设计和模块化架构
  4. 配置灵活: Token过期时间可配置,缓存策略可选

前端优势

  1. 用户体验佳: 提供Token自动刷新机制
  2. 错误处理完善: 多种错误场景的友好处理
  3. 性能优化: 请求队列和节流控制

前后端协同

  1. 完整生命周期: 从登录到登出的完整流程
  2. 智能刷新: 前端自动检测和刷新Token
  3. 状态同步: Cookie存储与后端验证同步

核心特性

  1. 缓存策略灵活: Access Token缓存可选,Refresh Token强制缓存
  2. TokenService独立: 可单独调用基础功能或完整管理功能
  3. 多缓存后端: 支持Redis和内存两种缓存后端
  4. 配置驱动: 所有关键参数均可通过配置文件调整

该系统能够满足企业级应用的安全认证需求,为多租户SaaS应用提供了可靠的Token管理解决方案,实现了前后端无缝协作的安全认证体系。

关键源码文件

后端核心文件

前端核心文件

优秀的个人博客,低调大师

微信关注我们

转载内容版权归作者及来源网站所有!本站原创内容转载请注明来源!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

相关文章

发表评论

资源下载

更多资源
优质分享Android(本站安卓app)

优质分享Android(本站安卓app)

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario,低调大师唯一一个Java游戏作品

Mario,低调大师唯一一个Java游戏作品

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Apache Tomcat7、8、9(Java Web服务器)

Apache Tomcat7、8、9(Java Web服务器)

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。