docs: define global cors boundary

This commit is contained in:
malei 2026-08-02 02:59:44 +08:00
parent c902bf4786
commit 811c2f8897
4 changed files with 35 additions and 7 deletions

View File

@ -138,8 +138,8 @@
{"id":"rule:BR-USER-001","type":"Rule","name":"员工管理与邀请仅 boss + 同店","domain":"user","appliesTo":["action:create_staff","action:list_staff","action:delete_staff","action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation"],"code":"backend/src/main/java/com/petstore/controller/UserController.java; backend/src/main/java/com/petstore/controller/StaffInvitationController.java","test":"StaffInvitationServiceTest","evidence":"anchored","note":"门店成员可看成员列表;仅 boss 可创建/查看/撤销邀请和删除本店 staff不能删除 boss"} {"id":"rule:BR-USER-001","type":"Rule","name":"员工管理与邀请仅 boss + 同店","domain":"user","appliesTo":["action:create_staff","action:list_staff","action:delete_staff","action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation"],"code":"backend/src/main/java/com/petstore/controller/UserController.java; backend/src/main/java/com/petstore/controller/StaffInvitationController.java","test":"StaffInvitationServiceTest","evidence":"anchored","note":"门店成员可看成员列表;仅 boss 可创建/查看/撤销邀请和删除本店 staff不能删除 boss"}
{"id":"rule:BR-USER-002","type":"Rule","name":"用户更新仅本人","domain":"user","appliesTo":["action:update_user","action:get_user_info"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser; #info","evidence":"anchored"} {"id":"rule:BR-USER-002","type":"Rule","name":"用户更新仅本人","domain":"user","appliesTo":["action:update_user","action:get_user_info"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser; #info","evidence":"anchored"}
{"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"} {"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"}
{"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile","test":"ProductionConfigurationValidatorTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS"} {"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/config/CorsConfig.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile; docs/架构决策-全局CORS单一策略-2026-08-02.md","test":"ProductionConfigurationValidatorTest; CorsBoundaryContractTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS;全局 CorsFilter 是唯一策略Controller 禁止局部 @CrossOrigin"}
{"id":"rule:BR-CONFIG-003","type":"Rule","name":"生产发布只读门禁","domain":"config","appliesTo":[],"code":"backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java; backend/src/main/java/com/petstore/config/PetstoreRuntimeHealthIndicator.java; backend/deploy/release-preflight.sh; backend/deploy/production-smoke.sh","doc":"docs/生产发布与回滚Runbook-2026-08-01.md; docs/生产监控与告警基线-2026-08-01.md","evidence":"anchored","note":"备份和八迁移后运行 schema validate + 31 项只读不变量readiness 覆盖 DB/磁盘/上传/FFmpeg自动 smoke 只读"} {"id":"rule:BR-CONFIG-003","type":"Rule","name":"生产发布只读门禁","domain":"config","appliesTo":[],"code":"backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java; backend/src/main/java/com/petstore/config/PetstoreRuntimeHealthIndicator.java; backend/deploy/release-preflight.sh; backend/deploy/production-smoke.sh","doc":"docs/生产发布与回滚Runbook-2026-08-01.md; docs/生产监控与告警基线-2026-08-01.md","test":"CorsBoundaryContractTest","evidence":"anchored","note":"备份和八迁移后运行 schema validate + 31 项只读不变量readiness 覆盖 DB/磁盘/上传/FFmpeg自动 smoke 只读并验证 CORS allow/deny"}
{"id":"rel:user_owns_pets","type":"Relation","subject":"entity:user","predicate":"OWNS","object":"entity:pet","cardinality":"1—N","evidence":"anchored","doc":"Pet.owner_user_id → User.id","domain":"user"} {"id":"rel:user_owns_pets","type":"Relation","subject":"entity:user","predicate":"OWNS","object":"entity:pet","cardinality":"1—N","evidence":"anchored","doc":"Pet.owner_user_id → User.id","domain":"user"}
{"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"} {"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"}
{"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"} {"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"}

View File

@ -364,15 +364,17 @@
**production profile**`--spring.profiles.active=production` 时 `ddl-auto=validate`(不改表)、`show-sql=false`、`SMS_UNIVERSAL_CODE` 默认空(关闭万能验证码 `rule:BR-AUTH-002`),并跳过开发期默认服务初始化。最终配置不满足强密钥、显式生产 HTTPS 域名、绝对上传目录和显式 CORS 源时启动失败。 **production profile**`--spring.profiles.active=production` 时 `ddl-auto=validate`(不改表)、`show-sql=false`、`SMS_UNIVERSAL_CODE` 默认空(关闭万能验证码 `rule:BR-AUTH-002`),并跳过开发期默认服务初始化。最终配置不满足强密钥、显式生产 HTTPS 域名、绝对上传目录和显式 CORS 源时启动失败。
**CORS 单一策略**:全局 `CorsFilter` 是唯一跨域策略,白名单只来自 `CORS_ALLOWED_ORIGINS`Controller 禁止局部 `@CrossOrigin`,防止配置合并或 Filter 变更后意外 fail-open。生产 smoke 必须同时验证一个白名单 Origin 精确回显、一个非白名单 Origin 返回 403。
- **适用动作**:—(全局约束) - **适用动作**:—(全局约束)
- **代码**`backend/src/main/resources/application.yml`multi-document production profile、`ProductionConfigurationValidator.java`、`PetstoreApplication.java` - **代码**`backend/src/main/resources/application.yml`multi-document production profile、`ProductionConfigurationValidator.java`、`CorsConfig.java`、`PetstoreApplication.java`
- **测试**`ProductionConfigurationValidatorTest` - **测试**`ProductionConfigurationValidatorTest`、`CorsBoundaryContractTest`
- **文档**`backend/README.md production profile` - **文档**`backend/README.md production profile`
- **证据**`anchored`(真实生产值与启动证据仍属发布门禁) - **证据**`anchored`(真实生产值与启动证据仍属发布门禁)
### rule:BR-CONFIG-003 ### rule:BR-CONFIG-003
**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和八个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 31 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe上线自动 smoke 只允许 GET不创建或修改业务数据。 **生产发布只读门禁**:正式切流前必须按固定顺序完成备份和八个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 31 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe上线自动 smoke 只允许 GET不创建或修改业务数据,并验证 CORS allow/deny
- **适用动作**:—(发布全局约束) - **适用动作**:—(发布全局约束)
- **代码**`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh` - **代码**`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh`

View File

@ -0,0 +1,22 @@
# 架构决策:全局 CORS 单一策略
> 日期2026-08-02
> 决策:仅使用全局 `CorsFilter`;所有 Controller 禁止 `@CrossOrigin`
> 本体:`rule:BR-CONFIG-002`、`rule:BR-CONFIG-003`
## 背景
后端已经通过 `CORS_ALLOWED_ORIGINS` 和全局 `CorsFilter` 配置显式 Web Origin但 17 个 Controller 同时保留了无参数 `@CrossOrigin`。在当前 Filter 顺序下,非白名单请求会先被拒绝;局部注解仍是危险的第二套策略:如果 Filter 注册、请求映射或 Spring 配置方式变化,默认允许全部 Origin 的局部策略可能重新暴露接口。
## 决策
1. 删除全部 Controller 级 `@CrossOrigin`,不允许页面或接口自行扩展 Origin。
2. 生产 profile 继续要求 `CORS_ALLOWED_ORIGINS` 只含显式 HTTPS origin禁止通配符、localhost、占位域名、path、query 和 fragment。
3. `CorsBoundaryContractTest` 直接执行真实 `CorsFilter`:非白名单 Origin 在进入 Controller 前返回 403白名单 Origin 精确回显并允许凭据;预检请求不进入业务代码;源码扫描保证局部注解为 0。
4. `production-smoke.sh` 要求提供一个真实白名单 Origin同时以固定非白名单 Origin 验证拒绝分支,整个过程只执行 GET/OPTIONS 语义,不写业务数据。
## 边界
- 微信小程序原生请求不依赖浏览器 CORS不需要把微信域名加入该白名单。
- Admin 与公开报告 H5 的正式 origin 必须分别列入API 自身域名不是前端页面 origin 时不自动加入。
- 临时联调域名不得直接写入 Controller必须通过受控环境配置、评审和生产 preflight。

View File

@ -14,6 +14,7 @@
4. 应用、静态站点采用版本目录加 `current` 软链接切换;数据库迁移为向前兼容的追加式变更,不做自动向下迁移。 4. 应用、静态站点采用版本目录加 `current` 软链接切换;数据库迁移为向前兼容的追加式变更,不做自动向下迁移。
5. Petstore 使用独立域名、Nginx `server` 块、systemd unit 和发布目录。不得覆盖、重启或复用同机 Gitea/GitLab 的配置与服务。 5. Petstore 使用独立域名、Nginx `server` 块、systemd unit 和发布目录。不得覆盖、重启或复用同机 Gitea/GitLab 的配置与服务。
6. 任何命令输出都不得包含数据库密码、微信 AppSecret、session secret、完整报告 token、手机号或私密媒体 URL。 6. 任何命令输出都不得包含数据库密码、微信 AppSecret、session secret、完整报告 token、手机号或私密媒体 URL。
7. CORS 只由后端全局 `CorsFilter` 控制Controller 不得声明局部 `@CrossOrigin`,生产 smoke 必须同时验证白名单 Origin 与拒绝分支。
## 2. 本次发布任务卡 ## 2. 本次发布任务卡
@ -51,6 +52,7 @@
- `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block - `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block
- readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败; - readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败;
- 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图; - 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图;
- 任一 Controller 出现局部 `@CrossOrigin`,或非白名单 Origin 未返回 403
- 无明确值班人、回滚执行人或生产变更授权。 - 无明确值班人、回滚执行人或生产变更授权。
## 4. 发布前冻结 ## 4. 发布前冻结
@ -185,10 +187,12 @@ npm run build:mp-weixin
```bash ```bash
cd /opt/petstore/backend/current cd /opt/petstore/backend/current
API_ORIGIN=https://<api-domain> ./deploy/production-smoke.sh API_ORIGIN=https://<api-domain> \
SMOKE_ALLOWED_ORIGIN=https://<admin-or-report-domain> \
./deploy/production-smoke.sh
``` ```
脚本验证 liveness、readiness、公开门店、服务时长和可约容量契约;提供受控的 `SMOKE_BEARER_TOKEN` 时才增加两个 protected GET。脚本不会创建或修改业务数据。 脚本验证 liveness、readiness、公开门店、服务时长、可约容量、白名单 Origin 精确回显和非白名单 Origin 403;提供受控的 `SMOKE_BEARER_TOKEN` 时才增加两个 protected GET。脚本不会创建或修改业务数据。
### 8.2 三角色人工 smoke ### 8.2 三角色人工 smoke