feat(auth): JWT 滑动续期 + 401 自动登出,修复长挂机莫名掉线
- 后端:JWT 有效期可配(JWT_EXPIRE_HOURS,默认 24h);令牌寿命过半时经 X-Renewed-Token 响应头滑动续期;新增 POST /api/auth/refresh 以数据库真实身份签发 - 前端 request 层:静默接管续期头并同步 localStorage;任何 401(登录/续期除外) 统一广播会话过期事件 - auth store:监听过期事件清理登录态并跳转 /login?expired=1(不再卡 403 页); 每 10 分钟定时续期,避免 WS 重连时令牌已过期 - 新增滑动续期单元测试(回签过期时间戳),go test -race 通过
This commit is contained in:
@@ -13,12 +13,21 @@ import (
|
||||
"kefu-cloud/server/internal/model"
|
||||
)
|
||||
|
||||
var jwtSecret []byte
|
||||
var (
|
||||
jwtSecret []byte
|
||||
jwtExpire = 24 * time.Hour
|
||||
)
|
||||
|
||||
func InitJWT(secret string) {
|
||||
func InitJWT(secret string, expire time.Duration) {
|
||||
jwtSecret = []byte(secret)
|
||||
if expire > 0 {
|
||||
jwtExpire = expire
|
||||
}
|
||||
}
|
||||
|
||||
// TokenExpire 返回令牌有效期时长。
|
||||
func TokenExpire() time.Duration { return jwtExpire }
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
TenantID uint `json:"tenant_id"`
|
||||
@@ -32,7 +41,7 @@ func GenerateToken(userID, tenantID uint, role string) (string, error) {
|
||||
TenantID: tenantID,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpire)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
@@ -125,6 +134,17 @@ func AuthRequired() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 滑动续期:令牌剩余寿命不足一半时签发新令牌,前端经 X-Renewed-Token 静默替换,
|
||||
// 避免活跃用户在请求间隙令牌过期而"莫名掉线"。
|
||||
if issuedAt := claims.IssuedAt; issuedAt != nil {
|
||||
elapsed := time.Since(issuedAt.Time)
|
||||
if elapsed >= jwtExpire/2 {
|
||||
if renewed, err := GenerateToken(claims.UserID, claims.TenantID, claims.Role); err == nil {
|
||||
c.Header("X-Renewed-Token", renewed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("tenant_id", claims.TenantID)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
Reference in New Issue
Block a user