fix(jwt): 修复仅配置小时时会话提前失效问题

- 将 jwt.access_token_expire_minutes 默认值改为 0,未显式配置时回退 expire_hour

- 调整配置校验为允许 0,仅拒绝负数并补充优先级注释

- 新增配置与认证服务单元测试,覆盖分钟优先与小时回退场景

- 更新示例配置文档,明确分钟/小时优先级与默认行为
This commit is contained in:
yangjianbo
2026-02-22 17:37:35 +08:00
parent 0b32f61062
commit c67f02eaf0
5 changed files with 121 additions and 9 deletions

View File

@@ -662,8 +662,9 @@ type OpsMetricsCollectorCacheConfig struct {
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpireHour int `mapstructure:"expire_hour"`
// AccessTokenExpireMinutes: Access Token有效期分钟默认15分钟
// 短有效期减少被盗用风险配合Refresh Token实现无感续期
// AccessTokenExpireMinutes: Access Token有效期分钟
// - >0: 使用分钟配置(优先级高于 ExpireHour
// - =0: 回退使用 ExpireHour向后兼容旧配置
AccessTokenExpireMinutes int `mapstructure:"access_token_expire_minutes"`
// RefreshTokenExpireDays: Refresh Token有效期默认30天
RefreshTokenExpireDays int `mapstructure:"refresh_token_expire_days"`
@@ -1047,9 +1048,9 @@ func setDefaults() {
// JWT
viper.SetDefault("jwt.secret", "")
viper.SetDefault("jwt.expire_hour", 24)
viper.SetDefault("jwt.access_token_expire_minutes", 360) // 6小时Access Token有效期
viper.SetDefault("jwt.refresh_token_expire_days", 30) // 30天Refresh Token有效期
viper.SetDefault("jwt.refresh_window_minutes", 2) // 过期前2分钟开始允许刷新
viper.SetDefault("jwt.access_token_expire_minutes", 0) // 0 表示回退到 expire_hour
viper.SetDefault("jwt.refresh_token_expire_days", 30) // 30天Refresh Token有效期
viper.SetDefault("jwt.refresh_window_minutes", 2) // 过期前2分钟开始允许刷新
// TOTP
viper.SetDefault("totp.encryption_key", "")
@@ -1343,8 +1344,8 @@ func (c *Config) Validate() error {
slog.Warn("jwt.expire_hour is high; consider shorter expiration for security", "expire_hour", c.JWT.ExpireHour)
}
// JWT Refresh Token配置验证
if c.JWT.AccessTokenExpireMinutes <= 0 {
return fmt.Errorf("jwt.access_token_expire_minutes must be positive")
if c.JWT.AccessTokenExpireMinutes < 0 {
return fmt.Errorf("jwt.access_token_expire_minutes must be non-negative")
}
if c.JWT.AccessTokenExpireMinutes > 720 {
slog.Warn("jwt.access_token_expire_minutes is high; consider shorter expiration for security", "access_token_expire_minutes", c.JWT.AccessTokenExpireMinutes)

View File

@@ -124,6 +124,36 @@ func TestLoadDefaultServerMode(t *testing.T) {
}
}
func TestLoadDefaultJWTAccessTokenExpireMinutes(t *testing.T) {
resetViperWithJWTSecret(t)
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if cfg.JWT.ExpireHour != 24 {
t.Fatalf("JWT.ExpireHour = %d, want 24", cfg.JWT.ExpireHour)
}
if cfg.JWT.AccessTokenExpireMinutes != 0 {
t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 0", cfg.JWT.AccessTokenExpireMinutes)
}
}
func TestLoadJWTAccessTokenExpireMinutesFromEnv(t *testing.T) {
resetViperWithJWTSecret(t)
t.Setenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "90")
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if cfg.JWT.AccessTokenExpireMinutes != 90 {
t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 90", cfg.JWT.AccessTokenExpireMinutes)
}
}
func TestLoadDefaultDatabaseSSLMode(t *testing.T) {
resetViperWithJWTSecret(t)
@@ -735,6 +765,11 @@ func TestValidateConfigErrors(t *testing.T) {
mutate: func(c *Config) { c.JWT.ExpireHour = 200 },
wantErr: "jwt.expire_hour must be <= 168",
},
{
name: "jwt access token expire minutes non-negative",
mutate: func(c *Config) { c.JWT.AccessTokenExpireMinutes = -1 },
wantErr: "jwt.access_token_expire_minutes must be non-negative",
},
{
name: "csp policy required",
mutate: func(c *Config) { c.Security.CSP.Enabled = true; c.Security.CSP.Policy = "" },