You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

8.7 KiB

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 黑盒通过后才能冻结。