Merge pull request #4666 from TTopoo/redis-username-support

fix(config): support Redis ACL username
This commit is contained in:
Wesley Liddick
2026-07-21 10:43:54 +08:00
committed by GitHub
18 changed files with 77 additions and 0 deletions
+1
View File
@@ -544,6 +544,7 @@ database:
redis:
host: "localhost"
port: 6379
username: ""
password: ""
jwt:
+2
View File
@@ -1403,6 +1403,7 @@ func (d *DatabaseConfig) DSNWithTimezone(tz string) string {
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
// 连接池与超时配置(性能优化:可配置化连接池参数)
@@ -1981,6 +1982,7 @@ func setDefaults() {
// Redis
viper.SetDefault("redis.host", "localhost")
viper.SetDefault("redis.port", 6379)
viper.SetDefault("redis.username", "")
viper.SetDefault("redis.password", "")
viper.SetDefault("redis.db", 0)
viper.SetDefault("redis.dial_timeout_seconds", 5)
+9
View File
@@ -37,6 +37,15 @@ func TestLoadServerTimingConfig(t *testing.T) {
})
}
func TestLoadRedisUsernameFromEnvironment(t *testing.T) {
resetViperWithJWTSecret(t)
t.Setenv("REDIS_USERNAME", "app-user")
cfg, err := Load()
require.NoError(t, err)
require.Equal(t, "app-user", cfg.Redis.Username)
}
func TestLoadHTTPIngressSafetyDefaults(t *testing.T) {
resetViperWithJWTSecret(t)
cfg, err := Load()
+1
View File
@@ -33,6 +33,7 @@ func InitRedis(cfg *config.Config) *redis.Client {
func buildRedisOptions(cfg *config.Config) *redis.Options {
opts := &redis.Options{
Addr: cfg.Redis.Address(),
Username: cfg.Redis.Username,
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
DialTimeout: time.Duration(cfg.Redis.DialTimeoutSeconds) * time.Second, // 建连超时
@@ -13,6 +13,7 @@ func TestBuildRedisOptions(t *testing.T) {
Redis: config.RedisConfig{
Host: "localhost",
Port: 6379,
Username: "app-user",
Password: "secret",
DB: 2,
DialTimeoutSeconds: 5,
@@ -25,6 +26,7 @@ func TestBuildRedisOptions(t *testing.T) {
opts := buildRedisOptions(cfg)
require.Equal(t, "localhost:6379", opts.Addr)
require.Equal(t, "app-user", opts.Username)
require.Equal(t, "secret", opts.Password)
require.Equal(t, 2, opts.DB)
require.Equal(t, 5*time.Second, opts.DialTimeout)
+12
View File
@@ -178,6 +178,7 @@ func testDatabase(c *gin.Context) {
type TestRedisRequest struct {
Host string `json:"host" binding:"required"`
Port int `json:"port" binding:"required"`
Username string `json:"username"`
Password string `json:"password"`
DB int `json:"db"`
EnableTLS bool `json:"enable_tls"`
@@ -204,10 +205,16 @@ func testRedis(c *gin.Context) {
response.Error(c, http.StatusBadRequest, "Invalid Redis database number (0-15)")
return
}
req.Username = strings.TrimSpace(req.Username)
if len(req.Username) > 128 {
response.Error(c, http.StatusBadRequest, "Invalid Redis username")
return
}
cfg := &RedisConfig{
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
DB: req.DB,
EnableTLS: req.EnableTLS,
@@ -252,6 +259,7 @@ func install(c *gin.Context) {
req.Database.User = strings.TrimSpace(req.Database.User)
req.Database.DBName = strings.TrimSpace(req.Database.DBName)
req.Redis.Host = strings.TrimSpace(req.Redis.Host)
req.Redis.Username = strings.TrimSpace(req.Redis.Username)
// ========== COMPREHENSIVE INPUT VALIDATION ==========
// Database validation
@@ -285,6 +293,10 @@ func install(c *gin.Context) {
response.Error(c, http.StatusBadRequest, "Invalid Redis database number")
return
}
if len(req.Redis.Username) > 128 {
response.Error(c, http.StatusBadRequest, "Invalid Redis username")
return
}
// Admin validation
if !validateEmail(req.Admin.Email) {
+3
View File
@@ -95,6 +95,7 @@ type DatabaseConfig struct {
type RedisConfig struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Username string `json:"username" yaml:"username"`
Password string `json:"password" yaml:"password"`
DB int `json:"db" yaml:"db"`
EnableTLS bool `json:"enable_tls" yaml:"enable_tls"`
@@ -251,6 +252,7 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error {
func TestRedisConnection(cfg *RedisConfig) error {
opts := &redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Username: cfg.Username,
Password: cfg.Password,
DB: cfg.DB,
}
@@ -570,6 +572,7 @@ func AutoSetupFromEnv() error {
Redis: RedisConfig{
Host: getEnvOrDefault("REDIS_HOST", "localhost"),
Port: getEnvIntOrDefault("REDIS_PORT", 6379),
Username: getEnvOrDefault("REDIS_USERNAME", ""),
Password: getEnvOrDefault("REDIS_PASSWORD", ""),
DB: getEnvIntOrDefault("REDIS_DB", 0),
EnableTLS: getEnvOrDefault("REDIS_ENABLE_TLS", "false") == "true",
+23
View File
@@ -105,6 +105,29 @@ func TestWriteConfigFileKeepsDefaultUserConcurrency(t *testing.T) {
}
}
func TestWriteConfigFileIncludesRedisUsername(t *testing.T) {
t.Setenv("DATA_DIR", t.TempDir())
if err := writeConfigFile(&SetupConfig{
Redis: RedisConfig{
Host: "redis",
Port: 6379,
Username: "app-user",
},
}); err != nil {
t.Fatalf("writeConfigFile() error = %v", err)
}
data, err := os.ReadFile(GetConfigFilePath())
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if !strings.Contains(string(data), "username: app-user") {
t.Fatalf("config missing Redis username, got:\n%s", string(data))
}
}
func TestBuildDatabaseConnectionDSNsUsesPostgresForBootstrap(t *testing.T) {
cfg := &DatabaseConfig{
Host: "db",
+2
View File
@@ -176,6 +176,8 @@ DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5
# -----------------------------------------------------------------------------
# Redis 监听端口(同时用于应用连接和 Redis 服务端,默认 6379)
REDIS_PORT=6379
# Redis ACL username; leave empty for the default user
REDIS_USERNAME=
# Leave empty for no password (default for local development)
REDIS_PASSWORD=
REDIS_DB=0
+3
View File
@@ -920,6 +920,9 @@ redis:
# Redis port
# Redis 端口
port: 6379
# Redis ACL username (leave empty for default user)
# Redis ACL 用户名(使用默认用户时留空)
username: ""
# Redis password (leave empty if no password is set)
# Redis 密码(如果未设置密码则留空)
password: ""
+1
View File
@@ -36,6 +36,7 @@ services:
- DATABASE_SSLMODE=disable
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
+1
View File
@@ -74,6 +74,7 @@ services:
# =======================================================================
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024}
+1
View File
@@ -60,6 +60,7 @@ services:
# =======================================================================
- REDIS_HOST=${REDIS_HOST:?REDIS_HOST is required}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024}
+1
View File
@@ -70,6 +70,7 @@ services:
# =======================================================================
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_USERNAME=${REDIS_USERNAME:-}
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
- REDIS_DB=${REDIS_DB:-0}
- REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024}
+1
View File
@@ -30,6 +30,7 @@ export interface DatabaseConfig {
export interface RedisConfig {
host: string
port: number
username: string
password: string
db: number
enable_tls: boolean
+2
View File
@@ -215,8 +215,10 @@ export default {
description: 'Connect to your Redis server',
host: 'Host',
port: 'Port',
username: 'Username (optional)',
password: 'Password (optional)',
database: 'Database',
usernamePlaceholder: 'Leave empty for default user',
passwordPlaceholder: 'Password',
enableTls: 'Enable TLS',
enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)'
+2
View File
@@ -215,8 +215,10 @@ export default {
description: '连接到您的 Redis 服务器',
host: '主机',
port: '端口',
username: '用户名(可选)',
password: '密码(可选)',
database: '数据库',
usernamePlaceholder: '默认用户留空',
passwordPlaceholder: '密码',
enableTls: '启用 TLS',
enableTlsHint: '连接 Redis 时使用 TLS(公共 CA 证书)'
@@ -217,6 +217,15 @@
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="input-label">{{ t('setup.redis.username') }}</label>
<input
v-model="formData.redis.username"
type="text"
class="input"
:placeholder="t('setup.redis.usernamePlaceholder')"
/>
</div>
<div>
<label class="input-label">{{ t('setup.redis.password') }}</label>
<input
@@ -542,6 +551,7 @@ const formData = reactive<InstallRequest>({
redis: {
host: 'localhost',
port: 6379,
username: '',
password: '',
db: 0,
enable_tls: false