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:
@@ -70,6 +70,27 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh 以当前登录身份签发新令牌(用于前端定时续期,避免长挂机后令牌过期掉线)。
|
||||
// 以数据库中的真实角色/租户为准,防止携带过期角色的令牌被无限续期。
|
||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
var user model.User
|
||||
if err := model.DB.Select("id", "tenant_id", "role", "status").First(&user, userID).Error; err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "账号状态已变化,请重新登录"})
|
||||
return
|
||||
}
|
||||
if status, message := middleware.ValidateUserAccess(user.ID, user.TenantID, user.Role); status != 0 {
|
||||
c.JSON(status, gin.H{"code": status, "message": message})
|
||||
return
|
||||
}
|
||||
token, err := middleware.GenerateToken(user.ID, user.TenantID, user.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "生成token失败"})
|
||||
return
|
||||
}
|
||||
middleware.JSON(c, gin.H{"token": token})
|
||||
}
|
||||
|
||||
// Me 当前登录用户信息。
|
||||
func (h *AuthHandler) Me(c *gin.Context) {
|
||||
var user model.User
|
||||
|
||||
@@ -56,10 +56,12 @@ func SetupRoutes(r *gin.Engine, store storage.ObjectStorage, storageCfg config.S
|
||||
authRequired := api.Group("")
|
||||
authRequired.Use(middleware.AuthRequired())
|
||||
{
|
||||
// 当前用户
|
||||
authRequired.GET("/me", auth.Me)
|
||||
authRequired.GET("/me/permissions", auth.MePermissions)
|
||||
authRequired.PUT("/me/status", auth.UpdatePresence)
|
||||
// 当前用户
|
||||
authRequired.GET("/me", auth.Me)
|
||||
authRequired.GET("/me/permissions", auth.MePermissions)
|
||||
authRequired.PUT("/me/status", auth.UpdatePresence)
|
||||
// 令牌续期(前端定时调用,防止长挂机后过期掉线)
|
||||
authRequired.POST("/auth/refresh", auth.Refresh)
|
||||
|
||||
// WebSocket
|
||||
authRequired.GET("/ws", wsHandler.Connect)
|
||||
|
||||
@@ -36,7 +36,7 @@ func setupRouter(t *testing.T) *gin.Engine {
|
||||
t.Fatalf("迁移测试数据库失败: %v", err)
|
||||
}
|
||||
model.DB = db
|
||||
middleware.InitJWT("test-secret")
|
||||
middleware.InitJWT("test-secret", time.Hour)
|
||||
router := gin.New()
|
||||
mem := storage.NewMemory("http://localhost:9100/kefu")
|
||||
handler.SetupRoutes(router, mem, config.StorageConfig{
|
||||
|
||||
Reference in New Issue
Block a user