Compare commits
23 Commits
master
...
dev-V1.0.9
| Author | SHA1 | Date |
|---|---|---|
|
|
d349dce593 | 5 days ago |
|
|
b724381de5 | 1 week ago |
|
|
bc669e7562 | 1 week ago |
|
|
b0269fcafa | 2 weeks ago |
|
|
2d64488882 | 2 weeks ago |
|
|
41989c6499 | 2 weeks ago |
|
|
850dc33081 | 2 weeks ago |
|
|
ea8fbc94f5 | 2 weeks ago |
|
|
3e01cb2d1f | 2 weeks ago |
|
|
8d26e6d149 | 2 weeks ago |
|
|
c3c82361a0 | 3 weeks ago |
|
|
694146a501 | 3 weeks ago |
|
|
c15949dc2e | 1 month ago |
|
|
004bbbabff | 1 month ago |
|
|
3ea55f30f8 | 1 month ago |
|
|
dcfce16959 | 1 month ago |
|
|
e1aba0df9f | 1 month ago |
|
|
9636cbc8a9 | 1 month ago |
|
|
2ddcbaae7e | 1 month ago |
|
|
ba5ecf719c | 1 month ago |
|
|
59d47f9be1 | 1 month ago |
|
|
ced9ce3fec | 1 month ago |
|
|
934c112976 | 1 month ago |
70 changed files with 3586 additions and 155 deletions
@ -0,0 +1,67 @@ |
|||
-- 通用密评敏感字段密文存储表 |
|||
CREATE TABLE crypto_sensitive_data ( |
|||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', |
|||
table_name VARCHAR(64) NOT NULL COMMENT '业务表名,如 sys_user', |
|||
biz_id VARCHAR(128) NOT NULL COMMENT '业务表主键ID,字符串兼容数字/UUID主键', |
|||
field_code VARCHAR(64) NOT NULL COMMENT '业务字段编码,username/id_card/phone/password', |
|||
field_value VARCHAR(512) DEFAULT NULL COMMENT '字段原始值', |
|||
cipher_text TEXT COMMENT 'SM4-CBC密文Base64;password单向哈希场景此字段为空', |
|||
field_iv VARCHAR(64) COMMENT 'SM4-CBC初始化向量,16字节随机值Base64;password此字段为空', |
|||
plain_hash VARCHAR(128) COMMENT '明文SM3哈希,仅用于等值查询,不可逆', |
|||
field_salt VARCHAR(64) COMMENT '哈希盐值,仅password单向哈希字段使用', |
|||
encrypt_alg VARCHAR(32) NOT NULL DEFAULT 'SM4-CBC' COMMENT '加密算法:SM4-CBC / SM3', |
|||
key_version VARCHAR(32) NOT NULL COMMENT '密钥版本号,用于密钥轮换,密评必查', |
|||
is_active TINYINT NOT NULL DEFAULT 1 COMMENT '是否为当前有效密文:1有效,0历史旧密文(密钥轮换保留)', |
|||
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0未删除,1已删除', |
|||
create_by BIGINT COMMENT '创建人ID', |
|||
update_by BIGINT COMMENT '更新人ID', |
|||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
|||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
|||
UNIQUE KEY uk_table_biz_field (table_name, biz_id, field_code, is_active, is_deleted), |
|||
KEY idx_table_bizid (table_name, biz_id), |
|||
KEY idx_plain_hash (plain_hash), |
|||
KEY idx_key_version (key_version), |
|||
KEY idx_field_code (field_code) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='密评敏感字段密文存储表'; |
|||
|
|||
-- 密评敏感操作审计表(记录解密、新增、修改、密钥轮换,满足密码应用审计) |
|||
CREATE TABLE crypto_operation_audit ( |
|||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', |
|||
sensitive_data_id BIGINT COMMENT '关联 crypto_sensitive_data 主键ID', |
|||
table_name VARCHAR(64) NOT NULL COMMENT '业务表名', |
|||
biz_id VARCHAR(128) NOT NULL COMMENT '业务记录主键', |
|||
field_code VARCHAR(64) NOT NULL COMMENT '敏感字段编码', |
|||
operate_type TINYINT NOT NULL COMMENT '操作类型:1新增密文 2解密查看 3修改密文 4密钥轮换', |
|||
operator_id BIGINT COMMENT '操作人ID(系统用户ID)', |
|||
operator_ip VARCHAR(64) COMMENT '操作客户端IP地址', |
|||
operate_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作发生时间', |
|||
remark TEXT COMMENT '操作备注信息', |
|||
is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除', |
|||
KEY idx_sensitive_data_id (sensitive_data_id), |
|||
KEY idx_table_bizid (table_name, biz_id), |
|||
KEY idx_operate_time (operate_time), |
|||
KEY idx_operator_id (operator_id) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='密评敏感字段操作审计表'; |
|||
|
|||
-- 通用数据完整性校验表(密评 GB/T 39786 数据完整性) |
|||
CREATE TABLE crypto_integrity_data ( |
|||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', |
|||
table_name VARCHAR(64) NOT NULL COMMENT '业务表名,如 sys_user', |
|||
biz_id VARCHAR(128) NOT NULL COMMENT '业务记录主键,字符串兼容数字/UUID', |
|||
field_list TEXT NOT NULL COMMENT '参与完整性摘要的字段列表,逗号分隔:username,id_card,phone', |
|||
sm3_hash VARCHAR(128) NOT NULL COMMENT '业务字段拼接明文的SM3摘要值,用于完整性校验', |
|||
hash_alg VARCHAR(32) NOT NULL DEFAULT 'SM3' COMMENT '完整性哈希算法:SM3', |
|||
generate_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '摘要生成时间', |
|||
last_verify_time DATETIME NULL COMMENT '最近一次完整性校验时间', |
|||
verify_result TINYINT NULL COMMENT '校验结果:0未校验 1校验通过 2校验失败(篡改)', |
|||
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除', |
|||
create_by BIGINT COMMENT '创建人ID', |
|||
update_by BIGINT COMMENT '更新人ID', |
|||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', |
|||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', |
|||
-- 唯一约束:一张业务表,一条业务记录,仅保留一条有效完整性摘要 |
|||
UNIQUE KEY uk_table_biz (table_name, biz_id, is_deleted), |
|||
KEY idx_table_bizid (table_name, biz_id), |
|||
KEY idx_verify_result (verify_result), |
|||
KEY idx_generate_time (generate_time) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据完整性校验表'; |
|||
@ -0,0 +1,205 @@ |
|||
package com.zdxt.enforcementcode.domain.bean; |
|||
|
|||
import org.dromara.common.mybatis.core.domain.BaseEntity; |
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
|
|||
import java.io.Serial; |
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 执法人员信息对象 s_gdsztk_y2010_dhr |
|||
* |
|||
* @author 庄锐波 |
|||
* @date 2026-10-16 |
|||
*/ |
|||
@Data |
|||
@TableName("s_gdsztk_y2010_dhr") |
|||
public class GdsztkY2010Dhr { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 标识ID |
|||
*/ |
|||
@TableId(value = "ID") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
private String y201000; |
|||
|
|||
/** |
|||
* 执法主体基本信息主键ID |
|||
*/ |
|||
private String z202000; |
|||
|
|||
/** |
|||
* 受委托组织基本信息ID |
|||
*/ |
|||
private String z203000; |
|||
|
|||
/** |
|||
* 姓名 |
|||
*/ |
|||
private String y201001; |
|||
|
|||
/** |
|||
* 所属主体名称 |
|||
*/ |
|||
private String y201002; |
|||
|
|||
/** |
|||
* 所属主体统一社会信用代码 |
|||
*/ |
|||
private String y201003; |
|||
|
|||
/** |
|||
* 照片 |
|||
*/ |
|||
private String y201004; |
|||
|
|||
/** |
|||
* 身份证号 |
|||
*/ |
|||
private String y201005; |
|||
|
|||
/** |
|||
* 性别 |
|||
*/ |
|||
private String y201006; |
|||
|
|||
/** |
|||
* 出生日期 |
|||
*/ |
|||
private Date y201007; |
|||
|
|||
/** |
|||
* 政治面貌 |
|||
*/ |
|||
private String y201008; |
|||
|
|||
/** |
|||
* 职级 |
|||
*/ |
|||
private String y201009; |
|||
|
|||
/** |
|||
* 民族 |
|||
*/ |
|||
private String y201010; |
|||
|
|||
/** |
|||
* 最高学历 |
|||
*/ |
|||
private String y201011; |
|||
|
|||
/** |
|||
* 最高学历专业 |
|||
*/ |
|||
private String y201012; |
|||
|
|||
/** |
|||
* 执法人员性质 |
|||
*/ |
|||
private String y201013; |
|||
|
|||
/** |
|||
* 是否监督人员 |
|||
*/ |
|||
private Integer y201014; |
|||
|
|||
/** |
|||
* 是否具有法律职业资格 |
|||
*/ |
|||
private Integer y201015; |
|||
|
|||
/** |
|||
* 联系电话 |
|||
*/ |
|||
private String y201016; |
|||
|
|||
/** |
|||
* 是否公开 |
|||
*/ |
|||
private Integer y201017; |
|||
|
|||
/** |
|||
* 数据编目挂接单位名称 |
|||
*/ |
|||
private String y201018; |
|||
|
|||
/** |
|||
* 数据编目挂接单位统一社会信用代码 |
|||
*/ |
|||
private String y201019; |
|||
|
|||
/** |
|||
* 数据编目挂接单位行政区划 |
|||
*/ |
|||
private String y201020; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
private Date addTime; |
|||
|
|||
/** |
|||
* 变更时间 |
|||
*/ |
|||
private Date cdTime; |
|||
|
|||
/** |
|||
* 增量标识 |
|||
*/ |
|||
private String cdOperation; |
|||
|
|||
/** |
|||
* 批次号 |
|||
*/ |
|||
private String cdBatch; |
|||
|
|||
/** |
|||
* 数据所属单位区划 |
|||
*/ |
|||
private String optAreaCode; |
|||
|
|||
/** |
|||
* 数据所属单位领域 |
|||
*/ |
|||
private String optFieldCode; |
|||
|
|||
/** |
|||
* 数据所属单位主体ID |
|||
*/ |
|||
private String optSubjectId; |
|||
|
|||
/** |
|||
* 数据所属单位主体名称 |
|||
*/ |
|||
private String optSubjectName; |
|||
|
|||
/** |
|||
* 数据所属单位部门ID |
|||
*/ |
|||
private String optDeptId; |
|||
|
|||
/** |
|||
* 数据所属单位部门名称 |
|||
*/ |
|||
private String optDeptName; |
|||
|
|||
/** |
|||
* 运营方技术字段,非业务字段,请勿使用 |
|||
*/ |
|||
private Date dmpShareCdTimeIdatat; |
|||
|
|||
/** |
|||
* 用数库自动配置的入库时间技术字段,非业务字段,请勿挂接 |
|||
*/ |
|||
private Date dataSharingCdTimeIdatat; |
|||
|
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
package com.zdxt.enforcementcode.domain.bean; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 执法人员证件信息对象 s_gdsztk_y2020_jb2 |
|||
* |
|||
* @author 庄锐波 |
|||
* @date 2026-10-16 |
|||
*/ |
|||
@Data |
|||
@TableName("s_gdsztk_y2020_jb2") |
|||
public class GdsztkY2020Jb2 { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 标识ID |
|||
*/ |
|||
@TableId(value = "ID") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
private String y202000; |
|||
|
|||
/** |
|||
* 执法人员基本信息主键ID |
|||
*/ |
|||
private String y201000; |
|||
|
|||
/** |
|||
* 证件编号 |
|||
*/ |
|||
private String y202001; |
|||
|
|||
/** |
|||
* 持证人姓名 |
|||
*/ |
|||
private String y202002; |
|||
|
|||
/** |
|||
* 证件类型 |
|||
*/ |
|||
private String y202003; |
|||
|
|||
/** |
|||
* 发证日期 |
|||
*/ |
|||
private Date y202004; |
|||
|
|||
/** |
|||
* 颁发机关 |
|||
*/ |
|||
private String y202005; |
|||
|
|||
/** |
|||
* 证件状态 |
|||
*/ |
|||
private String y202006; |
|||
|
|||
/** |
|||
* 执法区域 |
|||
*/ |
|||
private String y202007; |
|||
|
|||
/** |
|||
* 数据编目挂接单位名称 |
|||
*/ |
|||
private String y202008; |
|||
|
|||
/** |
|||
* 数据编目挂接单位统一社会信用代码 |
|||
*/ |
|||
private String y202009; |
|||
|
|||
/** |
|||
* 数据编目挂接单位行政区划 |
|||
*/ |
|||
private String y202010; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
private Date addTime; |
|||
|
|||
/** |
|||
* 变更时间 |
|||
*/ |
|||
private Date cdTime; |
|||
|
|||
/** |
|||
* 增量标识 |
|||
*/ |
|||
private String cdOperation; |
|||
|
|||
/** |
|||
* 批次号 |
|||
*/ |
|||
private String cdBatch; |
|||
|
|||
/** |
|||
* 数据所属单位区划 |
|||
*/ |
|||
private String optAreaCode; |
|||
|
|||
/** |
|||
* 数据所属单位领域 |
|||
*/ |
|||
private String optFieldCode; |
|||
|
|||
/** |
|||
* 数据所属单位主体ID |
|||
*/ |
|||
private String optSubjectId; |
|||
|
|||
/** |
|||
* 数据所属单位主体名称 |
|||
*/ |
|||
private String optSubjectName; |
|||
|
|||
/** |
|||
* 数据所属单位部门ID |
|||
*/ |
|||
private String optDeptId; |
|||
|
|||
/** |
|||
* 数据所属单位部门名称 |
|||
*/ |
|||
private String optDeptName; |
|||
|
|||
/** |
|||
* 运营方技术字段,非业务字段,请勿使用 |
|||
*/ |
|||
private Date dmpShareCdTimeIdatat; |
|||
|
|||
/** |
|||
* 用数库自动配置的入库时间技术字段,非业务字段,请勿挂接 |
|||
*/ |
|||
private Date dataSharingCdTimeIdatat; |
|||
|
|||
} |
|||
@ -0,0 +1,233 @@ |
|||
package com.zdxt.enforcementcode.domain.vo; |
|||
|
|||
import java.util.Date; |
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
|
|||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
|||
import cn.idev.excel.annotation.ExcelProperty; |
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2010Dhr; |
|||
import io.github.linpeilie.annotations.AutoMapper; |
|||
import lombok.Data; |
|||
import org.dromara.common.sensitive.annotation.Sensitive; |
|||
import org.dromara.common.sensitive.core.SensitiveStrategy; |
|||
|
|||
/** |
|||
* 执法人员信息视图对象 s_gdsztk_y2010_dhr |
|||
* |
|||
* @author 庄锐波 |
|||
* @date 2026-10-16 |
|||
*/ |
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
@AutoMapper(target = GdsztkY2010Dhr.class) |
|||
public class GdsztkY2010DhrVo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 标识ID |
|||
*/ |
|||
@ExcelProperty(value = "标识ID") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@ExcelProperty(value = "主键ID") |
|||
private String y201000; |
|||
|
|||
/** |
|||
* 执法主体基本信息主键ID |
|||
*/ |
|||
@ExcelProperty(value = "执法主体基本信息主键ID") |
|||
private String z202000; |
|||
|
|||
/** |
|||
* 受委托组织基本信息ID |
|||
*/ |
|||
@ExcelProperty(value = "受委托组织基本信息ID") |
|||
private String z203000; |
|||
|
|||
/** |
|||
* 姓名 |
|||
*/ |
|||
@ExcelProperty(value = "姓名") |
|||
private String y201001; |
|||
|
|||
/** |
|||
* 所属主体名称 |
|||
*/ |
|||
@ExcelProperty(value = "所属主体名称") |
|||
private String y201002; |
|||
|
|||
/** |
|||
* 所属主体统一社会信用代码 |
|||
*/ |
|||
@ExcelProperty(value = "所属主体统一社会信用代码") |
|||
private String y201003; |
|||
|
|||
/** |
|||
* 照片(不参与Excel导出) |
|||
*/ |
|||
private String y201004; |
|||
|
|||
/** |
|||
* 身份证号 |
|||
*/ |
|||
@ExcelProperty(value = "身份证号") |
|||
private String y201005; |
|||
|
|||
/** |
|||
* 性别 |
|||
*/ |
|||
@ExcelProperty(value = "性别") |
|||
private String y201006; |
|||
|
|||
/** |
|||
* 出生日期 |
|||
*/ |
|||
@ExcelProperty(value = "出生日期") |
|||
private Date y201007; |
|||
|
|||
/** |
|||
* 政治面貌 |
|||
*/ |
|||
@ExcelProperty(value = "政治面貌") |
|||
private String y201008; |
|||
|
|||
/** |
|||
* 职级 |
|||
*/ |
|||
@ExcelProperty(value = "职级") |
|||
private String y201009; |
|||
|
|||
/** |
|||
* 民族 |
|||
*/ |
|||
@ExcelProperty(value = "民族") |
|||
private String y201010; |
|||
|
|||
/** |
|||
* 最高学历 |
|||
*/ |
|||
@ExcelProperty(value = "最高学历") |
|||
private String y201011; |
|||
|
|||
/** |
|||
* 最高学历专业 |
|||
*/ |
|||
@ExcelProperty(value = "最高学历专业") |
|||
private String y201012; |
|||
|
|||
/** |
|||
* 执法人员性质 |
|||
*/ |
|||
@ExcelProperty(value = "执法人员性质") |
|||
private String y201013; |
|||
|
|||
/** |
|||
* 是否监督人员 |
|||
*/ |
|||
@ExcelProperty(value = "是否监督人员") |
|||
private Integer y201014; |
|||
|
|||
/** |
|||
* 是否具有法律职业资格 |
|||
*/ |
|||
@ExcelProperty(value = "是否具有法律职业资格") |
|||
private Integer y201015; |
|||
|
|||
/** |
|||
* 联系电话 |
|||
*/ |
|||
@Sensitive(strategy = SensitiveStrategy.PHONE) |
|||
@ExcelProperty(value = "联系电话") |
|||
private String y201016; |
|||
|
|||
/** |
|||
* 是否公开 |
|||
*/ |
|||
@ExcelProperty(value = "是否公开") |
|||
private Integer y201017; |
|||
|
|||
/** |
|||
* 数据编目挂接单位名称 |
|||
*/ |
|||
@ExcelProperty(value = "数据编目挂接单位名称") |
|||
private String y201018; |
|||
|
|||
/** |
|||
* 数据编目挂接单位统一社会信用代码 |
|||
*/ |
|||
@ExcelProperty(value = "数据编目挂接单位统一社会信用代码") |
|||
private String y201019; |
|||
|
|||
/** |
|||
* 数据编目挂接单位行政区划 |
|||
*/ |
|||
@ExcelProperty(value = "数据编目挂接单位行政区划") |
|||
private String y201020; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
@ExcelProperty(value = "创建时间") |
|||
private Date addTime; |
|||
|
|||
/** |
|||
* 变更时间 |
|||
*/ |
|||
@ExcelProperty(value = "变更时间") |
|||
private Date cdTime; |
|||
|
|||
/** |
|||
* 增量标识 |
|||
*/ |
|||
@ExcelProperty(value = "增量标识") |
|||
private String cdOperation; |
|||
|
|||
/** |
|||
* 批次号 |
|||
*/ |
|||
@ExcelProperty(value = "批次号") |
|||
private String cdBatch; |
|||
|
|||
/** |
|||
* 数据所属单位区划 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位区划") |
|||
private String optAreaCode; |
|||
|
|||
/** |
|||
* 数据所属单位领域 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位领域") |
|||
private String optFieldCode; |
|||
|
|||
/** |
|||
* 数据所属单位主体ID |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位主体ID") |
|||
private String optSubjectId; |
|||
|
|||
/** |
|||
* 数据所属单位主体名称 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位主体名称") |
|||
private String optSubjectName; |
|||
|
|||
/** |
|||
* 数据所属单位部门ID |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位部门ID") |
|||
private String optDeptId; |
|||
|
|||
/** |
|||
* 数据所属单位部门名称 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位部门名称") |
|||
private String optDeptName; |
|||
|
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
package com.zdxt.enforcementcode.domain.vo; |
|||
|
|||
import java.util.Date; |
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
|
|||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
|||
import cn.idev.excel.annotation.ExcelProperty; |
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2020Jb2; |
|||
import io.github.linpeilie.annotations.AutoMapper; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 执法人员证件信息视图对象 s_gdsztk_y2020_jb2 |
|||
* |
|||
* @author 庄锐波 |
|||
* @date 2026-10-16 |
|||
*/ |
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
@AutoMapper(target = GdsztkY2020Jb2.class) |
|||
public class GdsztkY2020Jb2Vo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 标识ID |
|||
*/ |
|||
@ExcelProperty(value = "标识ID") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@ExcelProperty(value = "主键ID") |
|||
private String y202000; |
|||
|
|||
/** |
|||
* 执法人员基本信息主键ID |
|||
*/ |
|||
@ExcelProperty(value = "执法人员基本信息主键ID") |
|||
private String y201000; |
|||
|
|||
/** |
|||
* 证件编号 |
|||
*/ |
|||
@ExcelProperty(value = "证件编号") |
|||
private String y202001; |
|||
|
|||
/** |
|||
* 持证人姓名 |
|||
*/ |
|||
@ExcelProperty(value = "持证人姓名") |
|||
private String y202002; |
|||
|
|||
/** |
|||
* 证件类型 |
|||
*/ |
|||
@ExcelProperty(value = "证件类型") |
|||
private String y202003; |
|||
|
|||
/** |
|||
* 发证日期 |
|||
*/ |
|||
@ExcelProperty(value = "发证日期") |
|||
private Date y202004; |
|||
|
|||
/** |
|||
* 颁发机关 |
|||
*/ |
|||
@ExcelProperty(value = "颁发机关") |
|||
private String y202005; |
|||
|
|||
/** |
|||
* 证件状态 |
|||
*/ |
|||
@ExcelProperty(value = "证件状态") |
|||
private String y202006; |
|||
|
|||
/** |
|||
* 执法区域 |
|||
*/ |
|||
@ExcelProperty(value = "执法区域") |
|||
private String y202007; |
|||
|
|||
/** |
|||
* 数据编目挂接单位名称 |
|||
*/ |
|||
@ExcelProperty(value = "数据编目挂接单位名称") |
|||
private String y202008; |
|||
|
|||
/** |
|||
* 数据编目挂接单位统一社会信用代码 |
|||
*/ |
|||
@ExcelProperty(value = "数据编目挂接单位统一社会信用代码") |
|||
private String y202009; |
|||
|
|||
/** |
|||
* 数据编目挂接单位行政区划 |
|||
*/ |
|||
@ExcelProperty(value = "数据编目挂接单位行政区划") |
|||
private String y202010; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
@ExcelProperty(value = "创建时间") |
|||
private Date addTime; |
|||
|
|||
/** |
|||
* 变更时间 |
|||
*/ |
|||
@ExcelProperty(value = "变更时间") |
|||
private Date cdTime; |
|||
|
|||
/** |
|||
* 增量标识 |
|||
*/ |
|||
@ExcelProperty(value = "增量标识") |
|||
private String cdOperation; |
|||
|
|||
/** |
|||
* 批次号 |
|||
*/ |
|||
@ExcelProperty(value = "批次号") |
|||
private String cdBatch; |
|||
|
|||
/** |
|||
* 数据所属单位区划 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位区划") |
|||
private String optAreaCode; |
|||
|
|||
/** |
|||
* 数据所属单位领域 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位领域") |
|||
private String optFieldCode; |
|||
|
|||
/** |
|||
* 数据所属单位主体ID |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位主体ID") |
|||
private String optSubjectId; |
|||
|
|||
/** |
|||
* 数据所属单位主体名称 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位主体名称") |
|||
private String optSubjectName; |
|||
|
|||
/** |
|||
* 数据所属单位部门ID |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位部门ID") |
|||
private String optDeptId; |
|||
|
|||
/** |
|||
* 数据所属单位部门名称 |
|||
*/ |
|||
@ExcelProperty(value = "数据所属单位部门名称") |
|||
private String optDeptName; |
|||
|
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.passwordparty; |
|||
|
|||
public class KeyPairVo { |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package com.zdxt.enforcementcode.mapper; |
|||
|
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2010Dhr; |
|||
import com.zdxt.enforcementcode.domain.vo.GdsztkY2010DhrVo; |
|||
import org.apache.ibatis.annotations.Param; |
|||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; |
|||
|
|||
/** |
|||
* 执法人员信息Mapper接口 |
|||
* |
|||
* @author 庄锐波 |
|||
* @date 2026-10-16 |
|||
*/ |
|||
public interface GdsztkY2010DhrMapper extends BaseMapperPlus<GdsztkY2010Dhr, GdsztkY2010DhrVo> { |
|||
|
|||
/** |
|||
* 根据身份证号码查询最新的一条记录数据 |
|||
* |
|||
* @param idCard 身份证号码 |
|||
* @return 最新的一条执法人员信息 |
|||
*/ |
|||
GdsztkY2010Dhr queryLatestByIdCard(@Param("idCard") String idCard); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package com.zdxt.enforcementcode.mapper; |
|||
|
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2020Jb2; |
|||
import com.zdxt.enforcementcode.domain.vo.GdsztkY2020Jb2Vo; |
|||
import org.apache.ibatis.annotations.Param; |
|||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; |
|||
|
|||
/** |
|||
* 执法人员证件信息Mapper接口 |
|||
* |
|||
* @author 庄锐波 |
|||
* @date 2026-10-16 |
|||
*/ |
|||
public interface GdsztkY2020Jb2Mapper extends BaseMapperPlus<GdsztkY2020Jb2, GdsztkY2020Jb2Vo> { |
|||
|
|||
/** |
|||
* 根据执法人员基本信息主键ID查询证件状态有效(Y202006=01)的最新一条记录 |
|||
* |
|||
* @param y201000 执法人员基本信息主键ID |
|||
* @return 最新的一条执法人员证件信息 |
|||
*/ |
|||
GdsztkY2020Jb2 queryLatestByY201000(@Param("y201000") String y201000); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
|
|||
package com.zdxt.enforcementcode.quartz.job; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.system.service.ICryptoSensitiveHsmService; |
|||
import org.dromara.system.service.IPasswordPartyService; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
/** |
|||
* 密码平台 |
|||
* <p>完整性校验</p> |
|||
* |
|||
* @author zrb |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class CryptoIntegrityHmackJob { |
|||
|
|||
private final ICryptoSensitiveHsmService cryptoSensitiveHsmService; |
|||
public void execute(){ |
|||
this.hmac(); |
|||
} |
|||
|
|||
public void hmac(){ |
|||
String [] tables = {"sys_user","sys_oper_log"}; |
|||
for (int i = 0; i < tables.length; i++) { |
|||
log.info("数据完整性校验表开始{}", tables[i]); |
|||
cryptoSensitiveHsmService.verifyIntegrityHash(tables[i]); |
|||
log.info("数据完整性校验表结束{}", tables[i]); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
package com.zdxt.enforcementcode.quartz.job; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.system.service.ICryptoSensitiveHsmService; |
|||
import org.dromara.system.service.IPasswordPartyService; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
/** |
|||
* 密码平台 |
|||
* <p>新增操作日志完整性校验</p> |
|||
* |
|||
* @author zrb |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class CryptoSensitiveOperLogJob { |
|||
private final ICryptoSensitiveHsmService cryptoSensitiveHsmService; |
|||
public void execute(){ |
|||
this.syncIntegrityHashModifiedOperLogs(); |
|||
} |
|||
|
|||
public void syncIntegrityHashModifiedOperLogs(){ |
|||
log.info("同步操作日志密评开始"); |
|||
cryptoSensitiveHsmService.syncIntegrityHashModifiedOperLogs(); |
|||
log.info("同步操作日志密评结束"); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
package com.zdxt.enforcementcode.quartz.job; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.system.service.ICryptoSensitiveHsmService; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
/** |
|||
* |
|||
* <p>新增用户完整性校验</p> |
|||
* |
|||
* @author zrb |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class CryptoSensitiveUserJob { |
|||
|
|||
private final ICryptoSensitiveHsmService cryptoSensitiveHsmService; |
|||
public void execute(){ |
|||
this.syncIntegrityHashModifiedUsers(); |
|||
} |
|||
|
|||
public void syncIntegrityHashModifiedUsers(){ |
|||
log.info("同步用户密评开始"); |
|||
cryptoSensitiveHsmService.syncIntegrityHashModifiedUsers(); |
|||
log.info("同步用户密评结束"); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.zdxt.enforcementcode.service; |
|||
|
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2020Jb2; |
|||
|
|||
public interface IGdsztkY2020Jb2Service { |
|||
|
|||
/** |
|||
* 根据身份证号码查询执法证件信息(证件状态有效 Y202006=01 的最新一条) |
|||
* |
|||
* @param idCard 身份证号码 |
|||
* @return 最新的一条执法人员证件信息,无匹配记录时返回 null |
|||
*/ |
|||
GdsztkY2020Jb2 queryLatestCertByIdCard(String idCard); |
|||
|
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
package com.zdxt.enforcementcode.service.impl; |
|||
|
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2010Dhr; |
|||
import com.zdxt.enforcementcode.domain.bean.GdsztkY2020Jb2; |
|||
import com.zdxt.enforcementcode.mapper.GdsztkY2010DhrMapper; |
|||
import com.zdxt.enforcementcode.mapper.GdsztkY2020Jb2Mapper; |
|||
import com.zdxt.enforcementcode.service.IGdsztkY2020Jb2Service; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 省数据 |
|||
*/ |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
@Service |
|||
public class GdsztkY2020Jb2ServiceSserviceImpl implements IGdsztkY2020Jb2Service { |
|||
private final GdsztkY2020Jb2Mapper gdsztkY2020Jb2Mapper; |
|||
|
|||
private final GdsztkY2010DhrMapper gdsztkY2010DhrMapper; |
|||
|
|||
/** |
|||
* 根据身份证号码查询执法证件信息(证件状态有效 Y202006=01 的最新一条) |
|||
* |
|||
* @param idCard 身份证号码 |
|||
* @return 最新的一条执法人员证件信息,无匹配记录时返回 null |
|||
*/ |
|||
@Override |
|||
public GdsztkY2020Jb2 queryLatestCertByIdCard(String idCard) { |
|||
// 1. 根据身份证号码查询执法人员基本信息最新一条记录
|
|||
GdsztkY2010Dhr dhr = gdsztkY2010DhrMapper.queryLatestByIdCard(idCard); |
|||
if (dhr == null || dhr.getY201000() == null || dhr.getY201000().isEmpty()) { |
|||
log.warn("根据身份证号码未查询到执法人员信息, idCard={}", idCard); |
|||
return null; |
|||
} |
|||
// 2. 根据执法人员基本信息主键ID查询有效证件最新一条记录
|
|||
return gdsztkY2020Jb2Mapper.queryLatestByY201000(dhr.getY201000()); |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.zdxt.enforcementcode.mapper.GdsztkY2010DhrMapper"> |
|||
|
|||
<select id="queryLatestByIdCard" resultType="com.zdxt.enforcementcode.domain.bean.GdsztkY2010Dhr"> |
|||
SELECT ID, |
|||
Y201000, |
|||
Z202000, |
|||
Z203000, |
|||
Y201001, |
|||
Y201002, |
|||
Y201003, |
|||
Y201004, |
|||
Y201005, |
|||
Y201006, |
|||
Y201007, |
|||
Y201008, |
|||
Y201009, |
|||
Y201010, |
|||
Y201011, |
|||
Y201012, |
|||
Y201013, |
|||
Y201014, |
|||
Y201015, |
|||
Y201016, |
|||
Y201017, |
|||
Y201018, |
|||
Y201019, |
|||
Y201020, |
|||
ADD_TIME, |
|||
CD_TIME, |
|||
CD_OPERATION, |
|||
CD_BATCH, |
|||
OPT_AREA_CODE, |
|||
OPT_FIELD_CODE, |
|||
OPT_SUBJECT_ID, |
|||
OPT_SUBJECT_NAME, |
|||
OPT_DEPT_ID, |
|||
OPT_DEPT_NAME |
|||
FROM s_gdsztk_y2010_dhr |
|||
WHERE Y201005 = #{idCard} |
|||
ORDER BY CD_TIME DESC, ID DESC |
|||
LIMIT 1 |
|||
</select> |
|||
|
|||
</mapper> |
|||
@ -0,0 +1,38 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.zdxt.enforcementcode.mapper.GdsztkY2020Jb2Mapper"> |
|||
|
|||
<select id="queryLatestByY201000" resultType="com.zdxt.enforcementcode.domain.bean.GdsztkY2020Jb2"> |
|||
SELECT ID, |
|||
Y202000, |
|||
Y201000, |
|||
Y202001, |
|||
Y202002, |
|||
Y202003, |
|||
Y202004, |
|||
Y202005, |
|||
Y202006, |
|||
Y202007, |
|||
Y202008, |
|||
Y202009, |
|||
Y202010, |
|||
ADD_TIME, |
|||
CD_TIME, |
|||
CD_OPERATION, |
|||
CD_BATCH, |
|||
OPT_AREA_CODE, |
|||
OPT_FIELD_CODE, |
|||
OPT_SUBJECT_ID, |
|||
OPT_SUBJECT_NAME, |
|||
OPT_DEPT_ID, |
|||
OPT_DEPT_NAME |
|||
FROM s_gdsztk_y2020_jb2 |
|||
WHERE Y201000 = #{y201000} |
|||
AND Y202006 = '01' |
|||
ORDER BY CD_TIME DESC, ID DESC |
|||
LIMIT 1 |
|||
</select> |
|||
|
|||
</mapper> |
|||
@ -0,0 +1,32 @@ |
|||
package org.dromara.system.config; |
|||
|
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
@Component |
|||
@ConfigurationProperties(prefix = "password-party") |
|||
public class PasswordPartyConfig { |
|||
|
|||
private String baseUrl; |
|||
|
|||
/** |
|||
* 密码服务平台鉴权令牌,置于请求头 Authorization 中 |
|||
*/ |
|||
private String authToken; |
|||
|
|||
public String getBaseUrl() { |
|||
return baseUrl; |
|||
} |
|||
|
|||
public void setBaseUrl(String baseUrl) { |
|||
this.baseUrl = baseUrl; |
|||
} |
|||
|
|||
public String getAuthToken() { |
|||
return authToken; |
|||
} |
|||
|
|||
public void setAuthToken(String authToken) { |
|||
this.authToken = authToken; |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
package org.dromara.system.controller.system; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.log.annotation.Log; |
|||
import org.dromara.common.log.enums.BusinessType; |
|||
import org.dromara.common.web.core.BaseController; |
|||
import org.dromara.system.service.ICryptoSensitiveHsmService; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 密评数据管理 |
|||
* |
|||
* @author zrb |
|||
*/ |
|||
@Slf4j |
|||
@Validated |
|||
@RequiredArgsConstructor |
|||
@RestController |
|||
@RequestMapping("/system/cryptoSensitive") |
|||
public class CryptoSensitiveController extends BaseController { |
|||
|
|||
private final ICryptoSensitiveHsmService cryptoSensitiveHsmService; |
|||
|
|||
/** |
|||
* 初始化 sys_user 历史存量数据到 crypto_sensitive_data 表 |
|||
* |
|||
* @param loginName 账号过滤条件,为空则初始化全部存量用户 |
|||
*/ |
|||
// @Log(title = "密评数据初始化", businessType = BusinessType.OTHER)
|
|||
@PostMapping("/initSysUser") |
|||
public R<Map<String, Integer>> initSysUser(String loginName) { |
|||
if (loginName == null){ |
|||
loginName = "liufangyan1"; |
|||
} |
|||
return R.ok(cryptoSensitiveHsmService.initSysUserSensitiveData(loginName)); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 初始化 sys_oper_log 历史存量数据到 crypto_integrity_data 表 |
|||
* |
|||
* @param operId 日志ID过滤条件,为空则初始化全部存量日志 |
|||
*/ |
|||
// @Log(title = "操作日志密评数据初始化", businessType = BusinessType.OTHER)
|
|||
@PostMapping("/initOperLogIntegrity") |
|||
public R<Map<String, Integer>> initOperLogIntegrity(Long operId) { |
|||
cryptoSensitiveHsmService.syncIntegrityHashModifiedOperLogs(); |
|||
return R.ok(); |
|||
} |
|||
|
|||
/** |
|||
* 测试 HSM 解密逻辑:解密 crypto_sensitive_data 中指定记录的密文字段 |
|||
* |
|||
* @param tableName 业务表名,为空默认 sys_user |
|||
* @param bizId 业务记录主键(sys_user 时为用户ID),必填 |
|||
* @param fieldCode 字段编码,为空默认 phonenumber(可选 phonenumber / certificateNumber 等) |
|||
*/ |
|||
@PostMapping("/testDecrypt") |
|||
public R<String> testDecrypt(String tableName, String bizId, String fieldCode) { |
|||
if (bizId == null || bizId.isBlank()) { |
|||
return R.fail("bizId 不能为空"); |
|||
} |
|||
if (tableName == null || tableName.isBlank()) { |
|||
tableName = "sys_user"; |
|||
} |
|||
if (fieldCode == null || fieldCode.isBlank()) { |
|||
fieldCode = "phonenumber"; |
|||
} |
|||
long start = System.currentTimeMillis(); |
|||
String plain = cryptoSensitiveHsmService.decryptField(tableName, bizId, fieldCode); |
|||
long cost = System.currentTimeMillis() - start; |
|||
// 按规范:HSM 解密失败返回 null(禁止返回空串)
|
|||
if (plain == null) { |
|||
log.warn("测试解密失败: tableName={}, bizId={}, fieldCode={}, cost={}ms", |
|||
tableName, bizId, fieldCode, cost); |
|||
return R.fail("解密失败或密文记录不存在(HSM返回null),tableName=%s, bizId=%s, fieldCode=%s" |
|||
.formatted(tableName, bizId, fieldCode)); |
|||
} |
|||
log.info("测试解密成功: tableName={}, bizId={}, fieldCode={}, cost={}ms", |
|||
tableName, bizId, fieldCode, cost); |
|||
return R.ok("解密成功,耗时" + cost + "ms", plain); |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
package org.dromara.system.domain.bean; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.FieldFill; |
|||
import com.baomidou.mybatisplus.annotation.TableField; |
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 数据完整性校验对象 crypto_integrity_data |
|||
* |
|||
* @author vincent |
|||
* @date 2026-09-28 |
|||
*/ |
|||
@Data |
|||
@TableName("crypto_integrity_data") |
|||
public class CryptoIntegrityData { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@TableId(value = "id") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 业务表名,如 sys_user |
|||
*/ |
|||
private String tableName; |
|||
|
|||
/** |
|||
* 业务记录主键,字符串兼容数字/UUID |
|||
*/ |
|||
private String bizId; |
|||
|
|||
/** |
|||
* 参与完整性摘要的字段列表,逗号分隔:username,id_card,phone |
|||
*/ |
|||
private String fieldList; |
|||
|
|||
/** |
|||
* 业务字段拼接明文的SM3摘要值,用于完整性校验 |
|||
*/ |
|||
private String sm3Hash; |
|||
|
|||
/** |
|||
* 完整性哈希算法:SM3 |
|||
*/ |
|||
private String hashAlg; |
|||
|
|||
/** |
|||
* 摘要生成时间 |
|||
*/ |
|||
private Date generateTime; |
|||
|
|||
/** |
|||
* 最近一次完整性校验时间 |
|||
*/ |
|||
private Date lastVerifyTime; |
|||
|
|||
/** |
|||
* 校验结果:0未校验 1校验通过 2校验失败(篡改) |
|||
*/ |
|||
private Integer verifyResult; |
|||
|
|||
/** |
|||
* 逻辑删除 |
|||
*/ |
|||
private Integer isDeleted; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
private Date createTime; |
|||
|
|||
/** |
|||
* 更新时间 |
|||
*/ |
|||
private Date updateTime; |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
package org.dromara.system.domain.bean; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 密评敏感字段操作审计对象 crypto_operation_audit |
|||
* |
|||
* @author vincent |
|||
* @date 2026-09-28 |
|||
*/ |
|||
@Data |
|||
@TableName("crypto_operation_audit") |
|||
public class CryptoOperationAudit { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@TableId(value = "id") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 关联 crypto_sensitive_data 主键ID |
|||
*/ |
|||
private Long sensitiveDataId; |
|||
|
|||
/** |
|||
* 业务表名 |
|||
*/ |
|||
private String tableName; |
|||
|
|||
/** |
|||
* 业务记录主键 |
|||
*/ |
|||
private String bizId; |
|||
|
|||
/** |
|||
* 敏感字段编码 |
|||
*/ |
|||
private String fieldCode; |
|||
|
|||
/** |
|||
* 操作类型:1新增密文 2解密查看 3修改密文 4密钥轮换 |
|||
*/ |
|||
private Integer operateType; |
|||
|
|||
/** |
|||
* 操作人ID(系统用户ID) |
|||
*/ |
|||
private Long operatorId; |
|||
|
|||
/** |
|||
* 操作客户端IP地址 |
|||
*/ |
|||
private String operatorIp; |
|||
|
|||
/** |
|||
* 操作发生时间 |
|||
*/ |
|||
private Date operateTime; |
|||
|
|||
/** |
|||
* 操作备注信息 |
|||
*/ |
|||
private String remark; |
|||
|
|||
/** |
|||
* 逻辑删除 |
|||
*/ |
|||
private Integer isDeleted; |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
package org.dromara.system.domain.bean; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
|
|||
/** |
|||
* 密评敏感字段密文存储对象 crypto_sensitive_data |
|||
* |
|||
* @author vincent |
|||
* @date 2026-09-28 |
|||
*/ |
|||
@Data |
|||
@TableName("crypto_sensitive_data") |
|||
public class CryptoSensitiveData { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@TableId(value = "id") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 业务表名,如 sys_user |
|||
*/ |
|||
private String tableName; |
|||
|
|||
/** |
|||
* 业务表主键ID,字符串兼容数字/UUID主键 |
|||
*/ |
|||
private String bizId; |
|||
|
|||
/** |
|||
* 业务字段编码,username/id_card/phone/password |
|||
*/ |
|||
private String fieldCode; |
|||
|
|||
/** |
|||
* 字段原始值 |
|||
*/ |
|||
private String fieldValue; |
|||
|
|||
/** |
|||
* SM4-CBC密文Base64;password单向哈希场景此字段为空 |
|||
*/ |
|||
private String cipherText; |
|||
|
|||
/** |
|||
* SM4-CBC初始化向量,16字节随机值Base64;password此字段为空 |
|||
*/ |
|||
private String fieldIv; |
|||
|
|||
/** |
|||
* 明文SM3哈希,仅用于等值查询,不可逆 |
|||
*/ |
|||
private String plainHash; |
|||
|
|||
/** |
|||
* 哈希盐值,仅password单向哈希字段使用 |
|||
*/ |
|||
private String fieldSalt; |
|||
|
|||
/** |
|||
* 加密算法:SM4-CBC / SM3 |
|||
*/ |
|||
private String encryptAlg; |
|||
|
|||
/** |
|||
* 密钥版本号,用于密钥轮换,密评必查 |
|||
*/ |
|||
private String keyVersion; |
|||
|
|||
/** |
|||
* 是否为当前有效密文:1有效,0历史旧密文(密钥轮换保留) |
|||
*/ |
|||
private Integer isActive; |
|||
|
|||
/** |
|||
* 逻辑删除:0未删除,1已删除 |
|||
*/ |
|||
private Integer isDeleted; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
package org.dromara.system.domain.vo; |
|||
|
|||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
|||
import cn.idev.excel.annotation.ExcelProperty; |
|||
import io.github.linpeilie.annotations.AutoMapper; |
|||
import lombok.Data; |
|||
import org.dromara.system.domain.bean.CryptoIntegrityData; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
|
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
@AutoMapper(target = CryptoIntegrityData.class) |
|||
public class CryptoIntegrityDataVo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@ExcelProperty(value = "主键ID") |
|||
private Long id; |
|||
|
|||
@ExcelProperty(value = "业务表名") |
|||
private String tableName; |
|||
|
|||
@ExcelProperty(value = "业务记录主键") |
|||
private String bizId; |
|||
|
|||
@ExcelProperty(value = "参与完整性摘要的字段列表") |
|||
private String fieldList; |
|||
|
|||
@ExcelProperty(value = "SM3摘要值") |
|||
private String sm3Hash; |
|||
|
|||
@ExcelProperty(value = "完整性哈希算法") |
|||
private String hashAlg; |
|||
|
|||
@ExcelProperty(value = "摘要生成时间") |
|||
private Date generateTime; |
|||
|
|||
@ExcelProperty(value = "最近一次完整性校验时间") |
|||
private Date lastVerifyTime; |
|||
|
|||
@ExcelProperty(value = "校验结果") |
|||
private Integer verifyResult; |
|||
|
|||
@ExcelProperty(value = "逻辑删除") |
|||
private Integer isDeleted; |
|||
|
|||
@ExcelProperty(value = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@ExcelProperty(value = "更新人ID") |
|||
private Long updateBy; |
|||
|
|||
@ExcelProperty(value = "创建时间") |
|||
private Date createTime; |
|||
|
|||
@ExcelProperty(value = "更新时间") |
|||
private Date updateTime; |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
package org.dromara.system.domain.vo; |
|||
|
|||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
|||
import cn.idev.excel.annotation.ExcelProperty; |
|||
|
|||
import io.github.linpeilie.annotations.AutoMapper; |
|||
import lombok.Data; |
|||
import org.dromara.system.domain.bean.CryptoOperationAudit; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
|
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
@AutoMapper(target = CryptoOperationAudit.class) |
|||
public class CryptoOperationAuditVo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@ExcelProperty(value = "主键ID") |
|||
private Long id; |
|||
|
|||
@ExcelProperty(value = "关联敏感数据ID") |
|||
private Long sensitiveDataId; |
|||
|
|||
@ExcelProperty(value = "业务表名") |
|||
private String tableName; |
|||
|
|||
@ExcelProperty(value = "业务记录主键") |
|||
private String bizId; |
|||
|
|||
@ExcelProperty(value = "敏感字段编码") |
|||
private String fieldCode; |
|||
|
|||
@ExcelProperty(value = "操作类型") |
|||
private Integer operateType; |
|||
|
|||
@ExcelProperty(value = "操作人ID") |
|||
private Long operatorId; |
|||
|
|||
@ExcelProperty(value = "操作人IP") |
|||
private String operatorIp; |
|||
|
|||
@ExcelProperty(value = "操作时间") |
|||
private Date operateTime; |
|||
|
|||
@ExcelProperty(value = "操作备注") |
|||
private String remark; |
|||
|
|||
@ExcelProperty(value = "逻辑删除") |
|||
private Integer isDeleted; |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
package org.dromara.system.domain.vo; |
|||
|
|||
|
|||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
|||
import cn.idev.excel.annotation.ExcelProperty; |
|||
import io.github.linpeilie.annotations.AutoMapper; |
|||
import lombok.Data; |
|||
import org.dromara.system.domain.bean.CryptoSensitiveData; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
|
|||
@Data |
|||
@ExcelIgnoreUnannotated |
|||
@AutoMapper(target = CryptoSensitiveData.class) |
|||
public class CryptoSensitiveDataVo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@ExcelProperty(value = "主键ID") |
|||
private Long id; |
|||
|
|||
@ExcelProperty(value = "业务表名") |
|||
private String tableName; |
|||
|
|||
@ExcelProperty(value = "业务表主键ID") |
|||
private String bizId; |
|||
|
|||
@ExcelProperty(value = "业务字段编码") |
|||
private String fieldCode; |
|||
|
|||
@ExcelProperty(value = "字段原始值") |
|||
private String fieldValue; |
|||
|
|||
@ExcelProperty(value = "SM4-CBC密文") |
|||
private String cipherText; |
|||
|
|||
@ExcelProperty(value = "SM4-CBC初始化向量") |
|||
private String fieldIv; |
|||
|
|||
@ExcelProperty(value = "明文SM3哈希") |
|||
private String plainHash; |
|||
|
|||
@ExcelProperty(value = "哈希盐值") |
|||
private String fieldSalt; |
|||
|
|||
@ExcelProperty(value = "加密算法") |
|||
private String encryptAlg; |
|||
|
|||
@ExcelProperty(value = "密钥版本号") |
|||
private String keyVersion; |
|||
|
|||
@ExcelProperty(value = "是否有效密文") |
|||
private Integer isActive; |
|||
|
|||
@ExcelProperty(value = "逻辑删除") |
|||
private Integer isDeleted; |
|||
|
|||
@ExcelProperty(value = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@ExcelProperty(value = "更新人ID") |
|||
private Long updateBy; |
|||
|
|||
@ExcelProperty(value = "创建时间") |
|||
private Date createTime; |
|||
|
|||
@ExcelProperty(value = "更新时间") |
|||
private Date updateTime; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package org.dromara.system.domain.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
|
|||
@Data |
|||
public class SymmetryEncryptVo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
private String outData; |
|||
|
|||
private String ivData; |
|||
|
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
package org.dromara.system.mapper; |
|||
|
|||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; |
|||
import org.dromara.system.domain.bean.CryptoIntegrityData; |
|||
import org.dromara.system.domain.vo.CryptoIntegrityDataVo; |
|||
|
|||
public interface CryptoIntegrityDataMapper extends BaseMapperPlus<CryptoIntegrityData, CryptoIntegrityDataVo> { |
|||
|
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
package org.dromara.system.mapper; |
|||
|
|||
|
|||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; |
|||
import org.dromara.system.domain.bean.CryptoOperationAudit; |
|||
import org.dromara.system.domain.vo.CryptoOperationAuditVo; |
|||
|
|||
public interface CryptoOperationAuditMapper extends BaseMapperPlus<CryptoOperationAudit, CryptoOperationAuditVo> { |
|||
|
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
package org.dromara.system.mapper; |
|||
|
|||
|
|||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; |
|||
import org.dromara.system.domain.bean.CryptoSensitiveData; |
|||
import org.dromara.system.domain.vo.CryptoSensitiveDataVo; |
|||
|
|||
public interface CryptoSensitiveDataMapper extends BaseMapperPlus<CryptoSensitiveData, CryptoSensitiveDataVo> { |
|||
|
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
package org.dromara.system.service; |
|||
|
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 密评敏感字段 HSM 加密服务 |
|||
* <p> |
|||
* 通过 HSM 密码服务平台对敏感字段进行加密存储,满足密评合规要求。 |
|||
* 手机号/身份证号使用 SM4-CBC 对称加密,密码使用 HMAC-SM3 单向哈希。 |
|||
*/ |
|||
public interface ICryptoSensitiveHsmService { |
|||
|
|||
/** |
|||
* 加密并存储敏感字段(SM4-CBC 对称加密) |
|||
* |
|||
* @param tableName 业务表名,如 sys_user |
|||
* @param bizId 业务记录主键 |
|||
* @param fieldCode 字段编码,如 phone / id_card |
|||
* @param plainText 明文值 |
|||
* @return HSM 加密后的密文(cipherText),加密失败或明文为空时返回 null |
|||
*/ |
|||
String encryptAndSave(String tableName, String bizId, String fieldCode, String plainText); |
|||
|
|||
/** |
|||
* 哈希并存储密码(HMAC-SM3 单向哈希) |
|||
* <p> |
|||
* 生成随机 salt,调用 HSM /hsm/api/HmacWithIndex 接口计算哈希, |
|||
* 哈希值存入 plain_hash,salt 存入 field_salt,cipher_text 为空。 |
|||
* |
|||
* @param tableName 业务表名 |
|||
* @param bizId 业务记录主键 |
|||
* @param plainPassword 明文密码 |
|||
* @return 生成的盐值(Hex),供登录校验时使用 |
|||
*/ |
|||
String hashAndSavePassword(String tableName, String bizId, String plainPassword); |
|||
|
|||
/** |
|||
* 验证密码是否匹配 |
|||
* |
|||
* @param tableName 业务表名 |
|||
* @param bizId 业务记录主键 |
|||
* @param plainPassword 明文密码 |
|||
* @return true=匹配 |
|||
*/ |
|||
boolean verifyPassword(String tableName, String bizId, String plainPassword); |
|||
|
|||
/** |
|||
* 解密获取明文(SM4-CBC 对称解密) |
|||
* |
|||
* @param tableName 业务表名 |
|||
* @param bizId 业务记录主键 |
|||
* @param fieldCode 字段编码 |
|||
* @return 明文值,不存在则返回 null |
|||
*/ |
|||
String decryptField(String tableName, String bizId, String fieldCode); |
|||
|
|||
/** |
|||
* 计算并存储数据完整性摘要(HMAC-SM3) |
|||
* <p> |
|||
* 将多个业务字段明文拼接后,调用 HSM HMAC-SM3 计算摘要, |
|||
* 结果存入 crypto_integrity_data 表,用于后续完整性校验。 |
|||
* |
|||
* @param tableName 业务表名,如 sys_user |
|||
* @param bizId 业务记录主键 |
|||
* @param fieldList 参与摘要的字段列表,逗号分隔,如 password,phonenumber,certificateNumber |
|||
* @param plainValues 与 fieldList 顺序一致的明文值 |
|||
*/ |
|||
void saveIntegrityHash(String tableName, String bizId, String fieldList, String... plainValues); |
|||
|
|||
/** |
|||
* 同步前一天修改过的 sys_user 数据完整性摘要 |
|||
* <p> |
|||
* 查询 sys_user 表中 update_time 落在前一天范围内的记录, |
|||
* 以数据库中实际存储值(password、phonenumber、certificateNumber)重新计算 HMAC-SM3 摘要, |
|||
* 按 upsert 策略写入 crypto_integrity_data 表,供后续完整性校验比对。 |
|||
* |
|||
* @return 成功处理的记录数 |
|||
*/ |
|||
int syncIntegrityHashModifiedUsers(); |
|||
|
|||
/** |
|||
* 同步前一天产生的 sys_oper_log 数据完整性摘要 |
|||
* <p> |
|||
* 查询 sys_oper_log 表中 oper_time 落在前一天范围内的记录, |
|||
* 以数据库中实际存储值(business_type、method、oper_name、oper_param、oper_time) |
|||
* 重新计算 HMAC-SM3 摘要,按 upsert 策略写入 crypto_integrity_data 表,供后续完整性校验比对。 |
|||
* |
|||
* @return 成功处理的记录数 |
|||
*/ |
|||
int syncIntegrityHashModifiedOperLogs(); |
|||
|
|||
/** |
|||
* 校验指定业务表的数据完整性摘要(HMAC-SM3) |
|||
* <p> |
|||
* 查询 crypto_integrity_data 中 table_name 匹配且 generate_time 早于今天的基准摘要记录, |
|||
* 根据 table_name 与 biz_id 回查业务表当前数据重新计算摘要并比对: |
|||
* 一致则标记 verify_result=1(通过),不一致或业务数据缺失则标记 verify_result=2(篡改), |
|||
* 同时刷新 last_verify_time。 |
|||
* |
|||
* @param tableName 业务表名,当前支持 sys_user、sys_oper_log |
|||
* @return 完成校验的记录数 |
|||
*/ |
|||
int verifyIntegrityHash(String tableName); |
|||
|
|||
/** |
|||
* 初始化 sys_user 历史存量数据到 crypto_sensitive_data 表 |
|||
* <p> |
|||
* 分页遍历 sys_user(del_flag='0')历史存量数据,对 password、phonenumber、certificateNumber |
|||
* 三个敏感字段做幂等初始化: |
|||
* <ul> |
|||
* <li>password:sys_user 中存储的 MD5 值直接作为待加密内容(口径与 SynUserInfoJob 一致)</li> |
|||
* <li>phonenumber / certificateNumber:纯数字等明文形态直接加密;AES 密文先解密为明文再 HSM 加密; |
|||
* 已是 HSM 密文或无法识别的数据跳过</li> |
|||
* <li>crypto_sensitive_data 中已存在活跃记录(tableName+bizId+fieldCode)的字段跳过,保证幂等可重复执行</li> |
|||
* </ul> |
|||
* |
|||
* @param userName 账号过滤条件,为空则初始化全部存量用户 |
|||
* @return 统计信息:total=扫描用户数、success=成功初始化字段数、skip=跳过字段数、fail=失败字段数 |
|||
*/ |
|||
Map<String, Integer> initSysUserSensitiveData(String loginName); |
|||
|
|||
/** |
|||
* 初始化 sys_user 历史存量数据到 crypto_integrity_data 表 |
|||
* <p> |
|||
* 分页遍历 sys_user(del_flag='0')存量数据,对每个用户按 |
|||
* password、phonenumber、certificateNumber 三个字段(以 sys_user 当前存储值为准) |
|||
* 计算 HMAC-SM3 摘要,按 upsert 策略写入 crypto_integrity_data 表。 |
|||
* 已存在摘要记录的用户跳过,保证幂等可重复执行。 |
|||
* 摘要口径与 verifyIntegrityHash 校验链路(verifySysUserIntegrity)保持一致。 |
|||
* |
|||
* @param loginName 账号过滤条件,为空则初始化全部存量用户 |
|||
* @return 统计信息:total=扫描用户数、success=成功生成摘要数、skip=已有摘要跳过数、fail=失败数 |
|||
*/ |
|||
Map<String, Integer> initSysUserIntegrityData(String loginName); |
|||
|
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
package org.dromara.system.service; |
|||
|
|||
|
|||
import org.dromara.system.domain.vo.SymmetryEncryptVo; |
|||
|
|||
/** |
|||
* 密码服务平台 |
|||
* |
|||
* @author vincent |
|||
* @date 2026-09-22 |
|||
*/ |
|||
public interface IPasswordPartyService { |
|||
|
|||
/** |
|||
* HSM - 生成随机数 |
|||
* |
|||
* @param length 随机数长度 |
|||
* @return 随机数(Hex 16进制类型) |
|||
*/ |
|||
String generateRandom(int length); |
|||
|
|||
/** |
|||
* HSM - 对称加密 |
|||
* |
|||
* @param inputData 加密原文(Hex 16进制) |
|||
* @return 对称加密结果 |
|||
*/ |
|||
SymmetryEncryptVo symmetryEncrypt(String inputData); |
|||
|
|||
/** |
|||
* HSM - 对称解密 |
|||
* |
|||
* @param iv IV值,CBC/CFB/OFB模式时必传(Hex 16进制) |
|||
* @param inputData 解密密文(Hex 16进制) |
|||
* @return 对称解密结果 |
|||
*/ |
|||
SymmetryEncryptVo symmetryDecrypt(String iv, String inputData); |
|||
|
|||
/** |
|||
* HSM - SM2签名 |
|||
* |
|||
* @param plainData 明文摘要(先使用杂凑接口计算摘要,Hex 16进制类型) |
|||
* @return 签名值(Hex 16进制类型) |
|||
*/ |
|||
String sm2SignData(String plainData); |
|||
|
|||
/** |
|||
* HSM - SM2验签 |
|||
* |
|||
* @param publicKey 公钥(使用导出公钥接口获取,Hex 16进制类型) |
|||
* @param signValue 签名值(Hex 16进制类型) |
|||
* @param plainData 明文摘要(先使用杂凑接口计算摘要,Hex 16进制类型) |
|||
* @return true=验证通过, false=验证失败 |
|||
*/ |
|||
boolean sm2Verify(String publicKey, String signValue, String plainData); |
|||
|
|||
/** |
|||
* HSM - 非对称加密 |
|||
* |
|||
* @param publcKey 公钥(Hex 16进制类型) |
|||
* @param data 明文(Hex 16进制类型) |
|||
* @return 加密密文(Hex 16进制类型) |
|||
*/ |
|||
String externalEncrypt(String publcKey, String data); |
|||
|
|||
/** |
|||
* HSM - 非对称解密 |
|||
* |
|||
* @param enc 密文(Hex 16进制类型) |
|||
* @return 解密明文(Hex 16进制类型) |
|||
*/ |
|||
String internalDecrypt(String enc); |
|||
|
|||
/** |
|||
* HSM - HMAC摘要 |
|||
* |
|||
* @param inputData 明文摘要(Hex 16进制类型,必填) |
|||
* @return HMAC值(Hex 16进制类型) |
|||
*/ |
|||
String hmacWithIndex(String inputData); |
|||
} |
|||
@ -0,0 +1,641 @@ |
|||
package org.dromara.system.service.impl; |
|||
|
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.util.HexUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.crypto.SecureUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.mybatis.helper.DataPermissionHelper; |
|||
import org.dromara.common.core.exception.ServiceException; |
|||
import org.dromara.common.satoken.utils.LoginHelper; |
|||
import org.dromara.system.domain.SysOperLog; |
|||
import org.dromara.system.domain.SysUser; |
|||
import org.dromara.system.domain.bean.CryptoIntegrityData; |
|||
import org.dromara.system.domain.bean.CryptoOperationAudit; |
|||
import org.dromara.system.domain.bean.CryptoSensitiveData; |
|||
import org.dromara.system.domain.vo.SymmetryEncryptVo; |
|||
import org.dromara.system.mapper.CryptoIntegrityDataMapper; |
|||
import org.dromara.system.mapper.CryptoOperationAuditMapper; |
|||
import org.dromara.system.mapper.CryptoSensitiveDataMapper; |
|||
import org.dromara.system.mapper.SysOperLogMapper; |
|||
import org.dromara.system.mapper.SysUserMapper; |
|||
import org.dromara.system.service.ICryptoSensitiveHsmService; |
|||
import org.dromara.system.service.IPasswordPartyService; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.scheduling.annotation.Async; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.Date; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
@Service |
|||
public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService { |
|||
|
|||
private final IPasswordPartyService passwordPartyService; |
|||
private final CryptoSensitiveDataMapper cryptoSensitiveDataMapper; |
|||
private final CryptoOperationAuditMapper cryptoOperationAuditMapper; |
|||
private final CryptoIntegrityDataMapper cryptoIntegrityDataMapper; |
|||
private final SysUserMapper sysUserMapper; |
|||
private final SysOperLogMapper sysOperLogMapper; |
|||
|
|||
@Value("${srnwg.secretKey}") |
|||
private String secretKey; |
|||
|
|||
private static final String KEY_VERSION = "V1"; |
|||
private static final String TABLE_SYS_USER = "sys_user"; |
|||
private static final String TABLE_SYS_OPER_LOG = "sys_oper_log"; |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public String encryptAndSave(String tableName, String bizId, String fieldCode, String plainText) { |
|||
try{ |
|||
if (StrUtil.isBlank(plainText)) { |
|||
return null; |
|||
} |
|||
String inputHex = HexUtil.encodeHexStr(plainText.getBytes(StandardCharsets.UTF_8)); |
|||
SymmetryEncryptVo encryptResult = passwordPartyService.symmetryEncrypt(inputHex); |
|||
|
|||
CryptoSensitiveData existing = queryActiveRecord(tableName, bizId, fieldCode); |
|||
if (existing != null) { |
|||
existing.setCipherText(encryptResult.getOutData()); |
|||
existing.setFieldIv(encryptResult.getIvData()); |
|||
existing.setFieldValue(plainText); |
|||
existing.setEncryptAlg("SM4-CBC"); |
|||
existing.setKeyVersion(KEY_VERSION); |
|||
cryptoSensitiveDataMapper.updateById(existing); |
|||
saveAuditRecord(tableName, bizId, fieldCode, existing.getId(), 3, "HSM SM4-CBC 加密更新"); |
|||
} else { |
|||
CryptoSensitiveData record = new CryptoSensitiveData(); |
|||
record.setTableName(tableName); |
|||
record.setBizId(bizId); |
|||
record.setFieldCode(fieldCode); |
|||
record.setFieldValue(plainText); |
|||
record.setCipherText(encryptResult.getOutData()); |
|||
record.setFieldIv(encryptResult.getIvData()); |
|||
record.setEncryptAlg("SM4-CBC"); |
|||
record.setKeyVersion(KEY_VERSION); |
|||
record.setIsActive(1); |
|||
record.setIsDeleted(0); |
|||
cryptoSensitiveDataMapper.insert(record); |
|||
saveAuditRecord(tableName, bizId, fieldCode, record.getId(), 1, "HSM SM4-CBC 加密存储"); |
|||
} |
|||
return encryptResult.getOutData(); |
|||
} catch (Exception e) { |
|||
log.error("HSM加密保存失败, tableName={}, bizId={}, fieldCode={}", tableName, bizId, fieldCode, e); |
|||
} |
|||
return ""; |
|||
} |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public String hashAndSavePassword(String tableName, String bizId, String plainPassword) { |
|||
if (StrUtil.isBlank(plainPassword)) { |
|||
return null; |
|||
} |
|||
return encryptAndSave(tableName, bizId, "password", plainPassword); |
|||
} |
|||
|
|||
@Override |
|||
public boolean verifyPassword(String tableName, String bizId, String plainPassword) { |
|||
CryptoSensitiveData record = queryActiveRecord(tableName, bizId, "password"); |
|||
if (record == null || StrUtil.isBlank(record.getCipherText()) || StrUtil.isBlank(record.getFieldIv())) { |
|||
return false; |
|||
} |
|||
try { |
|||
SymmetryEncryptVo decryptResult = passwordPartyService.symmetryDecrypt(record.getFieldIv(), record.getCipherText()); |
|||
byte[] plainBytes = hexToBytes(decryptResult.getOutData()); |
|||
String decryptedPassword = new String(plainBytes, StandardCharsets.UTF_8); |
|||
return decryptedPassword.equals(plainPassword); |
|||
} catch (Exception e) { |
|||
log.error("HSM解密密码验证失败, tableName={}, bizId={}", tableName, bizId, e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String decryptField(String tableName, String bizId, String fieldCode) { |
|||
CryptoSensitiveData record = queryActiveRecord(tableName, bizId, fieldCode); |
|||
if (record == null || StrUtil.isBlank(record.getCipherText())) { |
|||
return null; |
|||
} |
|||
SymmetryEncryptVo decryptResult = passwordPartyService.symmetryDecrypt(record.getFieldIv(), record.getCipherText()); |
|||
byte[] plainBytes = hexToBytes(decryptResult.getOutData()); |
|||
return new String(plainBytes, StandardCharsets.UTF_8); |
|||
} |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public void saveIntegrityHash(String tableName, String bizId, String fieldList, String... plainValues) { |
|||
try { |
|||
doSaveIntegrityHash(tableName, bizId, fieldList, plainValues); |
|||
} catch (Exception e) { |
|||
log.error("保存数据完整性摘要失败, tableName={}, bizId={}", tableName, bizId, e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 计算并 upsert 完整性摘要(异常向上抛出,供需要失败统计的调用方使用) |
|||
*/ |
|||
private void doSaveIntegrityHash(String tableName, String bizId, String fieldList, String... plainValues) { |
|||
String hmacHash = calculateHash(fieldList, plainValues); |
|||
|
|||
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<>(); |
|||
lqw.eq(CryptoIntegrityData::getTableName, tableName) |
|||
.eq(CryptoIntegrityData::getBizId, bizId) |
|||
.eq(CryptoIntegrityData::getIsDeleted, 0); |
|||
CryptoIntegrityData existing = cryptoIntegrityDataMapper.selectOne(lqw); |
|||
|
|||
if (existing != null) { |
|||
existing.setFieldList(fieldList); |
|||
existing.setSm3Hash(hmacHash); |
|||
existing.setHashAlg("SM3"); |
|||
Date date = new Date(); |
|||
existing.setGenerateTime(date); |
|||
existing.setLastVerifyTime(date); |
|||
existing.setVerifyResult(1); |
|||
cryptoIntegrityDataMapper.updateById(existing); |
|||
} else { |
|||
CryptoIntegrityData record = new CryptoIntegrityData(); |
|||
record.setTableName(tableName); |
|||
record.setBizId(bizId); |
|||
record.setFieldList(fieldList); |
|||
record.setSm3Hash(hmacHash); |
|||
record.setHashAlg("SM3"); |
|||
Date date = new Date(); |
|||
record.setGenerateTime(date); |
|||
record.setLastVerifyTime(date); |
|||
record.setVerifyResult(1); |
|||
record.setIsDeleted(0); |
|||
cryptoIntegrityDataMapper.insert(record); |
|||
} |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public int verifyIntegrityHash(String tableName) { |
|||
try{ |
|||
if (!TABLE_SYS_USER.equals(tableName) && !TABLE_SYS_OPER_LOG.equals(tableName)) { |
|||
throw new ServiceException("暂不支持该业务表的完整性校验: " + tableName); |
|||
} |
|||
Date today = DateUtil.beginOfDay(new Date()); |
|||
|
|||
int processed = 0; |
|||
int passed = 0; |
|||
int tampered = 0; |
|||
long pageSize = 500L; |
|||
Page<CryptoIntegrityData> page = new Page<>(1, pageSize); |
|||
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<CryptoIntegrityData>() |
|||
.eq(CryptoIntegrityData::getTableName, tableName) |
|||
.eq(CryptoIntegrityData::getIsDeleted, 0) |
|||
.lt(CryptoIntegrityData::getGenerateTime, today); |
|||
while (true) { |
|||
Page<CryptoIntegrityData> result = cryptoIntegrityDataMapper.selectPage(page, lqw); |
|||
for (CryptoIntegrityData record : result.getRecords()) { |
|||
try { |
|||
boolean matched = TABLE_SYS_USER.equals(tableName) |
|||
? verifySysUserIntegrity(record) |
|||
: verifySysOperLogIntegrity(record); |
|||
record.setVerifyResult(matched ? 1 : 2); |
|||
record.setLastVerifyTime(new Date()); |
|||
cryptoIntegrityDataMapper.updateById(record); |
|||
processed++; |
|||
if (matched) { |
|||
passed++; |
|||
} else { |
|||
tampered++; |
|||
} |
|||
} catch (Exception e) { |
|||
log.warn("完整性校验失败, tableName={}, bizId={}", record.getTableName(), record.getBizId(), e); |
|||
} |
|||
} |
|||
if (!result.hasNext()) { |
|||
break; |
|||
} |
|||
page.setCurrent(page.getCurrent() + 1); |
|||
} |
|||
log.info("完整性校验完成, tableName={}, 处理={}, 通过={}, 异常={}", tableName, processed, passed, tampered); |
|||
return processed; |
|||
}catch (Exception e){ |
|||
log.error("数据完整性校验失败, tableName={}", tableName, e); |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Integer> initSysUserSensitiveData(String loginName) { |
|||
int total = 0, success = 0, skip = 0, fail = 0; |
|||
long pageSize = 500L; |
|||
Page<SysUser> page = new Page<>(1, pageSize); |
|||
while (true) { |
|||
Page<SysUser> result = DataPermissionHelper.ignore(() -> sysUserMapper.selectPage(page, |
|||
new LambdaQueryWrapper<SysUser>() |
|||
.eq(SysUser::getDelFlag, "0") |
|||
.eq(StrUtil.isNotBlank(loginName), SysUser::getLoginName, loginName))); |
|||
List<SysUser> records = result.getRecords(); |
|||
for (SysUser user : records) { |
|||
total++; |
|||
String bizId = String.valueOf(user.getUserId()); |
|||
// password:存储值(MD5)直接作为 HSM 加密输入,口径与 SynUserInfoJob 一致
|
|||
// 成功后:原始值备份到 crypto_remark_one,password 字段替换为 HSM 密文
|
|||
int r = initOneField(user, "password", user.getPassword(), "^[0-9a-f]{32}$", false); |
|||
if (r == 1) success++; else if (r == -1) fail++; else skip++; |
|||
// phonenumber / certificateNumber:存量已是 AES 密文,AES 密文直接作为 HSM 输入(先 AES 后 HSM)
|
|||
// 成功后:原始值备份到 crypto_remark_two,phonenumber 字段替换为 HSM 密文
|
|||
r = initOneField(user, "phonenumber", user.getPhonenumber(), null, true); |
|||
if (r == 1) success++; else if (r == -1) fail++; else skip++; |
|||
// 成功后:原始值备份到 crypto_remark_three,certificateNumber 字段替换为 HSM 密文
|
|||
r = initOneField(user, "certificateNumber", user.getCertificateNumber(), null, true); |
|||
if (r == 1) success++; else if (r == -1) fail++; else skip++; |
|||
|
|||
// 备份字段 + 密文替换更新回 sys_user(须在完整性摘要之前落库,保证摘要口径与库中值一致)
|
|||
updateSensitiveFields(user); |
|||
|
|||
try { |
|||
doSaveIntegrityHash(TABLE_SYS_USER, bizId, |
|||
"password,phonenumber,certificateNumber", |
|||
StrUtil.nullToEmpty(user.getPassword()), |
|||
StrUtil.nullToEmpty(user.getPhonenumber()), |
|||
StrUtil.nullToEmpty(user.getCertificateNumber())); |
|||
} catch (Exception e) { |
|||
log.error("存量数据完整性摘要初始化失败, bizId={}", bizId, e); |
|||
} |
|||
} |
|||
if (!result.hasNext()) { |
|||
break; |
|||
} |
|||
page.setCurrent(page.getCurrent() + 1); |
|||
} |
|||
log.info("sys_user 存量数据初始化完成: 扫描用户数={}, 成功字段数={}, 跳过字段数={}, 失败字段数={}", |
|||
total, success, skip, fail); |
|||
Map<String, Integer> stats = new HashMap<>(); |
|||
stats.put("total", total); |
|||
stats.put("success", success); |
|||
stats.put("skip", skip); |
|||
stats.put("fail", fail); |
|||
return stats; |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public int syncIntegrityHashModifiedUsers() { |
|||
Date begin = DateUtil.beginOfDay(DateUtil.yesterday()); |
|||
Date end = DateUtil.endOfDay(DateUtil.yesterday()); |
|||
|
|||
int processed = 0; |
|||
long pageSize = 500L; |
|||
Page<SysUser> page = new Page<>(1, pageSize); |
|||
while (true) { |
|||
Page<SysUser> result = DataPermissionHelper.ignore( |
|||
() -> sysUserMapper.selectModifiedUserByUpdateTime(page, begin, end)); |
|||
for (SysUser user : result.getRecords()) { |
|||
try { |
|||
saveIntegrityHash(TABLE_SYS_USER, String.valueOf(user.getUserId()), |
|||
"password,phonenumber,certificateNumber", |
|||
user.getPassword() != null ? user.getPassword() : "", |
|||
user.getPhonenumber() != null ? user.getPhonenumber() : "", |
|||
user.getCertificateNumber() != null ? user.getCertificateNumber() : ""); |
|||
processed++; |
|||
} catch (Exception e) { |
|||
log.warn("同步前一天修改用户完整性摘要失败, userId={}", user.getUserId(), e); |
|||
} |
|||
} |
|||
if (!result.hasNext()) { |
|||
break; |
|||
} |
|||
page.setCurrent(page.getCurrent() + 1); |
|||
} |
|||
log.info("同步前一天修改用户完整性摘要完成, 处理数量={}", processed); |
|||
return processed; |
|||
} |
|||
|
|||
@Override |
|||
public int syncIntegrityHashModifiedOperLogs() { |
|||
Date begin = DateUtil.beginOfDay(DateUtil.yesterday()); |
|||
Date end = DateUtil.endOfDay(DateUtil.yesterday()); |
|||
|
|||
int processed = 0; |
|||
long pageSize = 500L; |
|||
Page<SysOperLog> page = new Page<>(1, pageSize); |
|||
while (true) { |
|||
Page<SysOperLog> result = DataPermissionHelper.ignore( |
|||
() -> sysOperLogMapper.selectByOperTimeRange(page, begin, end)); |
|||
for (SysOperLog operLog : result.getRecords()) { |
|||
try { |
|||
saveIntegrityHash(TABLE_SYS_OPER_LOG, String.valueOf(operLog.getOperId()), |
|||
"business_type,method,oper_name,oper_param,oper_time", |
|||
toStr(operLog.getBusinessType()), |
|||
toStr(operLog.getMethod()), |
|||
toStr(operLog.getOperName()), |
|||
toStr(operLog.getOperParam()), |
|||
toStr(operLog.getOperTime())); |
|||
processed++; |
|||
} catch (Exception e) { |
|||
log.warn("同步前一天操作日志完整性摘要失败, operId={}", operLog.getOperId(), e); |
|||
} |
|||
} |
|||
if (!result.hasNext()) { |
|||
break; |
|||
} |
|||
page.setCurrent(page.getCurrent() + 1); |
|||
} |
|||
log.info("同步前一天操作日志完整性摘要完成, 处理数量={}", processed); |
|||
return processed; |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Integer> initSysUserIntegrityData(String loginName) { |
|||
int total = 0, success = 0, skip = 0, fail = 0; |
|||
long pageSize = 500L; |
|||
Page<SysUser> page = new Page<>(1, pageSize); |
|||
while (true) { |
|||
Page<SysUser> result = DataPermissionHelper.ignore(() -> sysUserMapper.selectPage(page, |
|||
new LambdaQueryWrapper<SysUser>() |
|||
.eq(SysUser::getDelFlag, "0") |
|||
.eq(StrUtil.isNotBlank(loginName), SysUser::getLoginName, loginName))); |
|||
List<SysUser> records = result.getRecords(); |
|||
for (SysUser user : records) { |
|||
total++; |
|||
String bizId = String.valueOf(user.getUserId()); |
|||
if (existsIntegrityRecord(TABLE_SYS_USER, bizId)) { |
|||
skip++; |
|||
continue; |
|||
} |
|||
try { |
|||
doSaveIntegrityHash(TABLE_SYS_USER, bizId, |
|||
"password,phonenumber,certificateNumber", |
|||
StrUtil.nullToEmpty(user.getPassword()), |
|||
StrUtil.nullToEmpty(user.getPhonenumber()), |
|||
StrUtil.nullToEmpty(user.getCertificateNumber())); |
|||
success++; |
|||
} catch (Exception e) { |
|||
log.error("存量数据完整性摘要初始化失败, bizId={}", bizId, e); |
|||
fail++; |
|||
} |
|||
} |
|||
if (!result.hasNext()) { |
|||
break; |
|||
} |
|||
page.setCurrent(page.getCurrent() + 1); |
|||
} |
|||
log.info("sys_user 存量完整性摘要初始化完成: 扫描用户数={}, 成功={}, 跳过={}, 失败={}", |
|||
total, success, skip, fail); |
|||
Map<String, Integer> stats = new HashMap<>(); |
|||
stats.put("total", total); |
|||
stats.put("success", success); |
|||
stats.put("skip", skip); |
|||
stats.put("fail", fail); |
|||
return stats; |
|||
} |
|||
|
|||
/** |
|||
* 查询指定业务记录是否已存在完整性摘要 |
|||
*/ |
|||
private boolean existsIntegrityRecord(String tableName, String bizId) { |
|||
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<>(); |
|||
lqw.eq(CryptoIntegrityData::getTableName, tableName) |
|||
.eq(CryptoIntegrityData::getBizId, bizId) |
|||
.eq(CryptoIntegrityData::getIsDeleted, 0); |
|||
return cryptoIntegrityDataMapper.selectCount(lqw) > 0; |
|||
} |
|||
|
|||
/** |
|||
* 初始化单个敏感字段(幂等),成功后原始值备份到 crypto_remark_*,原字段替换为 HSM 密文 |
|||
* |
|||
* @param plainPattern 明文形态兜底正则(aesCipherInput=false 时使用) |
|||
* @param aesCipherInput true=存储值为 AES 密文,AES 密文直接作为 HSM 加密输入(先 AES 后 HSM) |
|||
* @return 1=成功、0=跳过、-1=失败 |
|||
*/ |
|||
private int initOneField(SysUser user, String fieldCode, String storedValue, |
|||
String plainPattern, boolean aesCipherInput) { |
|||
String bizId = String.valueOf(user.getUserId()); |
|||
if (StrUtil.isBlank(storedValue)) { |
|||
return 0; |
|||
} |
|||
CryptoSensitiveData existing = queryActiveRecord(TABLE_SYS_USER, bizId, fieldCode); |
|||
if (existing != null) { |
|||
// 已有密文记录(历史初始化过):直接用已有密文做备份+替换,保证重跑可补齐
|
|||
if (StrUtil.isNotBlank(existing.getCipherText())) { |
|||
applyCipher(user, fieldCode, storedValue, existing.getCipherText()); |
|||
return 1; |
|||
} |
|||
return 0; |
|||
} |
|||
String hsmInput; |
|||
if (aesCipherInput) { |
|||
// 双加密规则:明文 → AES 加密 → HSM 加密。
|
|||
// 存量即为 AES 密文,AES 解密仅用于形态判定,解密成功则原值直接作为 HSM 输入
|
|||
if (!isAesCipherText(storedValue)) { |
|||
log.warn("存量字段非AES密文形态, 跳过初始化, bizId={}, fieldCode={}", bizId, fieldCode); |
|||
return 0; |
|||
} |
|||
hsmInput = storedValue; |
|||
} else { |
|||
hsmInput = (plainPattern != null && storedValue.matches(plainPattern)) ? storedValue : null; |
|||
if (hsmInput == null) { |
|||
return 0; |
|||
} |
|||
} |
|||
try { |
|||
String cipherText = encryptAndSave(TABLE_SYS_USER, bizId, fieldCode, hsmInput); |
|||
if (StrUtil.isBlank(cipherText)) { |
|||
return -1; |
|||
} |
|||
applyCipher(user, fieldCode, storedValue, cipherText); |
|||
return 1; |
|||
} catch (Exception e) { |
|||
log.error("存量数据初始化失败, bizId={}, fieldCode={}", bizId, fieldCode, e); |
|||
return -1; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 原始值备份到 crypto_remark_*,原字段替换为 HSM 密文(仅修改内存对象,统一由 updateSensitiveFields 落库) |
|||
* password→crypto_remark_one、phonenumber→crypto_remark_two、certificateNumber→crypto_remark_three |
|||
*/ |
|||
private void applyCipher(SysUser user, String fieldCode, String originalValue, String cipherText) { |
|||
switch (fieldCode) { |
|||
case "password" -> { |
|||
user.setCryptoRemarkOne(originalValue); |
|||
user.setPassword(cipherText); |
|||
} |
|||
case "phonenumber" -> { |
|||
user.setCryptoRemarkTwo(originalValue); |
|||
user.setPhonenumber(cipherText); |
|||
} |
|||
case "certificateNumber" -> { |
|||
user.setCryptoRemarkThree(originalValue); |
|||
user.setCertificateNumber(cipherText); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将备份字段与密文替换结果更新回 sys_user(使用 wrapper 仅更新涉及列,不触发 update_time 自动填充) |
|||
*/ |
|||
private void updateSensitiveFields(SysUser user) { |
|||
if (StrUtil.isBlank(user.getCryptoRemarkOne()) |
|||
&& StrUtil.isBlank(user.getCryptoRemarkTwo()) |
|||
&& StrUtil.isBlank(user.getCryptoRemarkThree())) { |
|||
return; |
|||
} |
|||
LambdaUpdateWrapper<SysUser> uw = new LambdaUpdateWrapper<>(); |
|||
uw.eq(SysUser::getUserId, user.getUserId()) |
|||
.set(StrUtil.isNotBlank(user.getCryptoRemarkOne()), SysUser::getCryptoRemarkOne, user.getCryptoRemarkOne()) |
|||
.set(StrUtil.isNotBlank(user.getPassword()), SysUser::getPassword, user.getPassword()) |
|||
.set(StrUtil.isNotBlank(user.getCryptoRemarkTwo()), SysUser::getCryptoRemarkTwo, user.getCryptoRemarkTwo()) |
|||
.set(StrUtil.isNotBlank(user.getPhonenumber()), SysUser::getPhonenumber, user.getPhonenumber()) |
|||
.set(StrUtil.isNotBlank(user.getCryptoRemarkThree()), SysUser::getCryptoRemarkThree, user.getCryptoRemarkThree()) |
|||
.set(StrUtil.isNotBlank(user.getCertificateNumber()), SysUser::getCertificateNumber, user.getCertificateNumber()); |
|||
sysUserMapper.update(null, uw); |
|||
} |
|||
|
|||
/** |
|||
* 判断存储值是否为 AES 密文形态(用 srnwg.secretKey 尝试解密,成功即可解密) |
|||
*/ |
|||
private boolean isAesCipherText(String value) { |
|||
try { |
|||
return StrUtil.isNotBlank(SecureUtil.aes(SecureUtil.decode(secretKey)).decryptStr(value)); |
|||
} catch (Exception ignored) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private CryptoSensitiveData queryActiveRecord(String tableName, String bizId, String fieldCode) { |
|||
LambdaQueryWrapper<CryptoSensitiveData> lqw = new LambdaQueryWrapper<>(); |
|||
lqw.eq(CryptoSensitiveData::getTableName, tableName) |
|||
.eq(CryptoSensitiveData::getBizId, bizId) |
|||
.eq(CryptoSensitiveData::getFieldCode, fieldCode) |
|||
.eq(CryptoSensitiveData::getIsActive, 1) |
|||
.eq(CryptoSensitiveData::getIsDeleted, 0); |
|||
return cryptoSensitiveDataMapper.selectOne(lqw); |
|||
} |
|||
|
|||
private void deactivateExisting(String tableName, String bizId, String fieldCode) { |
|||
LambdaUpdateWrapper<CryptoSensitiveData> uw = new LambdaUpdateWrapper<>(); |
|||
uw.eq(CryptoSensitiveData::getTableName, tableName) |
|||
.eq(CryptoSensitiveData::getBizId, bizId) |
|||
.eq(CryptoSensitiveData::getFieldCode, fieldCode) |
|||
.eq(CryptoSensitiveData::getIsActive, 1) |
|||
.set(CryptoSensitiveData::getIsActive, 0); |
|||
cryptoSensitiveDataMapper.update(null, uw); |
|||
} |
|||
|
|||
private void saveAuditRecord(String tableName, String bizId, String fieldCode, |
|||
Long sensitiveDataId, int operateType, String remark) { |
|||
CryptoOperationAudit audit = new CryptoOperationAudit(); |
|||
audit.setSensitiveDataId(sensitiveDataId); |
|||
audit.setTableName(tableName); |
|||
audit.setBizId(bizId); |
|||
audit.setFieldCode(fieldCode); |
|||
audit.setOperateType(operateType); |
|||
audit.setRemark(remark); |
|||
try { |
|||
audit.setOperatorId(LoginHelper.getUserId()); |
|||
} catch (Exception ignored) { |
|||
} |
|||
cryptoOperationAuditMapper.insert(audit); |
|||
} |
|||
|
|||
private String calculateHash(String fieldList, String... plainValues) { |
|||
StringBuilder sb = new StringBuilder(); |
|||
for (int i = 0; i < plainValues.length; i++) { |
|||
if (i > 0) sb.append(","); |
|||
sb.append(plainValues[i] != null ? plainValues[i] : ""); |
|||
} |
|||
String inputHex = HexUtil.encodeHexStr(sb.toString().getBytes(StandardCharsets.UTF_8)); |
|||
return passwordPartyService.hmacWithIndex(inputHex); |
|||
} |
|||
|
|||
private static byte[] hexToBytes(String hex) { |
|||
int len = hex.length(); |
|||
byte[] data = new byte[len / 2]; |
|||
for (int i = 0; i < len; i += 2) { |
|||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) |
|||
+ Character.digit(hex.charAt(i + 1), 16)); |
|||
} |
|||
return data; |
|||
} |
|||
|
|||
/** |
|||
* 校验 sys_user 表数据完整性 |
|||
* <p> |
|||
* 根据基准摘要记录的 biz_id 回查 sys_user 当前数据, |
|||
* 按 field_list 解析参与字段(password、phonenumber、certificateNumber), |
|||
* 重新计算 HMAC-SM3 摘要并与基准 sm3_hash 比对。 |
|||
* |
|||
* @param record 基准摘要记录 |
|||
* @return true=校验通过,false=数据缺失或摘要不一致(疑似篡改) |
|||
*/ |
|||
private boolean verifySysUserIntegrity(CryptoIntegrityData record) { |
|||
SysUser user = DataPermissionHelper.ignore( |
|||
() -> sysUserMapper.selectById(Long.valueOf(record.getBizId()))); |
|||
if (user == null) { |
|||
log.warn("完整性校验: sys_user 数据不存在, bizId={}", record.getBizId()); |
|||
return false; |
|||
} |
|||
String[] fields = record.getFieldList().split(","); |
|||
String[] values = new String[fields.length]; |
|||
for (int i = 0; i < fields.length; i++) { |
|||
String field = fields[i].trim(); |
|||
switch (field) { |
|||
case "password" -> values[i] = user.getPassword() != null ? user.getPassword() : ""; |
|||
case "phonenumber" -> values[i] = user.getPhonenumber() != null ? user.getPhonenumber() : ""; |
|||
case "certificateNumber" -> values[i] = user.getCertificateNumber() != null ? user.getCertificateNumber() : ""; |
|||
default -> throw new ServiceException("不支持的完整性校验字段: " + field); |
|||
} |
|||
} |
|||
String currentHash = calculateHash(record.getFieldList(), values); |
|||
boolean matched = currentHash.equals(record.getSm3Hash()); |
|||
if (!matched) { |
|||
log.warn("数据完整性校验未通过, 疑似篡改, tableName={}, bizId={}", record.getTableName(), record.getBizId()); |
|||
} |
|||
return matched; |
|||
} |
|||
|
|||
/** |
|||
* 校验 sys_oper_log 表数据完整性 |
|||
* <p> |
|||
* 根据基准摘要记录的 biz_id 回查 sys_oper_log 当前数据, |
|||
* 固定按 business_type、method、oper_name、oper_param、oper_time 五个字段 |
|||
* 重新计算 HMAC-SM3 摘要并与基准 sm3_hash 比对。 |
|||
* 非字符串字段统一按 String.valueOf 转换,与保存摘要时的取值口径保持一致。 |
|||
* |
|||
* @param record 基准摘要记录 |
|||
* @return true=校验通过,false=数据缺失或摘要不一致(疑似篡改) |
|||
*/ |
|||
private boolean verifySysOperLogIntegrity(CryptoIntegrityData record) { |
|||
SysOperLog operLog = DataPermissionHelper.ignore( |
|||
() -> sysOperLogMapper.selectById(Long.valueOf(record.getBizId()))); |
|||
if (operLog == null) { |
|||
log.warn("完整性校验: sys_oper_log 数据不存在, bizId={}", record.getBizId()); |
|||
return false; |
|||
} |
|||
String[] values = new String[]{ |
|||
toStr(operLog.getBusinessType()), |
|||
toStr(operLog.getMethod()), |
|||
toStr(operLog.getOperName()), |
|||
toStr(operLog.getOperParam()), |
|||
toStr(operLog.getOperTime()) |
|||
}; |
|||
String currentHash = calculateHash(record.getFieldList(), values); |
|||
boolean matched = currentHash.equals(record.getSm3Hash()); |
|||
if (!matched) { |
|||
log.warn("数据完整性校验未通过, 疑似篡改, tableName={}, bizId={}", record.getTableName(), record.getBizId()); |
|||
} |
|||
return matched; |
|||
} |
|||
|
|||
private static String toStr(Object value) { |
|||
return value != null ? String.valueOf(value) : ""; |
|||
} |
|||
} |
|||
@ -0,0 +1,267 @@ |
|||
package org.dromara.system.service.impl; |
|||
|
|||
import cn.hutool.http.HttpRequest; |
|||
import cn.hutool.http.HttpResponse; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.core.exception.base.BaseException; |
|||
import org.dromara.common.core.utils.StringUtils; |
|||
import org.dromara.system.config.PasswordPartyConfig; |
|||
import org.dromara.system.domain.vo.SymmetryEncryptVo; |
|||
import org.dromara.system.service.IPasswordPartyService; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 密码服务平台 |
|||
* |
|||
* @author vincent |
|||
* @date 2026-09-22 |
|||
*/ |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
@Service |
|||
public class PasswordPartyServiceImpl implements IPasswordPartyService { |
|||
|
|||
private final PasswordPartyConfig passwordPartyConfig; |
|||
|
|||
private static final int KEY_INDEX = 5; |
|||
private static final String ALG_TYPE = "SM4-CBC"; |
|||
|
|||
/** |
|||
* 生成随机数 |
|||
* |
|||
* @param length 随机数长度 |
|||
* @return 随机数(Hex 16进制类型) |
|||
*/ |
|||
@Override |
|||
public String generateRandom(int length) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("length", length); |
|||
JSONObject data = callHsmApi("/hsm/api/GenerateRandom", params, "生成随机数"); |
|||
return data.getString("random"); |
|||
} |
|||
|
|||
/** |
|||
* 对称加密 |
|||
* |
|||
* @param inputData 加密原文(Hex 16进制类型) |
|||
* @return 对称加密结果 |
|||
*/ |
|||
@Override |
|||
public SymmetryEncryptVo symmetryEncrypt(String inputData) { |
|||
// SymmetryEncryptVo symmetryEncryptVo = new SymmetryEncryptVo();
|
|||
// symmetryEncryptVo.setIvData("1234567890123456");
|
|||
// symmetryEncryptVo.setOutData("123");
|
|||
// return symmetryEncryptVo;
|
|||
// TODO 生成随机数,目前使用SecureRandom,后续需要替换为HSM生成随机数
|
|||
String iv = generateRandom(16); |
|||
// SecureRandom secureRandom = new SecureRandom();
|
|||
// byte[] ivBytes = new byte[16];
|
|||
// secureRandom.nextBytes(ivBytes);
|
|||
// StringBuilder sb = new StringBuilder();
|
|||
// for (byte b : ivBytes) {
|
|||
// sb.append(String.format("%02x", b));
|
|||
// }
|
|||
// String iv = sb.toString();
|
|||
JSONObject params = buildSymmetryParams(iv, inputData); |
|||
|
|||
JSONObject data = callHsmApi("/hsm/api/symmetryEncrypt", params, "对称加密"); |
|||
return buildSymmetryEncryptVo(data); |
|||
} |
|||
|
|||
/** |
|||
* 对称解密 |
|||
* |
|||
* @param iv IV值,CBC/CFB/OFB模式时必传(Hex 16进制类型) |
|||
* @param inputData 解密原文(Hex 16进制类型) |
|||
* @return 对称解密结果 |
|||
*/ |
|||
@Override |
|||
public SymmetryEncryptVo symmetryDecrypt(String iv, String inputData) { |
|||
JSONObject params = buildSymmetryParams(iv, inputData); |
|||
|
|||
JSONObject data = callHsmApi("/hsm/api/symmetryDecrypt", params, "对称解密"); |
|||
return buildSymmetryEncryptVo(data); |
|||
} |
|||
|
|||
/** |
|||
* SM2签名 |
|||
* |
|||
* @param plainData 明文摘要,先使用杂凑接口计算摘要(Hex 16进制类型) |
|||
* @return 签名值(Hex 16进制类型) |
|||
*/ |
|||
@Override |
|||
public String sm2SignData(String plainData) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("keyIndex", KEY_INDEX); |
|||
params.put("plainData", plainData); |
|||
|
|||
JSONObject data = callHsmApi("/hsm/api/SM2SignData", params, "SM2签名"); |
|||
return data.getString("signData"); |
|||
} |
|||
|
|||
/** |
|||
* SM2验签 |
|||
* |
|||
* @param publicKey 公钥,使用导出公钥接口获取(Hex 16进制类型) |
|||
* @param signValue 签名值(Hex 16进制类型) |
|||
* @param plainData 明文摘要,先使用杂凑接口计算摘要(Hex 16进制类型) |
|||
* @return true=验证通过 |
|||
*/ |
|||
@Override |
|||
public boolean sm2Verify(String publicKey, String signValue, String plainData) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("algType", ALG_TYPE); |
|||
params.put("publicKey", publicKey); |
|||
params.put("signValue", signValue); |
|||
params.put("plainData", plainData); |
|||
|
|||
doPost("/hsm/api/sm2Verify", params, "SM2验签"); |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* 非对称加密 |
|||
* |
|||
* @param publcKey 公钥(Hex 16进制类型) |
|||
* @param data 明文(Hex 16进制类型) |
|||
* @return 加密密文(Hex 16进制类型) |
|||
*/ |
|||
@Override |
|||
public String externalEncrypt(String publcKey, String data) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("algType", ALG_TYPE); |
|||
params.put("publcKey", publcKey); |
|||
params.put("data", data); |
|||
|
|||
JSONObject result = callHsmApi("/hsm/api/externalEncrypt", params, "非对称加密"); |
|||
return result.getString("encryptData"); |
|||
} |
|||
|
|||
/** |
|||
* 非对称解密 |
|||
* |
|||
* @param enc 密文(Hex 16进制类型) |
|||
* @return 解密明文(Hex 16进制类型) |
|||
*/ |
|||
@Override |
|||
public String internalDecrypt(String enc) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("keyIndex", KEY_INDEX); |
|||
params.put("algId", ALG_TYPE); |
|||
params.put("enc", enc); |
|||
|
|||
JSONObject data = callHsmApi("/hsm/api/internalDecrypt", params, "非对称解密"); |
|||
return data.getString("plainData"); |
|||
} |
|||
|
|||
/** |
|||
* HMAC摘要 |
|||
* |
|||
* @param inputData 明文摘要(Hex 16进制类型,必填) |
|||
* @return HMAC值(Hex 16进制类型) |
|||
*/ |
|||
@Override |
|||
public String hmacWithIndex(String inputData) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("keyIndex", KEY_INDEX); |
|||
params.put("algType", "SM3"); |
|||
params.put("inputData", inputData); |
|||
|
|||
JSONObject data = callHsmApi("/hsm/api/HmacWithIndex", params, "HMAC"); |
|||
return data.getString("hmac"); |
|||
} |
|||
|
|||
/** |
|||
* 构建对称加密/解密的公共请求参数 |
|||
*/ |
|||
private JSONObject buildSymmetryParams(String iv, String inputData) { |
|||
JSONObject params = new JSONObject(); |
|||
params.put("keyIndex", KEY_INDEX); |
|||
params.put("algType", ALG_TYPE); |
|||
if (iv != null) { |
|||
params.put("iv", iv); |
|||
} |
|||
params.put("inputData", inputData); |
|||
return params; |
|||
} |
|||
|
|||
/** |
|||
* 组装对称加密/解密的响应VO |
|||
*/ |
|||
private SymmetryEncryptVo buildSymmetryEncryptVo(JSONObject data) { |
|||
SymmetryEncryptVo vo = new SymmetryEncryptVo(); |
|||
vo.setOutData(data.getString("outData")); |
|||
vo.setIvData(data.getString("ivData")); |
|||
return vo; |
|||
} |
|||
|
|||
/** |
|||
* HSM接口公共调用方法(含data非空校验) |
|||
* |
|||
* @param apiPath 接口路径 |
|||
* @param params 请求参数 |
|||
* @param apiName 接口名称,用于日志和异常信息 |
|||
* @return 响应中的data节点 |
|||
*/ |
|||
private JSONObject callHsmApi(String apiPath, JSONObject params, String apiName) { |
|||
JSONObject jsonObject = doPost(apiPath, params, apiName); |
|||
JSONObject data = jsonObject.getJSONObject("data"); |
|||
|
|||
//TODO 测试数据
|
|||
// String jsonStr = "{\"outData\":\"8216fc247b987ca99e9b5f77b375ed1a\",\"ivData\":\"123\"}";
|
|||
// JSONObject data = JSONObject.parseObject(jsonStr);
|
|||
|
|||
if (data == null) { |
|||
throw new BaseException("HSM" + apiName + "响应data为空"); |
|||
} |
|||
return data; |
|||
} |
|||
|
|||
/** |
|||
* HSM接口底层HTTP调用(POST + 状态码校验 + code校验) |
|||
* |
|||
* @param apiPath 接口路径 |
|||
* @param params 请求参数 |
|||
* @param apiName 接口名称,用于日志和异常信息 |
|||
* @return 完整响应JSON |
|||
*/ |
|||
private JSONObject doPost(String apiPath, JSONObject params, String apiName) { |
|||
try { |
|||
String url = passwordPartyConfig.getBaseUrl() + apiPath; |
|||
log.info("HSM{}请求URL: {}, 参数: {}", apiName, url, params); |
|||
|
|||
HttpRequest request = HttpRequest.post(url) |
|||
.header("Content-Type", "application/json;charset=utf-8") |
|||
.body(params.toJSONString()) |
|||
.timeout(10000); |
|||
// 令牌配置存在时才携带鉴权头(dev 本地服务可能无鉴权)
|
|||
if (StringUtils.isNotBlank(passwordPartyConfig.getAuthToken())) { |
|||
request.header("Authorization", passwordPartyConfig.getAuthToken()); |
|||
} |
|||
HttpResponse response = request.execute(); |
|||
|
|||
if (!response.isOk()) { |
|||
response.close(); |
|||
throw new BaseException("HSM" + apiName + "接口调用失败,HTTP状态码:" + response.getStatus()); |
|||
} |
|||
|
|||
String result = response.body(); |
|||
response.close(); |
|||
log.info("HSM{}响应: {}", apiName, result); |
|||
|
|||
JSONObject jsonObject = JSONObject.parseObject(result); |
|||
int code = jsonObject.getIntValue("code"); |
|||
if (code != 0) { |
|||
throw new BaseException("HSM" + apiName + "失败: " + jsonObject.getString("msg")); |
|||
} |
|||
return jsonObject; |
|||
} catch (BaseException e) { |
|||
throw e; |
|||
} catch (Exception e) { |
|||
log.error("HSM{}异常", apiName, e); |
|||
throw new BaseException("HSM" + apiName + "异常: " + e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue