14 KiB
14 KiB
ISSUE-01-02:学员基础主数据管理 P0 开发与验收报告
文档定位: 本文档记录 RailwaySystem P0 阶段
ISSUE-01-02 学员主档建档与安全脱敏管理的完整实施与验收结果。 实施分支:feat/issue-01-02-studentIntegration Baseline:4682df03c566776562b693ed5cb48fd98cda2472(integration/wave1a-foundation)
1. 任务目标与交付范围
- 业务目标: 建立全局学员长期主档管理,支撑后续培训名单与学员考勤,落实身份证敏感数据的安全散列查重、密文存储与脱敏掩码展示规范。
- 正式交付 P0 API 列表(严格等于 3 个):
GET /train/student/list(权限:train:student:list)- 分页检索学员列表,身份证掩码脱敏展示。GET /train/student/{sId}(权限:train:student:query)- 获取学员主档详情,身份证默认掩码脱敏展示。POST /train/student(权限:train:student:add)- 学员手工建档录入,可逆加密存储与散列查重。
- 严格不做什么(Out of Scope):
- 不开放
PUT /train/student...(学员信息修改属于 P1 阶段); - 不开放
DELETE /train/student...(未在冻结契约规划内); - 不开放
POST /train/student/export(P0 严禁批量导出明文/密文身份证); - 不开放
POST /train/student/import(Excel 批量导入学员主档属于 P1 阶段); - 不公开任何历史旧 CRUD 别名或未冻结端点,Contract-extra Endpoint 严格为 0。
- 不开放
2. 身份证安全治理红线与技术落地
依据 Database V2(train_student)已冻结的存储模型,彻底杜绝身份证明文落库:
2.1 存储模型与字段语义
| 物理字段 | 类型 | 语义与用途 |
|---|---|---|
id_card_ciphertext |
TEXT | 可逆加密密文,仅为确需恢复身份信息的内部安全边界保留(允许为 NULL) |
id_card_hash |
VARCHAR(128) | 确定性安全散列,用于查重与精确匹配检索(具备 UNIQUE 约束 uq_student_idcard_hash,允许为 NULL) |
id_card_last4 |
VARCHAR(4) | 末四位明文,专用于前端脱敏展示与轻量确认(允许为 NULL) |
2.2 无身份证学员语义支持
Database V2 已明确允许 id_card_ciphertext、id_card_hash、id_card_last4 为 NULL。针对实际业务中可能暂未采集身份证的情形,落实以下统一规则:
StudentCreateDTO与 Service 层全面支持idCard为null、""(空串)或" "(纯空格);- 统一规范化规则:
- 当
idCard == null或idCard.trim().isEmpty()时,统一规范化为 Javanull; - 赋值
idCardCiphertext = null、idCardHash = null、idCardLast4 = null; - 不调用 可逆加密组件;
- 不调用 确定性散列计算;
- 不执行 身份证 Hash 查重查询;
- 写入数据库时严格落为 SQL
NULL,绝对不会写入空字符串''; - 多个无身份证学员建档时,由于各行均为
NULL,在数据库uq_student_idcard_hash约束下互不冲突,绝不触发 40002 重复异常;
- 当
- 只有在身份证非空时,才进入后续的规范化校验、查重、加密及提取 last4 流程。
2.3 规范化(Canonicalization)
为避免因输入格式差异导致相同身份证生成不同散列值(如包含前后空白字符、末位大小写混用),统一非空身份证的规范化规则:
- 执行
trim()去除前后空白字符; - 执行
toUpperCase(Locale.ROOT)将末位校验码统一转换为大写字符(如x->X); - 正则格式校验:
^[0-9]{17}[0-9X]$,保证进入密码组件的身份证格式绝对一致。
2.4 确定性散列(Deterministic Hash)与查重
- 查重散列采用独立的
HmacSHA256算法与专用 Hash 密钥计算,输出 64 位小写十六进制确定性散列值; - 新增学员前执行
SELECT 1 FROM train_student WHERE id_card_hash = ? LIMIT 1进行前置预检; - 遇到重复身份证直接阻断,抛出业务异常
40002 STUDENT_IDCARD_DUPLICATED。
2.5 并发 UNIQUE 冲突兜底
- Database V2 在
train_student.id_card_hash上已建立唯一约束uq_student_idcard_hash; - 在高并发极端情况下,若两个相同身份证请求同时穿透 Service 前置查重并尝试 INSERT,底层抛出 Spring
DuplicateKeyException; - Service 层显式捕获
DuplicateKeyException并统一映射为40002 STUDENT_IDCARD_DUPLICATED,彻底杜绝底层数据库异常裸报 500。
2.6 脱敏掩码规则(Masking)
- 掩码格式:前 6 位 + 8 个星号 + 末 4 位(例如:
110101********1234); - 列表接口(
StudentItemVO)与详情接口(StudentDetailVO)统一仅返回idCardMasked与idCardLast4; - 对于无身份证学员,掩码字段自然返回
null。
2.7 算法表述纠偏(Implementation Decision)
- 算法声明: 当前实现选择
AES/GCM/NoPadding(每次加密生成 12 字节安全随机 IV,版本前缀v1.)与HmacSHA256(确定性散列),这是 Security Implementation 当前阶段的具体工程实现决策,不是 API Contract / PRD 强制指定算法; - 算法实现统一封装在独立的
StudentIdentityProtection接口及JceStudentIdentityProtection组件之后,Controller 与上层业务模块对底层算法完全无感,后续可平滑替换。
2.8 密钥管理、部署注入与 Fail-Closed 原则
- 密钥来源: 生产环境与测试环境严格由外部环境变量或配置动态注入:
train.student.identity.encryption-key: ${TRAIN_STUDENT_IDENTITY_ENCRYPTION_KEY:}train.student.identity.hash-key: ${TRAIN_STUDENT_IDENTITY_HASH_KEY:}
- Fail-Closed: 生产配置缺失密钥或密钥长度不合规时,组件构造直接抛出
IllegalStateException拒绝初始化,代码库和构建产物中严禁保留任何默认密钥、弱密钥或生产 fallback; - 部署与 CI/CD 说明:
- GitLab CI/CD / 部署环境必须以 Secret Variables(或 Kubernetes Secret / 外部配置中心)注入以下两个变量:
TRAIN_STUDENT_IDENTITY_ENCRYPTION_KEYTRAIN_STUDENT_IDENTITY_HASH_KEY
- 严禁将真实密钥写入代码库或提交至 Git 仓库。
- GitLab CI/CD / 部署环境必须以 Secret Variables(或 Kubernetes Secret / 外部配置中心)注入以下两个变量:
- 单元测试提供专用包级构造器注入独立的 test-only secret,测试密钥与生产环境彻底物理隔离。
2.9 全方位防泄漏防线
- 持久化防线:
train_student无任何身份证明文字段,密文与散列分别入库,支持安全为 NULL; - DTO / 请求防线:
StudentCreateDTO.idCard声明@JsonProperty(access = Access.WRITE_ONLY)与@JSONField(serialize = false),其toString()仅打印是否包含标识(idCardProvided=true),绝不打印号码; - Query 防线:
StudentQuery最终完整字段仅包含sName,orgName,sPhone,status,严禁且不包含 任何身份证明文字段,防止通过 URL、代理访问日志或 RuoYi 访问日志泄漏证件号; - VO / 响应防线:
StudentItemVO与StudentDetailVO仅包含idCardMasked与idCardLast4,无密文、哈希或明文字段; - Domain 实体防线:
TrainStudent的idCardCiphertext与idCardHash标注@JsonIgnore;重写的toString()明确剔除密文、哈希与明文,仅保留idCardLast4; - 操作日志防线: Controller 录入接口明确配置
@Log(..., isSaveRequestData = false, isSaveResponseData = true),防止 RuoYi 操作日志落库泄露请求体中的明文身份证; - Excel 导出防线:
TrainStudent彻底移除历史敏感@Excel注解,本 Issue 不开放导出端点; - 异常堆栈防线: 业务异常信息(如 40002 提示)为固定文案,严禁拼接或携带身份证号。
3. DTO / VO / Domain 模型目录
| 类型 | 路径 | 职责边界 |
|---|---|---|
StudentCreateDTO |
com.ruoyi.train.domain.dto.StudentCreateDTO |
学员录入输入模型,身份证为选填只写属性,严格校验格式与长度 |
StudentQuery |
com.ruoyi.train.domain.query.StudentQuery |
学员分页检索条件(仅含 sName, orgName, sPhone, status,无任何证件字段) |
StudentItemVO |
com.ruoyi.train.domain.vo.StudentItemVO |
列表轻量脱敏视图,包含 idCardMasked 与 idCardLast4 |
StudentDetailVO |
com.ruoyi.train.domain.vo.StudentDetailVO |
详情脱敏视图,继承 StudentItemVO 并扩充人脸凭证、备注及创建审计信息 |
TrainStudent |
com.ruoyi.train.domain.TrainStudent |
数据库持久化实体,仅在 Mapper 与 Service 内部流转,严禁直接暴露给 Controller |
4. 业务错误码定义
对齐 docs/api-v2/10_permission_and_error_codes.md:
| 错误码 | 标识常量 | HTTP Status | 说明与触发场景 |
|---|---|---|---|
| 40002 | STUDENT_IDCARD_DUPLICATED |
200 (业务码) | 学员身份证号已建档,不可重复录入(散列查重或并发唯一约束命中) |
| 40402 | STUDENT_NOT_FOUND |
200 (业务码) | 查询指定 sId 学员不存在或已被逻辑删除时抛出 |
5. Generator 策略说明
- 冻结策略: PARTIAL
- 本轮实际执行情况: 未重新执行代码生成器。本 Issue 延续了在既有工程结构上的精细化手写实现,保留了规范的 DTO/VO 隔离与安全密码学边界,避免因机械生成覆盖已有的安全加固代码。
- Database V2 状态: 未做任何 DDL 修改,完全适配现有
train_student表结构与约束。
6. 前端 P0-Only 改造与构建验证
- 视图文件:
build_ui/src/views/train/student/index.vue - API 客户端:
build_ui/src/api/train/student.js - 功能保留范围:
- 学员列表分页检索与展示(展示脱敏身份证号
idCardMasked); - 学员详情弹窗(只读查看脱敏身份证及学员详细档案);
- 学员新增表单(身份证字段选填,输入时前端进行 18 位身份证号码正则校验与姓名必填校验)。
- 学员列表分页检索与展示(展示脱敏身份证号
- 完全关闭与剔除的能力:
- 剔除编辑按钮、更新方法及
PUTAPI 调用; - 剔除删除按钮、删除方法及
DELETEAPI 调用; - 剔除导出按钮及
exportAPI 调用; - 表单提交后清空敏感明文字段,前端无
console.log身份证日志。
- 剔除编辑按钮、更新方法及
- 构建环境治理:
- ESLint 检查:
./node_modules/.bin/eslint --no-ignore src/api/train/student.js src/views/train/student/index.vue零 Warning / 零 Error; - 生产构建:
npm run build:prod顺利构建成功(DONE Build complete); - 构建验证结束后已立即清理临时软链接与
dist构建物。
- ESLint 检查:
7. 自动化测试与验收结果
7.1 Student 专项测试套件(共 20 个用例,全部通过)
JceStudentIdentityProtectionTest(6 个测试用例):shouldEncryptReversiblyWithoutPersistingPlaintext: 验证 AES-GCM 随机 IV 可逆加密,密文不含明文,相同明文产生不同密文且能正确解密;shouldProduceDeterministicHashAndFrozenMask: 验证 HMAC-SHA256 散列确定性计算及110101********1234掩码规范;missingEncryptionKeyShouldFailClosed: 验证加密密钥缺失时 fail-closed 抛出IllegalStateException;invalidEncryptionKeyLengthShouldFailClosed: 验证加密密钥长度无效时抛出异常;missingHashKeyShouldFailClosed: 验证散列密钥缺失时 fail-closed 抛出IllegalStateException;invalidHashKeyLengthShouldFailClosed: 验证散列密钥过短时抛出异常。
TrainStudentServiceImplTest(11 个测试用例):createShouldPersistOnlyProtectedIdentityMaterial: 验证正常录入学员、s_id自动生成、明文不持久化、密文与散列落库、last4提取正确;duplicateIdentityHashShouldBeBlockedWithFrozenCode: 验证相同身份证重复录入抛出40002 STUDENT_IDCARD_DUPLICATED;canonicalizedXxShouldBeDuplicateBlocked: 验证小写x与大写X(含首尾空格)规范化后一致查重阻断(40002);databaseUniqueRaceShouldAlsoMapToDuplicateCode: 验证并发冲突触发DuplicateKeyException时映射为40002;listAndDetailShouldExposeMaskedIdentityOnly: 验证列表与详情均只暴露idCardMasked与idCardLast4;missingStudentShouldReturnFrozenNotFoundCode: 验证查询不存在学员抛出40402 STUDENT_NOT_FOUND;logsAndDtoStringShouldNotContainPlainIdentity: 验证 DTO、实体与异常的toString()和 FastJSON 序列化均不泄漏明文;domainJacksonSerializationShouldNotLeakSensitiveFields: 验证 Jackson 序列化TrainStudent时@JsonIgnore生效,密文与哈希不外泄;testCreateStudentWithoutIdCard: 验证无身份证学员正常创建,密文/哈希/last4 均为 null;testCreateMultipleStudentsWithoutIdCard: 验证多个无身份证学员同时建档成功,不触发 40002 重复异常;testBlankIdCardNormalizedToNull: 验证传入空字符串""或空白字符串" "规范化为 null 入库。
StudentApiTest(3 个测试用例):shouldExposeFrozenP0MethodsAndPermissions: 验证 Controller 暴露的 3 个方法路径、注解、权限标识(train:student:*)以及@Log(isSaveRequestData = false);controllerMustNotExposePersistenceDomain: 验证 Controller 的出参入参均不暴露持久化实体TrainStudent;controllerMustNotExposeUnfrozenEndpoints: 验证 Controller 严格只有 3 个端点,绝对不存在任何PUT、DELETE或export方法。
7.2 全量工程验证汇总
- Maven Test:
mvn test -pl ruoyi-train,ruoyi-admin -am -f RuoYi-Vue/pom.xml-> Total tests run: 34, Failures: 0, Errors: 0, Skipped: 0 (BUILD SUCCESS) - Maven Compile:
mvn compile -DskipTests -f RuoYi-Vue/pom.xml-> 8/8 Reactor Modules 全部 SUCCESS - 前端 ESLint: 0 error, 0 warning
- 前端 Build:
npm run build:prod-> 成功通过