GinFast 开源后台管理框架的 Token 管理业务逻辑及源码解析
项目地址
概述
本文档详细分析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生命周期
登录过程
Token自动刷新过程
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工具类

