GinFast 开源后台管理框架的 Token 管理业务逻辑及源码解析
项目地址 后端项目: https://github.com/qxkjsoft/ginfast 前端项目: https://github.com/qxkjsoft/ginfast-ui 概述 本文档详细分析GinFast项目中的Token管理系统,涵盖前后端JWT令牌的生成、验证、刷新、撤销等核心业务逻辑,以及相关的源码实现。包括后端Go服务和前端Vue应用的完整Token管理流程。 系统架构 后端核心组件 TokenService - 令牌服务核心实现 JWTAuthMiddleware - JWT认证中间件 AuthController - 认证控制器 TokenHelper - 令牌工具类 前端核心组件 Http拦截器 - 自动Token管理和刷新 Auth工具类 - Token存储和格式化 用户状态管理 - 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管理系统设计完善,具备以下特点: 后端优势 安全性高: 支持Token撤销、登录锁定、多租户隔离 灵活性好: 支持可配置的缓存模式(Redis/内存) 扩展性强: 清晰的接口设计和模块化架构 配置灵活: Token过期时间可配置,缓存策略可选 前端优势 用户体验佳: 提供Token自动刷新机制 错误处理完善: 多种错误场景的友好处理 性能优化: 请求队列和节流控制 前后端协同 完整生命周期: 从登录到登出的完整流程 智能刷新: 前端自动检测和刷新Token 状态同步: Cookie存储与后端验证同步 核心特性 缓存策略灵活: Access Token缓存可选,Refresh Token强制缓存 TokenService独立: 可单独调用基础功能或完整管理功能 多缓存后端: 支持Redis和内存两种缓存后端 配置驱动: 所有关键参数均可通过配置文件调整 该系统能够满足企业级应用的安全认证需求,为多租户SaaS应用提供了可靠的Token管理解决方案,实现了前后端无缝协作的安全认证体系。 关键源码文件 后端核心文件 app/utils/tokenhelper/tokenhelper.go - Token服务实现 app/middleware/jwt.go - JWT认证中间件 app/controllers/auth.go - 认证控制器 app/routes/routes.go - 路由配置 app/utils/cachehelper/ - 缓存实现(Redis和内存) 前端核心文件 src\\utils\\http\\index.ts - HTTP拦截器 src\\utils\\auth.ts - Token工具类