security: remove hardcoded credentials and add WebSocket auth/CORS

- Clear default passwords in all service configs, require env vars
- Add JWT auth + role middleware to WebSocket endpoints
- Add origin whitelist to WebSocket upgrader (CORS protection)
- Fix goroutine leak in WS proxy (double errCh read)
- Update docker-compose to require secrets via ${VAR:?...} syntax
- Mark deprecated k8s configmap passwords as REPLACE_AT_DEPLOY_TIME

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chever John 2026-06-17 23:16:17 +08:00
parent 08b6c7cbeb
commit 24c449ecae
No known key found for this signature in database
GPG Key ID: 2894D52ED1211DF0
11 changed files with 81 additions and 44 deletions

View File

@ -1,18 +1,20 @@
# iloom 微服务环境变量配置 # iloom 微服务环境变量配置
# Copy this file to .env and fill in real values. NEVER commit .env to git.
# MySQL (共享 muyu_wms 数据库) # MySQL (共享 muyu_wms 数据库)
DB_DSN=root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 MYSQL_ROOT_PASSWORD=your_mysql_password_here
DB_DSN=root:your_password@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
# JWT (复用 muyu 认证) # JWT (复用 muyu 认证)
JWT_SECRET=muyu-wms-jwt-secret-key-2026 JWT_SECRET=your_jwt_secret_here
JWT_REFRESH_SECRET=muyu-wms-jwt-refresh-2026 JWT_REFRESH_SECRET=your_jwt_refresh_secret_here
# muyu gRPC 地址 # muyu gRPC 地址
MUYU_SYSTEM_RPC_ADDR=system-rpc:9001 MUYU_SYSTEM_RPC_ADDR=system-rpc:9001
MUYU_INVENTORY_RPC_ADDR=inventory-rpc:9002 MUYU_INVENTORY_RPC_ADDR=inventory-rpc:9002
# 服务间调用密钥 # 服务间调用密钥
INTERNAL_SERVICE_KEY=internal-service-shared-secret INTERNAL_SERVICE_KEY=your_internal_service_key_here
# 服务端口 # 服务端口
GATEWAY_PORT=8080 GATEWAY_PORT=8080

View File

@ -13,9 +13,9 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8081"), Port: getEnv("PORT", "8081"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
JWTSecret: getEnv("JWT_SECRET", "muyu-wms-jwt-secret-key-2026"), JWTSecret: getEnv("JWT_SECRET", ""),
JWTRefreshSecret: getEnv("JWT_REFRESH_SECRET", "muyu-wms-jwt-refresh-2026"), JWTRefreshSecret: getEnv("JWT_REFRESH_SECRET", ""),
SystemRPCAddr: getEnv("MUYU_SYSTEM_RPC_ADDR", "127.0.0.1:9001"), SystemRPCAddr: getEnv("MUYU_SYSTEM_RPC_ADDR", "127.0.0.1:9001"),
} }
} }

View File

@ -10,7 +10,7 @@ services:
- "${GATEWAY_PORT:-8080}:8080" - "${GATEWAY_PORT:-8080}:8080"
environment: environment:
- PORT=8080 - PORT=8080
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- AUTH_SERVICE_URL=http://auth-service:8081 - AUTH_SERVICE_URL=http://auth-service:8081
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
- TEXTILE_SERVICE_URL=http://textile-service:8083 - TEXTILE_SERVICE_URL=http://textile-service:8083
@ -37,9 +37,9 @@ services:
- "${AUTH_SERVICE_PORT:-8081}:8081" - "${AUTH_SERVICE_PORT:-8081}:8081"
environment: environment:
- PORT=8081 - PORT=8081
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-muyu-wms-jwt-refresh-2026} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:?Set JWT_REFRESH_SECRET in .env}
- MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001 - MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8081/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8081/health"]
@ -58,8 +58,8 @@ services:
- "${PURCHASER_SERVICE_PORT:-8082}:8082" - "${PURCHASER_SERVICE_PORT:-8082}:8082"
environment: environment:
- PORT=8082 - PORT=8082
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002 - MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8082/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8082/health"]
@ -78,8 +78,8 @@ services:
- "${TEXTILE_SERVICE_PORT:-8083}:8083" - "${TEXTILE_SERVICE_PORT:-8083}:8083"
environment: environment:
- PORT=8083 - PORT=8083
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8083/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8083/health"]
@ -98,8 +98,8 @@ services:
- "${WASHING_SERVICE_PORT:-8084}:8084" - "${WASHING_SERVICE_PORT:-8084}:8084"
environment: environment:
- PORT=8084 - PORT=8084
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8084/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8084/health"]

View File

@ -6,7 +6,7 @@
# 集成模式(与 muyu-apiserver 联合): # 集成模式(与 muyu-apiserver 联合):
# 1. cd ../muyu-apiserver/deploy && docker compose up # 1. cd ../muyu-apiserver/deploy && docker compose up
# 2. 设置环境变量指向 muyu 的服务: # 2. 设置环境变量指向 muyu 的服务:
# export DB_DSN="root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4" # export DB_DSN="root:yourpassword@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4"
# export MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001 # export MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001
# export MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002 # export MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002
# 3. docker compose --profile integrated up # 3. docker compose --profile integrated up
@ -15,7 +15,7 @@ services:
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
environment: environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-muyu2026} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}
MYSQL_DATABASE: ${MYSQL_DATABASE:-muyu_wms} MYSQL_DATABASE: ${MYSQL_DATABASE:-muyu_wms}
TZ: Asia/Shanghai TZ: Asia/Shanghai
ports: ports:
@ -26,7 +26,7 @@ services:
- ./scripts/init-iloom.sql:/docker-entrypoint-initdb.d/02-iloom.sql - ./scripts/init-iloom.sql:/docker-entrypoint-initdb.d/02-iloom.sql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-muyu2026}"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 10 retries: 10
@ -39,7 +39,7 @@ services:
- "${GATEWAY_PORT:-8080}:8080" - "${GATEWAY_PORT:-8080}:8080"
environment: environment:
- PORT=8080 - PORT=8080
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- AUTH_SERVICE_URL=http://auth-service:8081 - AUTH_SERVICE_URL=http://auth-service:8081
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
- TEXTILE_SERVICE_URL=http://textile-service:8083 - TEXTILE_SERVICE_URL=http://textile-service:8083
@ -63,9 +63,9 @@ services:
- "${AUTH_SERVICE_PORT:-8081}:8081" - "${AUTH_SERVICE_PORT:-8081}:8081"
environment: environment:
- PORT=8081 - PORT=8081
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-muyu-wms-jwt-refresh-2026} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:?Set JWT_REFRESH_SECRET in .env}
- MUYU_SYSTEM_RPC_ADDR=${MUYU_SYSTEM_RPC_ADDR:-system-rpc:9001} - MUYU_SYSTEM_RPC_ADDR=${MUYU_SYSTEM_RPC_ADDR:-system-rpc:9001}
depends_on: depends_on:
mysql: mysql:
@ -84,8 +84,8 @@ services:
- "${PURCHASER_SERVICE_PORT:-8082}:8082" - "${PURCHASER_SERVICE_PORT:-8082}:8082"
environment: environment:
- PORT=8082 - PORT=8082
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- MUYU_INVENTORY_RPC_ADDR=${MUYU_INVENTORY_RPC_ADDR:-inventory-rpc:9002} - MUYU_INVENTORY_RPC_ADDR=${MUYU_INVENTORY_RPC_ADDR:-inventory-rpc:9002}
depends_on: depends_on:
mysql: mysql:
@ -104,8 +104,8 @@ services:
- "${TEXTILE_SERVICE_PORT:-8083}:8083" - "${TEXTILE_SERVICE_PORT:-8083}:8083"
environment: environment:
- PORT=8083 - PORT=8083
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
depends_on: depends_on:
mysql: mysql:
@ -124,8 +124,8 @@ services:
- "${WASHING_SERVICE_PORT:-8084}:8084" - "${WASHING_SERVICE_PORT:-8084}:8084"
environment: environment:
- PORT=8084 - PORT=8084
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
depends_on: depends_on:
mysql: mysql:

View File

@ -58,10 +58,22 @@ func main() {
washing.Any("/*path", proxy.NewReverseProxy(cfg.WashingServiceURL)) washing.Any("/*path", proxy.NewReverseProxy(cfg.WashingServiceURL))
} }
// WebSocket proxy — route by role // WebSocket proxy — route by role (JWT + role check applied)
r.GET("/ws/v1/purchaser", proxy.NewWSProxy(cfg.PurchaserServiceURL)) r.GET("/ws/v1/purchaser",
r.GET("/ws/v1/textile", proxy.NewWSProxy(cfg.TextileServiceURL)) middleware.AuthRequired(cfg.JWTSecret),
r.GET("/ws/v1/washing", proxy.NewWSProxy(cfg.WashingServiceURL)) middleware.RoleRequired("purchaser"),
proxy.NewWSProxy(cfg.PurchaserServiceURL, cfg.AllowOrigins),
)
r.GET("/ws/v1/textile",
middleware.AuthRequired(cfg.JWTSecret),
middleware.RoleRequired("textile"),
proxy.NewWSProxy(cfg.TextileServiceURL, cfg.AllowOrigins),
)
r.GET("/ws/v1/washing",
middleware.AuthRequired(cfg.JWTSecret),
middleware.RoleRequired("washing"),
proxy.NewWSProxy(cfg.WashingServiceURL, cfg.AllowOrigins),
)
addr := ":" + cfg.Port addr := ":" + cfg.Port
log.Printf("gateway listening on %s", addr) log.Printf("gateway listening on %s", addr)

View File

@ -18,7 +18,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8080"), Port: getEnv("PORT", "8080"),
JWTSecret: getEnv("JWT_SECRET", "muyu-wms-jwt-secret-key-2026"), JWTSecret: getEnv("JWT_SECRET", ""),
AuthServiceURL: getEnv("AUTH_SERVICE_URL", "http://localhost:8081"), AuthServiceURL: getEnv("AUTH_SERVICE_URL", "http://localhost:8081"),
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
TextileServiceURL: getEnv("TEXTILE_SERVICE_URL", "http://localhost:8083"), TextileServiceURL: getEnv("TEXTILE_SERVICE_URL", "http://localhost:8083"),

View File

@ -11,11 +11,29 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var upgrader = websocket.Upgrader{ func newUpgrader(allowedOrigins []string) websocket.Upgrader {
CheckOrigin: func(r *http.Request) bool { return true }, originSet := make(map[string]bool, len(allowedOrigins))
for _, o := range allowedOrigins {
originSet[strings.TrimSpace(o)] = true
}
return websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if originSet["*"] {
return true
}
origin := r.Header.Get("Origin")
if origin == "" {
return false
}
return originSet[origin]
},
}
} }
func NewWSProxy(targetURL string) gin.HandlerFunc { func NewWSProxy(targetURL string, allowedOrigins []string) gin.HandlerFunc {
upgrader := newUpgrader(allowedOrigins)
return func(c *gin.Context) { return func(c *gin.Context) {
clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
@ -50,6 +68,7 @@ func NewWSProxy(targetURL string) gin.HandlerFunc {
go pumpMessages(backendConn, clientConn, errCh) go pumpMessages(backendConn, clientConn, errCh)
<-errCh <-errCh
<-errCh
} }
} }

View File

@ -1,3 +1,7 @@
## DEPRECATED: These raw K8s manifests are superseded by iloom-deploy/charts/.
## DataSource values must be injected via Kubernetes Secrets at deploy time.
## See iloom-deploy/charts/infra/templates/secrets.yaml for the Helm-based approach.
##
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
@ -13,7 +17,7 @@ data:
- etcd:2379 - etcd:2379
Key: system.rpc Key: system.rpc
DataSource: root:muyu2026@tcp(mysql:3306)/muyu_wms?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai DataSource: REPLACE_AT_DEPLOY_TIME
Cache: Cache:
- Host: redis:6379 - Host: redis:6379
@ -38,7 +42,7 @@ data:
- etcd:2379 - etcd:2379
Key: inventory.rpc Key: inventory.rpc
DataSource: root:muyu2026@tcp(mysql:3306)/muyu_wms?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai DataSource: REPLACE_AT_DEPLOY_TIME
Cache: Cache:
- Host: redis:6379 - Host: redis:6379
@ -60,7 +64,7 @@ data:
Port: 8888 Port: 8888
Auth: Auth:
AccessSecret: muyu-wms-jwt-secret-key-2026 AccessSecret: REPLACE_AT_DEPLOY_TIME
AccessExpire: 7200 AccessExpire: 7200
SystemRpc: SystemRpc:

View File

@ -12,7 +12,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8082"), Port: getEnv("PORT", "8082"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"), InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
InventoryRPCAddr: getEnv("MUYU_INVENTORY_RPC_ADDR", "127.0.0.1:9002"), InventoryRPCAddr: getEnv("MUYU_INVENTORY_RPC_ADDR", "127.0.0.1:9002"),
} }

View File

@ -12,7 +12,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8083"), Port: getEnv("PORT", "8083"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"), InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
} }

View File

@ -12,7 +12,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8084"), Port: getEnv("PORT", "8084"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"), InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
} }