# 统一手机号账号注册与首次身份选择实施报告 ## 当前状态 统一手机号账号注册与首次身份选择本地实施完成,待独立代码复验。 最终全量验证与独立 JVM 最新 JAR 黑盒均通过。没有暂存、提交、推送或部署。 ## 范围与基线 项目为职来后端(项目目录 `zhilai`)。分支 master;开工 HEAD 为 `8c9429108cbd5d85e5245f6c404b2172bf6dd7f3`。 开工已存在 Phase B、验证码 P0、岗位等未提交改动,以及 290 个历史未跟踪条目。本功能叠加于工作区,不能把全部 diff 当成本轮改动。 最新迁移 V46;角色字段是 VARCHAR,无需新增迁移。不实现双身份切换、历史账号转角色、公开管理员注册或 Phase C。 ## 权威库存 原 `AuthService.mockLogin` 为新手机号自动创建 STUDENT;密码登录不负责注册。 `users.role` 与 `user_roles` 都被生产代码读取;JWT 认证以服务端有效会话为准,业务角色由 Service 校验。 `AuthSessionService` 的会话创建/撤销参与调用方事务;刷新按 users → auth_tokens 顺序加锁。 撤销 Repository modifying 操作会清空 persistence context,身份更新必须先 flush。 企业资料复用既有 `CompanyDetails` 的 Validation 约束,开关复用只接受字面量 true 的企业注册配置。 ## 新注册与旧账号兼容 新手机号仍通过 `POST /auth/login` 验证 login 用途短信,创建 UNASSIGNED 主角色和空业务 roles。 登录、密码登录和 `/auth/me` 都新增派生字段 `identitySelectionRequired`,成功业务码仍为 200;旧字段不删除。 既有 STUDENT、COMPANY、ADMIN 不重置、不迁移。实体和数据库的 STUDENT 默认值保留以兼容其他明确的内部创建点,统一注册路径显式覆盖为 UNASSIGNED。 原 `/auth/company/register` 保留为兼容入口,并不强迫旧客户端立即改造。 ## 首次身份选择接口 `POST /auth/identity/select` 必须使用有效 Access Token;身份和 sessionId 取自当前已验证会话。 ```json {"identity":"STUDENT"} ``` ```json {"identity":"COMPANY","company":{"name":"示例企业","industry":"互联网"}} ``` identity 仅允许 STUDENT/COMPANY;COMPANY 必须提供 company,STUDENT 禁止提供 company。 不接受客户端 userId、role、verified 或其他未知字段;嵌套企业资料也不允许非法授权字段。 首次成功返回新 Token 对、userId、role、identitySelectionRequired=false;企业额外返回 companyId 和 UNVERIFIED。 同身份重复请求只返回当前状态、reauthenticationRequired=true,不重复发 Token、修改资料或追加审计。 原会话失效后重复使用旧 Token 返回 401;客户端应采用新凭证,丢失响应时通过原登录方式重新登录。 已经选择另一种身份的账号返回 409,不能用该接口切换身份。 ## 待选身份访问门禁 `IdentityAccessFilter` 只作用于 JWT 过滤器建立的已验证会话主体,在 JWT 之后、MVC 路由和参数解析之前执行。 它读取数据库当前身份,使用精确的 HTTP 方法/路径白名单,未知业务入口默认拒绝。 白名单包含身份查询/选择、登出、密码维护和原匿名认证入口,以及既有公开只读页面。 企业资料、认证申请、测评、消息、投递、候选人、岗位写操作和管理员接口不对 UNASSIGNED 放行。 容器自动注册被禁用,避免过滤器在 JWT 之前执行或重复执行。 公共 `UserRoleGuard.requireRole` 额外拒绝 UNASSIGNED,即使 user_roles 被误写成业务角色也不能通过该 guard。 旧 Controller 接线探针使用的非 JWT 测试主体不新增解析调用;生产仍只有 JWT 认证方式,未启用 Basic/Form 登录。 ## 事务与锁顺序 1. Service 层再次验证 DTO。 2. 锁定 users,重新核验 active、当前会话和注销状态。 3. 确认主角色/roles 无矛盾、没有孤立企业资料;校验企业开关。 4. 更新 users.role 与唯一业务 role,saveAndFlush。 5. COMPANY 创建唯一 verified=false 企业资料并 flush。 6. 撤销全部旧会话,创建带新角色的新会话。 7. 同事务执行 MANDATORY 必需审计,固定事件 IDENTITY_SELECTED,明细只含结果。 任何异常回滚角色、企业和 Token 更改;不返回未提交的凭证。锁顺序与刷新一致,不增加跨请求缓存。 注销 PENDING/COMPLETED 拒绝选择,取消后的 NONE 可继续;禁用账号在锁内再次拒绝。 原验证码原子消费、独立错误计数事务和 Redis 限流不被修改,也不虚称这些 Redis 状态会自动回滚。 ## 错误响应 新增唯一数字码:10401 身份待选/403,10402 身份冲突/409,10403 身份选择依赖不可用/503。 非法 JSON/字段/参数为原 400;匿名、失效会话和非法 Token 为原 401;企业注册开关关闭沿用原 503。 没有修改任何历史错误码数字;历史 9103 重码事实仍保留。 历史映射快照资源未改,只在契约测试中单独登记三个新增映射和相应数量。 ## 修复前证据 生产修改前新增 6 个 HTTP 测试:6 run / 6 failures / 0 errors / 0 skipped。 明确复现:新账号实际为 STUDENT;待选业务请求实际 200;身份选择路径不存在。 失败证据在忽略目录 `docs/acceptance/unified-identity/`,不会进入提交。 ## 实施中发现与修正 - Jackson 全局忽略未知字段,初版 userId 注入请求被接受;改为新 DTO/企业嵌套 DTO 安全拒绝未知字段。 - 初版 MVC 拦截器晚于 multipart/Content-Type 路由匹配;全端点测试定位到格式不符请求先出错,改为安全过滤链门禁。 - 新过滤器初版增加历史 Controller 接线探针的 Accessor 调用;限定为 JWT 已验证会话主体后保留旧精确调用次数断言。 - MySQL 回滚测试初版在事务代理外 stub MANDATORY 审计,导致测试装配失败;改为对代理内部 spy 注入故障,实际调用仍经过真实事务代理。 - Repository 会将故障转换为 DataAccessException,回滚测试明确验证安全业务码 10403,不要求错误穿透为原异常类型。 - 密码 HTTP 测试初版遗漏密码模块开关和 mock Redis 原子脚本返回值;补齐测试前置。真实 Redis 证据由最新 JAR 验收单独提供。 - 新增接口使旧 OpenAPI 数量断言失败;只将数量改为 183/221,保留原简历安全字段和接口断言。 - MySQL Service 并发只产生一份身份,但首次最新 JAR HTTP 并发出现 200/500:失败请求在锁内抛出 InvalidTokenException,新 Controller 未转换。补确定性测试先复现 1 run / 1 failure(expected 401 but was 500),再复用原认证安全响应返回 401;没有改变锁、角色或数据断言。 - JAR 验收脚本初版把 `/auth/sms/send` 的查询参数放进 JSON body,实测缺少 phone 返回 400;按现有 RequestParam 契约修正脚本,未修改生产短信接口。Maven exec 参数需整体引号;RabbitMQ 改用本机已缓存的官方 3.13 management 镜像,未通过反复网络拉取干扰其他服务。 ## 精确本轮文件范围 新增生产文件(4): - `auth/identity/IdentitySelectionRequest.java` - `auth/identity/IdentitySelectionResponse.java` - `auth/identity/IdentitySelectionService.java` - `auth/identity/IdentityAccessFilter.java` 修改生产文件(9):`AuthService`、`User`、`PasswordAuthenticationService`、`UserRoleGuard`、`ErrorCode`、`ErrorHttpStatusRegistry`、`CompanyRegistrationRequest`、`SecurityConfig`、`AuthController`。 其中异常码文件已有 Phase B 改动,本轮只叠加 10401–10403;其他原有 P0/B/岗位文件不被本轮覆盖。 新增测试文件(4):`IdentityPolicyTest`、`UnifiedRegistrationIntegrationTest`、`IdentitySelectionMySqlTest`、`UnifiedIdentityJarAcceptanceProbe`。最后一项为手工最新 JAR 验收 main,不冒充 JUnit 数量。 修改既有测试(8):`AuthServiceTest`、`CompanyControllerMvcTest`、`CompanyOnboardingControllerTest`、`GlobalExceptionHandlerStatusContractTest`、`ErrorHttpStatusRegistryTest`、`GlobalExceptionHandlerMappingParityTest`、`R01BIndependentHttpMappingParityTest`、`CompanyResumeDownloadIntegrationTest`。 修改仅涉及新账号默认身份、新依赖装配、新枚举映射计数和新增接口计数;原角色、归属、隐私及历史映射断言保留。 新增设计与本报告;修改 `openapi-rc.json` 与 `verify_openapi.py`。没有修改迁移、依赖、短信服务商、Jenkins 或线上配置。 ## 测试及真实基础设施 最终定向命令 `mvn -B -Dtest=UnifiedRegistrationIntegrationTest,IdentityPolicyTest,IdentitySelectionMySqlTest test`:55 / F=0 / E=0 / S=0。 其中策略测试 27、HTTP 集成 13、真实 MySQL 15;HTTP 库存测试独立列出公开与账号操作清单,不调用生产白名单生成期望,196 个受限 operation 全部拒绝。 首次全量 test 曾有 7 个失败(旧接口计数 + 旧接线精确次数),修正测试计数与过滤器主体边界后 `mvn test` 为 2952 / F=0 / E=0 / S=0。 之后新增失效会话 HTTP 反证并修复 500,最终 `mvn -B clean verify`:2953 / F=0 / E=0 / S=0 / BUILD SUCCESS,耗时 13:13。 独立读取 284 份 Surefire XML 汇总同为 2953 / 0 / 0 / 0。相对采用的 2889 基线,新增 55 个 JUnit 执行项,既有枚举参数化测试增加 9 项,数量自洽。 真实 MySQL 15 项及既有 Testcontainers 测试均实际执行;没有 Docker 不可用静默跳过。 MySQL/Testcontainers 不设置静默跳过 Docker 的开关。 并发使用 CountDownLatch,不用 sleep 调节竞争。 最新 JAR 验收使用隔离 MySQL 8.4、Redis 7.4、RabbitMQ 3;所有夹具仅在隔离库,不使用真实手机号或线上密钥。 短信 sender 使用受控 Mock,数据库获取测试验证码;不是实际运营商投递,也没有为前端增加 mockCode 旁路。 最新 JAR 三中间件黑盒场景包括:企业开关关闭安全拒绝;UNASSIGNED 空业务 roles;13 张表逐字段快照零写;求职者/企业身份选择;密码登录身份标记;全部旧 Access/Refresh 失效;同身份重试只读;跨身份冲突;未认证企业候选人访问被拒;真实 HTTP 双请求只一个 200、另一个 401;四个 RabbitMQ 队列消息数、ready、unacked 均为 0;精确删除三账号夹具及验证码,审计 append-only。 首次以 Maven exec:java 同 JVM 运行时,场景已产生 66 checks / 0 failures,但 Testcontainers Ryuk 守护线程在插件清理阶段被中断,导致外层 Maven 返回非零;保留该失败事实,不写成 BUILD SUCCESS。 最终改为独立 `java -cp` JVM 执行同一验收 main:再次 66 checks / 0 failures,进程正常退出 0,三容器按生命周期关闭,无插件线程清理异常。 最终结果在被忽略的 `target/identity-probe-result.json`,日志在 `docs/acceptance/unified-identity/jar-probe-standalone.log`;不新增未跟踪 JSON 验收产物。 被验收的最终 JAR SHA-256:`77d0125ba1d20007966be9fa7a7b3f82ef09fb5628b83bb5190262d318720cc9`。最终全量打包后仅重编译手工验收 main 的输出路径调整,没有再修改生产代码。 ## OpenAPI 与前端切换 新增一个 path/operation,静态验证为 183 paths / 221 operations,`verify_openapi.py` All checks passed。 最新 JAR `/v3/api-docs` 的完整 path/method operation 集合与静态文件完全一致,不只比较数量。 前端登录成功后查看 identitySelectionRequired:true 进入身份选择页;false 根据旧 role 进入原工作台。 首次选择成功必须同时替换 Access/Refresh Token,不能继续使用旧 Token。 COMPANY 仍进入原 UNVERIFIED 入驻流程;不能直接进入已认证招聘能力。刷新后如需身份展示,继续查询 `/auth/me`。 新账号已不自动成为求职者,因此部署必须与前端身份选择页协调,不能宣称旧前端零改动兼容。 ## 敏感信息分类 本轮新增源码、测试与两份文档扫描:完整 JWT、字面 BCrypt 摘要、邮箱、PRIVATE KEY、AWS key、本机用户绝对路径均为 0 命中。 手机号命中 22 行、15 个唯一值,全部来自独占的 `199000094xx/199000095xx/199000096xx` 假号段夹具;没有使用用户提供的真实手机号发送短信。 手工验收 main 中的测试密码、JWT 测试密钥和 Testcontainers 口令为显式 test-only 常量,只在隔离库/容器使用,不是生产凭据。 成功 JAR 日志通过自动断言确认不含三条测试手机号、测试明文密码、对应 BCrypt 摘要及指定签发 Token;拒绝响应不含异常类、SQL 连接串或存储字段。 失败的 MockMvc 证据可能包含已失效的本地测试 Token,日志由既有 `**/*.log` 规则忽略,不进入提交;不把这类失败证据宣称为“所有日志零 Token”。 历史启动日志中的本机目录与 Spring generated security password 警告仍属于既有环境输出,本轮未顺手重构该功能,也不声称这些观察项已消失。 ## 边界与结束检查 最终分支 master,HEAD 仍为 `8c9429108cbd5d85e5245f6c404b2172bf6dd7f3`,暂存区空,`git diff --check` 为 0。 V1–V46 与 pom.xml 无改动、无新迁移;WebMvcConfig 内容与 HEAD 一致,没有保留初版拦截器配置。 历史 290 个未跟踪条目未删除,当前 300 = 历史 290 + 本轮 10 个新增文件。原有未提交 Phase B/P0/岗位改动保留,不把它们记成本轮产出。 最新 JAR 两次启动按各自 Process 对象精确停止;最终无本轮 Java 残留,8080/8081 空闲。验收容器自动关闭,Docker 守护进程可保持运行。 三账号及验证码夹具精确清理,清理过程中不删除审计行;审计随隔离容器自然生命周期结束,不触及线上审计数据。四个消息队列在关闭前 messages/ready/unacked 均为 0。 未进入 Phase C;本轮不暂存、不提交、不推送、不部署。上线开关和已有生产账号由后续独立验收与发布检查点另行处理。