diff --git a/backend/.env.example b/backend/.env.example index 6b7b8eb..b4e7ff2 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -56,8 +56,8 @@ REALNAME_CLOUDMARKET_APPCODE= # 生成方式:openssl rand -hex 16 PAYMENT_CONFIG_ENCRYPTION_KEY= -# 开放导入接口签名密钥;留空时 /api/open/listing-uploads 不可用。 -# 请求需携带 X-HFB-Timestamp 和 X-HFB-Signature。 +# 开放导入接口可选签名密钥;留空或请求未带签名时按旧方式导入。 +# 若调用方携带 X-HFB-Timestamp 和 X-HFB-Signature,则会校验 HMAC 签名。 EXTERNAL_UPLOAD_SECRET= # 可选:逗号分隔的 IP 或 CIDR 白名单,例如 127.0.0.1,10.0.0.0/8。 EXTERNAL_UPLOAD_ALLOWED_IPS= diff --git a/backend/.env.prod.example b/backend/.env.prod.example index 2367079..152cf96 100644 --- a/backend/.env.prod.example +++ b/backend/.env.prod.example @@ -62,7 +62,8 @@ REALNAME_CLOUDMARKET_APPCODE= # 警告:此密钥一旦设置不要更改,否则已有配置无法解密 PAYMENT_CONFIG_ENCRYPTION_KEY=change-to-32-byte-encryption-key -# 开放导入接口签名密钥,生产环境必填;生成方式:openssl rand -hex 32 -EXTERNAL_UPLOAD_SECRET=change-to-a-long-random-upload-secret +# 开放导入接口可选签名密钥;内部调用方暂不签名时可留空。 +# 若启用签名,生成方式:openssl rand -hex 32 +EXTERNAL_UPLOAD_SECRET= # 可选:逗号分隔的 IP 或 CIDR 白名单,例如 203.0.113.10,10.0.0.0/8。 EXTERNAL_UPLOAD_ALLOWED_IPS= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 31c5cc5..c18c588 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -120,9 +120,6 @@ func (c Config) ValidateProductionSecurity() error { if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) { return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production") } - if strings.TrimSpace(c.ExternalUploadSecret) == "" || isPlaceholder(c.ExternalUploadSecret) { - return errors.New("EXTERNAL_UPLOAD_SECRET is required in production") - } if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) { return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production") } diff --git a/backend/internal/modules/listing/handler_external.go b/backend/internal/modules/listing/handler_external.go index 3a90e2b..d3331ea 100644 --- a/backend/internal/modules/listing/handler_external.go +++ b/backend/internal/modules/listing/handler_external.go @@ -47,24 +47,26 @@ func (h *Handler) ImportExternalUpload(c *gin.Context) { } var ( - errExternalUploadSecretMissing = errors.New("external upload secret missing") - errExternalUploadForbiddenIP = errors.New("external upload forbidden ip") - errExternalUploadTimestamp = errors.New("external upload timestamp invalid") - errExternalUploadSignature = errors.New("external upload signature invalid") + errExternalUploadForbiddenIP = errors.New("external upload forbidden ip") + errExternalUploadTimestamp = errors.New("external upload timestamp invalid") + errExternalUploadSignature = errors.New("external upload signature invalid") ) const externalUploadMaxClockSkew = 5 * time.Minute func (h *Handler) verifyExternalUpload(c *gin.Context, raw []byte) error { - secret := strings.TrimSpace(h.externalUploadSecret) - if secret == "" { - return errExternalUploadSecretMissing - } if !externalUploadIPAllowed(c.ClientIP(), h.externalUploadAllowedIPs) { return errExternalUploadForbiddenIP } + secret := strings.TrimSpace(h.externalUploadSecret) + if secret == "" { + return nil + } timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp")) signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature")) + if timestamp == "" && signature == "" { + return nil + } if timestamp == "" || signature == "" { return errExternalUploadSignature } @@ -111,8 +113,6 @@ func externalUploadIPAllowed(clientIP string, allowed []string) bool { func writeExternalUploadAuthError(c *gin.Context, err error) { switch err { - case errExternalUploadSecretMissing: - response.ServiceUnavailable(c, "开放导入签名密钥未配置") case errExternalUploadForbiddenIP: response.Error(c, http.StatusForbidden, "forbidden", "来源 IP 不允许访问") case errExternalUploadTimestamp: diff --git a/backend/internal/modules/listing/handler_external_test.go b/backend/internal/modules/listing/handler_external_test.go index 37645f4..c774501 100644 --- a/backend/internal/modules/listing/handler_external_test.go +++ b/backend/internal/modules/listing/handler_external_test.go @@ -30,6 +30,28 @@ func TestVerifyExternalUploadSignature(t *testing.T) { } } +func TestVerifyExternalUploadAllowsUnsignedRequest(t *testing.T) { + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("POST", "/api/open/listing-uploads", nil) + + handler := &Handler{} + if err := handler.verifyExternalUpload(c, []byte(`{}`)); err != nil { + t.Fatalf("verifyExternalUpload() error = %v", err) + } +} + +func TestVerifyExternalUploadAllowsUnsignedRequestWithSecret(t *testing.T) { + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("POST", "/api/open/listing-uploads", nil) + + handler := &Handler{externalUploadSecret: "test-upload-secret"} + if err := handler.verifyExternalUpload(c, []byte(`{}`)); err != nil { + t.Fatalf("verifyExternalUpload() error = %v", err) + } +} + func TestVerifyExternalUploadRejectsBadSignature(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder())