# S03 生产配置、简历隐私与 Actuator 隔离设计 状态:设计已确认,待按独立检查点实施。 ## 1. 目标与边界 S03 只处理三类高风险问题: 1. 禁用仍保持原始状态的通用 Flyway 演示账号,并提供显式、一次性的管理员初始化能力; 2. 让 `resumeVisibility` 在企业读取简历文件的正式边界真正生效; 3. 将 Actuator 健康探针与指标端点隔离,防止普通业务用户读取运行指标。 S03 不修改 Access/Refresh Token 语义,不修改评分、排名、推荐、人才发现、邀约、Offer、申诉、消息撤回、简历上传与学生本人下载语义,不做性能重构,不进入 Q01/R01。 ## 2. 已核实的现状 - `V1_1__seed_data.sql` 无环境隔离地插入已知 ADMIN、STUDENT、COMPANY 账号及公开口令哈希。 - V1–V42 已经执行,不允许修改校验和;S03 使用新的 V43 迁移。 - `student_privacy_settings.resume_visibility` 已支持 `PRIVATE`、`APPLIED_COMPANIES`、`VERIFIED_COMPANIES`,默认 `PRIVATE`。 - `PrivacySettingsService.canCompanyViewResume(...)` 没有生产调用方。 - 该方法的 `APPLIED_COMPANIES` 分支没有核对投递岗位归属 `companyId`,不能直接作为授权边界使用。 - 企业候选人 DTO 当前主动排除了 `resumeUrl`;当前没有企业下载简历文件的正式接口。 - `/actuator/health/**` 和 `/actuator/info` 公开;`metrics/prometheus` 被暴露后会落入普通 `authenticated()`,任何登录用户均可能访问。 ## 3. S03-A 历史种子账号治理 ### 3.1 V43 迁移原则 新增且仅新增: `V43__disable_legacy_seed_accounts.sql` V43 只能禁用仍与 V1_1 原始指纹完全一致的三条种子账号。匹配至少同时包含: - 原始手机号; - 原始邮箱; - 原始公开 bcrypt 哈希; - 原始主角色; - `is_active=1`。 不得依赖固定 `id=1/2/3`。不得删除用户、角色、审计或业务数据,不得重置自增。不得修改已被运营人员改过手机号、邮箱、哈希或角色的账号。 命中后仅将 `users.is_active` 置为 `0`。保留记录与关联关系,用于历史数据完整性和审计追溯。由于认证签发链已拒绝 inactive 用户,已知账号不能再取得会话。 迁移必须幂等,并分别覆盖 ADMIN、STUDENT、COMPANY 三条原始种子。迁移报告只记录命中数量,不输出完整手机号、邮箱或哈希。 ### 3.2 管理员一次性初始化 新增显式开关,默认关闭: - `app.bootstrap.admin.enabled=${ADMIN_BOOTSTRAP_ENABLED:false}` - `app.bootstrap.admin.phone=${ADMIN_BOOTSTRAP_PHONE:}` - `app.bootstrap.admin.nickname=${ADMIN_BOOTSTRAP_NICKNAME:平台管理员}` 初始化器仅在开关严格为 `true` 时装配。普通启动不得隐式创建管理员。 初始化规则: 1. 手机号必须通过现有手机号格式校验;不得写日志、异常详情或审计 details。 2. 数据库已有活动 ADMIN 时安全跳过,不创建第二个管理员。 3. 数据库没有活动 ADMIN 且手机号不存在时,创建 `active=true`、`verified=true` 的 ADMIN,并同时写入 `users.role` 与 `user_roles`。 4. 手机号已属于 STUDENT/COMPANY、inactive 用户或角色信息不一致时启动失败,禁止自动提权或复活。 5. 相同配置重复启动必须幂等。 6. 初始化成功后应提示固定文本,部署方必须移除开关;日志不得包含手机号。 7. 多实例部署时只允许一个实例带初始化开关启动;数据库唯一手机号约束作为最后防线,冲突必须失败而不是吞掉。 不新增公网初始化接口,不接受请求参数创建 ADMIN,不设置默认密码。管理员仍使用正式短信认证流程;真实短信供应商未配置时不能以 Mock 绕过。 ## 4. S03-B 简历隐私授权 ### 4.1 唯一新增企业接口 新增: `GET /company/candidates/{applicationId}/resume` 该接口返回文件流,不返回 `resumeUrl`、存储相对路径、绝对路径、学生用户 ID、手机号或邮箱。文件名必须继续经过 `ResumeFilenameSanitizer`。 除该接口外不新增按 `studentUserId`、`candidateKey` 或任意 URL 下载简历的入口。人才发现接口本轮不得直接下载简历。 ### 4.2 授权顺序 必须在读取文件前依次完成: 1. 从 `SecurityContext` 取得当前用户,要求 COMPANY; 2. 定位当前用户拥有的 Company; 3. 查询 `applicationId` 及对应 Job; 4. 校验 Job 属于当前 Company; 5. 取得投递对应的 `studentUserId`; 6. 按隐私设置判定可见性; 7. 通过后才读取 `student_profiles.resume_url` 和文件内容。 跨企业投递统一返回既有 403 语义,不通过差异响应泄露归属。隐私拒绝使用 `FORBIDDEN(2002)`。不存在的投递保持既有 `APPLICATION_NOT_FOUND`;授权通过但学生没有简历时使用既有 `PROFILE_NOT_FOUND`,不新增 ErrorCode。 ### 4.3 可见性决策表 | 设置 | 企业条件 | 投递条件 | 结果 | |---|---|---|---| | 无设置记录/null | 任意 | 任意 | 403,按 PRIVATE 处理 | | PRIVATE | 任意 | 包括已投递、面试、录用 | 403 | | APPLIED_COMPANIES | 岗位属于当前企业 | SUBMITTED/VIEWED/INTERVIEW/HIRED | 200 | | APPLIED_COMPANIES | 岗位属于当前企业 | WITHDRAWN/REJECTED | 403 | | APPLIED_COMPANIES | 其他企业 | 任意 | 403 | | VERIFIED_COMPANIES | 当前企业 `verified=true` 且拥有该投递岗位 | 任意合法投递状态 | 200 | | VERIFIED_COMPANIES | 未认证企业或其他企业 | 任意 | 403 | `VERIFIED_COMPANIES` 不是“知道学生 ID 即可查看”。它仍要求合法的候选业务上下文,本轮该上下文限定为当前企业拥有的 `applicationId`。 ### 4.4 代码结构 将现有布尔方法收束为失败关闭的授权组件,例如 `CompanyResumeAccessPolicy`。授权组件必须校验 company ownership,不能依赖 Controller 先验,也不能把客户端传入的 `studentUserId/companyId` 当成可信事实。 文件读取放在独立 Service 方法中。Controller 只解析身份、路径参数、构建安全响应头。任何拒绝路径都不得调用 `ResumeStorageService.read()`。 企业候选人列表与详情 DTO 保持不含 `resumeUrl`。学生本人上传、下载、版本、导出接口完全不变。 ## 5. S03-C Actuator 隔离 ### 5.1 运行边界 推荐配置: - `management.server.port=${MANAGEMENT_SERVER_PORT:8081}` - `management.server.address=${MANAGEMENT_SERVER_ADDRESS:127.0.0.1}` - 仅暴露 `health,info,metrics,prometheus`; - `health.show-details=never`; - `health.show-components=never`。 生产默认只绑定回环地址。需要采集 Prometheus 时,由部署层通过受控网络或反向代理访问管理端口,不通过公网业务端口开放。 ### 5.2 业务端口行为 业务端口不得提供 `/actuator/metrics` 或 `/actuator/prometheus`。普通 STUDENT、COMPANY、ADMIN 的业务 JWT 不能把管理指标变成公网资源。健康检查继续提供最小 `UP/DOWN` 信息,不返回数据库、Redis、RabbitMQ、磁盘路径或连接池细节。 本轮不新增运维账号体系,不把 ADMIN 等同于基础设施运维身份。 ## 6. 数据与副作用 允许的数据库变化仅为: - V43 对完全匹配的旧种子账号执行 `is_active: 1 -> 0`; - 显式管理员初始化首次创建一条 ADMIN 用户和对应角色; - 企业成功下载简历如需审计,只允许白名单动作与 `applicationId/companyId`,不得记录路径、文件名、学生 ID、联系方式或简历内容。 简历访问不得修改 `student_profiles`、`resume_versions`、投递、评分、排名、推荐、邀约、消息、通知或风险表,不发送 RabbitMQ、短信或邮件。 ## 7. 契约与兼容性 - S02 基线为 175 paths / 213 operations。 - 新增一个企业简历下载接口后,预期为 176 paths / 214 operations;最终以运行时 OpenAPI 导出为准。 - 不修改既有响应字段,不把 `resumeUrl` 加回任何企业 DTO。 - 不新增 ErrorCode。 - V43 是唯一新增迁移,V1–V42 不得修改。 ## 8. 验收门槛 必须同时通过: - V43 在 H2 快速层和真实 MySQL 8 执行成功; - 仅完全匹配的三条原始种子账号被禁用,改过任一指纹的账号不受影响; - 管理员初始化关闭、成功、重复、已有 ADMIN、手机号冲突、并发冲突场景; - 三种简历可见性及所有投递状态、跨企业、未认证企业、ADMIN/STUDENT/匿名权限矩阵; - 拒绝路径零文件读取,企业 JSON/响应头无路径及个人信息泄漏; - Actuator 业务端口与管理端口真实启动验证; - 全量测试和 `mvn clean verify` 零失败、零错误、零跳过; - 静态与运行时 OpenAPI 一致; - 真实 MySQL/Docker 黑盒通过后才能冻结。