Browse Source

优化密评相关接口

dev-V1.0.9
vincent 3 weeks ago
parent
commit
c3c82361a0
  1. 4
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/executor/BusinessDataSyncScheduler.java
  2. 3
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnforcementRegistrationController.java
  3. 1
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppNonEnforcementRegistrationController.java
  4. 58
      zdxt-modules/zdxt-system/src/main/java/org/dromara/system/controller/system/CryptoSensitiveController.java
  5. 33
      zdxt-modules/zdxt-system/src/main/java/org/dromara/system/service/ICryptoSensitiveHsmService.java
  6. 189
      zdxt-modules/zdxt-system/src/main/java/org/dromara/system/service/impl/CryptoSensitiveHsmServiceImpl.java
  7. 4
      zdxt-modules/zdxt-system/src/main/java/org/dromara/system/service/impl/PasswordPartyServiceImpl.java
  8. 2
      zdxt-web-server/zdxt-admin-web-server/src/main/resources/businessdatasyn/sys_user.json

4
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/executor/BusinessDataSyncScheduler.java

@ -59,6 +59,10 @@ public class BusinessDataSyncScheduler {
log.warn("[BizSync] 配置不存在: {}", configKey); log.warn("[BizSync] 配置不存在: {}", configKey);
return; return;
} }
if (!config.isEnabled()) {
log.warn("[BizSync] 配置已禁用, 跳过手动同步: {}", configKey);
return;
}
tableSyncExecutor.runOneTable(config, false); tableSyncExecutor.runOneTable(config, false);
} }

3
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnforcementRegistrationController.java

@ -90,7 +90,6 @@ public class AppEnforcementRegistrationController extends BaseController {
@PostMapping("/add") @PostMapping("/add")
@ResponseBody @ResponseBody
@RepeatSubmit() @RepeatSubmit()
@Log(title = "行政执法登记", businessType = BusinessType.INSERT)
public R<Void> addSave(@RequestBody EnforcementRegistrationBo bo) public R<Void> addSave(@RequestBody EnforcementRegistrationBo bo)
{ {
if (StringUtils.isNotEmpty(bo.getId())) { if (StringUtils.isNotEmpty(bo.getId())) {
@ -201,7 +200,6 @@ public class AppEnforcementRegistrationController extends BaseController {
@PostMapping("/edit") @PostMapping("/edit")
@ResponseBody @ResponseBody
@RepeatSubmit() @RepeatSubmit()
@Log(title = "行政执法登记", businessType = BusinessType.UPDATE)
public R<Void> editSave(@RequestBody EnforcementRegistrationBo bo) public R<Void> editSave(@RequestBody EnforcementRegistrationBo bo)
{ {
int resNum = 0; int resNum = 0;
@ -238,7 +236,6 @@ public class AppEnforcementRegistrationController extends BaseController {
@PostMapping("/editInfo") @PostMapping("/editInfo")
@ResponseBody @ResponseBody
@RepeatSubmit() @RepeatSubmit()
@Log(title = "行政执法登记", businessType = BusinessType.UPDATE)
public R<Void> editInfo(@RequestBody EnforcementRegistrationBo bo) public R<Void> editInfo(@RequestBody EnforcementRegistrationBo bo)
{ {
bo.setIsSubmit(0L); bo.setIsSubmit(0L);

1
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppNonEnforcementRegistrationController.java

@ -28,7 +28,6 @@ public class AppNonEnforcementRegistrationController extends BaseController {
*/ */
@PostMapping("/add") @PostMapping("/add")
@ResponseBody @ResponseBody
@Log(title = "非行政执法登记", businessType = BusinessType.INSERT)
public R<Void> addSave(@RequestBody NonEnforcementRegistrationBo bo) public R<Void> addSave(@RequestBody NonEnforcementRegistrationBo bo)
{ {

58
zdxt-modules/zdxt-system/src/main/java/org/dromara/system/controller/system/CryptoSensitiveController.java

@ -0,0 +1,58 @@
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 = "dumoulin392";
}
return R.ok(cryptoSensitiveHsmService.initSysUserSensitiveData(loginName));
}
// /**
// * 初始化 sys_user 历史存量数据到 crypto_integrity_data 表
// *
// * @param loginName 账号过滤条件,为空则初始化全部存量用户
// */
// @Log(title = "密评完整性摘要初始化", businessType = BusinessType.OTHER)
// @PostMapping("/initSysUserIntegrity")
// public R<Map<String, Integer>> initSysUserIntegrity(String loginName) {
// if (loginName == null){
// loginName = "dumoulin392";
// }
// return R.ok(cryptoSensitiveHsmService.initSysUserIntegrityData(loginName));
// }
}

33
zdxt-modules/zdxt-system/src/main/java/org/dromara/system/service/ICryptoSensitiveHsmService.java

@ -1,5 +1,7 @@
package org.dromara.system.service; package org.dromara.system.service;
import java.util.Map;
/** /**
* 密评敏感字段 HSM 加密服务 * 密评敏感字段 HSM 加密服务
* <p> * <p>
@ -100,4 +102,35 @@ public interface ICryptoSensitiveHsmService {
*/ */
int verifyIntegrityHash(String tableName); 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);
} }

189
zdxt-modules/zdxt-system/src/main/java/org/dromara/system/service/impl/CryptoSensitiveHsmServiceImpl.java

@ -3,6 +3,7 @@ package org.dromara.system.service.impl;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.HexUtil; import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil; 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.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -24,12 +25,16 @@ import org.dromara.system.mapper.SysOperLogMapper;
import org.dromara.system.mapper.SysUserMapper; import org.dromara.system.mapper.SysUserMapper;
import org.dromara.system.service.ICryptoSensitiveHsmService; import org.dromara.system.service.ICryptoSensitiveHsmService;
import org.dromara.system.service.IPasswordPartyService; import org.dromara.system.service.IPasswordPartyService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@ -43,6 +48,9 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
private final SysUserMapper sysUserMapper; private final SysUserMapper sysUserMapper;
private final SysOperLogMapper sysOperLogMapper; private final SysOperLogMapper sysOperLogMapper;
@Value("${srnwg.secretKey}")
private String secretKey;
private static final String KEY_VERSION = "V1"; private static final String KEY_VERSION = "V1";
private static final String TABLE_SYS_USER = "sys_user"; private static final String TABLE_SYS_USER = "sys_user";
private static final String TABLE_SYS_OPER_LOG = "sys_oper_log"; private static final String TABLE_SYS_OPER_LOG = "sys_oper_log";
@ -50,6 +58,7 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public String encryptAndSave(String tableName, String bizId, String fieldCode, String plainText) { public String encryptAndSave(String tableName, String bizId, String fieldCode, String plainText) {
try{
if (StrUtil.isBlank(plainText)) { if (StrUtil.isBlank(plainText)) {
return null; return null;
} }
@ -81,6 +90,10 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
saveAuditRecord(tableName, bizId, fieldCode, record.getId(), 1, "HSM SM4-CBC 加密存储"); saveAuditRecord(tableName, bizId, fieldCode, record.getId(), 1, "HSM SM4-CBC 加密存储");
} }
return encryptResult.getOutData(); return encryptResult.getOutData();
} catch (Exception e) {
log.error("HSM加密保存失败, tableName={}, bizId={}, fieldCode={}", tableName, bizId, fieldCode, e);
}
return "";
} }
@Override @Override
@ -123,7 +136,17 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveIntegrityHash(String tableName, String bizId, String fieldList, String... plainValues) { public void saveIntegrityHash(String tableName, String bizId, String fieldList, String... plainValues) {
try{ 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); String hmacHash = calculateHash(fieldList, plainValues);
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<>();
@ -155,13 +178,12 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
record.setIsDeleted(0); record.setIsDeleted(0);
cryptoIntegrityDataMapper.insert(record); cryptoIntegrityDataMapper.insert(record);
} }
} catch (Exception e) {
log.error("保存数据完整性摘要失败, tableName={}, bizId={}", tableName, bizId, e);
}
} }
@Override @Override
public int verifyIntegrityHash(String tableName) { public int verifyIntegrityHash(String tableName) {
try{
if (!TABLE_SYS_USER.equals(tableName) && !TABLE_SYS_OPER_LOG.equals(tableName)) { if (!TABLE_SYS_USER.equals(tableName) && !TABLE_SYS_OPER_LOG.equals(tableName)) {
throw new ServiceException("暂不支持该业务表的完整性校验: " + tableName); throw new ServiceException("暂不支持该业务表的完整性校验: " + tableName);
} }
@ -203,8 +225,61 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
} }
log.info("完整性校验完成, tableName={}, 处理={}, 通过={}, 异常={}", tableName, processed, passed, tampered); log.info("完整性校验完成, tableName={}, 处理={}, 通过={}, 异常={}", tableName, processed, passed, tampered);
return processed; 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 一致
int r = initOneField(bizId, "password", user.getPassword(), "^[0-9a-f]{32}$", false);
if (r == 1) success++; else if (r == -1) fail++; else skip++;
// phonenumber / certificateNumber:存量已是 AES 密文,先 AES 加密(已有)再 HSM 加密
r = initOneField(bizId, "phonenumber", user.getPhonenumber(), null, true);
if (r == 1) success++; else if (r == -1) fail++; else skip++;
r = initOneField(bizId, "certificateNumber", user.getCertificateNumber(), null, true);
if (r == 1) success++; else if (r == -1) fail++; else skip++;
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 @Override
public int syncIntegrityHashModifiedUsers() { public int syncIntegrityHashModifiedUsers() {
Date begin = DateUtil.beginOfDay(DateUtil.yesterday()); Date begin = DateUtil.beginOfDay(DateUtil.yesterday());
@ -271,6 +346,112 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService
return 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;
}
/**
* 初始化单个敏感字段(幂等)
*
* @param plainPattern 明文形态兜底正则(aesCipherInput=false 时使用)
* @param aesCipherInput true=存储值为 AES 密文,AES 密文直接作为 HSM 加密输入(先 AES 后 HSM)
* @return 1=成功、0=跳过、-1=失败
*/
private int initOneField(String bizId, String fieldCode, String storedValue,
String plainPattern, boolean aesCipherInput) {
if (StrUtil.isBlank(storedValue)) {
return 0;
}
if (queryActiveRecord(TABLE_SYS_USER, bizId, fieldCode) != null) {
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 {
encryptAndSave(TABLE_SYS_USER, bizId, fieldCode, hsmInput);
return 1;
} catch (Exception e) {
log.error("存量数据初始化失败, bizId={}, fieldCode={}", bizId, fieldCode, e);
return -1;
}
}
/**
* 判断存储值是否为 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) { private CryptoSensitiveData queryActiveRecord(String tableName, String bizId, String fieldCode) {
LambdaQueryWrapper<CryptoSensitiveData> lqw = new LambdaQueryWrapper<>(); LambdaQueryWrapper<CryptoSensitiveData> lqw = new LambdaQueryWrapper<>();
lqw.eq(CryptoSensitiveData::getTableName, tableName) lqw.eq(CryptoSensitiveData::getTableName, tableName)

4
zdxt-modules/zdxt-system/src/main/java/org/dromara/system/service/impl/PasswordPartyServiceImpl.java

@ -49,6 +49,10 @@ public class PasswordPartyServiceImpl implements IPasswordPartyService {
*/ */
@Override @Override
public SymmetryEncryptVo symmetryEncrypt(String inputData) { public SymmetryEncryptVo symmetryEncrypt(String inputData) {
// SymmetryEncryptVo symmetryEncryptVo = new SymmetryEncryptVo();
// symmetryEncryptVo.setIvData("1234567890123456");
// symmetryEncryptVo.setOutData("123");
// return symmetryEncryptVo;
// TODO 生成随机数,目前使用SecureRandom,后续需要替换为HSM生成随机数 // TODO 生成随机数,目前使用SecureRandom,后续需要替换为HSM生成随机数
String iv = generateRandom(16); String iv = generateRandom(16);
// SecureRandom secureRandom = new SecureRandom(); // SecureRandom secureRandom = new SecureRandom();

2
zdxt-web-server/zdxt-admin-web-server/src/main/resources/businessdatasyn/sys_user.json

@ -1,7 +1,7 @@
{ {
"configKey": "sys_user", "configKey": "sys_user",
"description": "用户表双向同步", "description": "用户表双向同步",
"enabled": true, "enabled": false,
"v1": { "v1": {
"table": "sys_user", "table": "sys_user",

Loading…
Cancel
Save