Compare commits
23 Commits
master
...
dev-V1.0.9
| Author | SHA1 | Date |
|---|---|---|
|
|
d349dce593 | 1 week ago |
|
|
b724381de5 | 2 weeks ago |
|
|
bc669e7562 | 2 weeks 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 | 3 weeks ago |
|
|
c3c82361a0 | 4 weeks ago |
|
|
694146a501 | 4 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