Compare commits

...

2 Commits

Author SHA1 Message Date
zhangwenzan c96663187e Merge branch 'test' into zzm1 2025-07-29 11:16:18 +08:00
zhangwenzan 1924cc6cb3 fix:数据加密 2025-07-29 11:10:56 +08:00
16 changed files with 445 additions and 26 deletions

View File

@ -75,4 +75,10 @@ public class Const implements Serializable {
public static final String ADMIN_USER_DEPT_CACHE_NAME = "ADMIN:USER:DEPT:CACHE:";
/**
* 加密前缀
*/
public static final String ENCRYPTED_PREFIX = "ENCRYPTED:";
}

View File

@ -5,8 +5,10 @@ import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.MybatisMapWrapperFactory;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import com.kakarote.core.security.converter.SensitiveDataConverter;
import com.kakarote.core.utils.BaseUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -22,11 +24,22 @@ public class MybatisPlusConfig {
return paginationInterceptor;
}
// @Bean
// public ConfigurationCustomizer configurationCustomizer() {
// return i -> i.setObjectWrapperFactory(new MybatisMapWrapperFactory());
// }
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return i -> i.setObjectWrapperFactory(new MybatisMapWrapperFactory());
return i -> {
i.setObjectWrapperFactory(new MybatisMapWrapperFactory());
// 注册敏感数据类型处理器
TypeHandlerRegistry registry = i.getTypeHandlerRegistry();
registry.register(SensitiveDataConverter.class);
};
}
@Bean
public IdentifierGenerator idGenerator() {
return new CustomIdGenerator();

View File

@ -0,0 +1,105 @@
package com.kakarote.core.security;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.security.SecureRandom;
import java.util.Base64;
@Slf4j
@Component
public class EncryptionService {
// AES-GCM参数配置
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private static final String AES_ALGORITHM = "AES/GCM/NoPadding";
private static final String AES_KEY_ALGORITHM = "AES";
// 系统主密钥(生产环境应从KMS获取)
@Value("${encryption.system-key}")
private String systemKey;
static {
// 注册BouncyCastle加密提供者
Security.addProvider(new BouncyCastleProvider());
}
/**
* AES加密
* @param plaintext 明文
* @return Base64编码的密文
*/
public String encryptAes(String plaintext) {
try {
// 生成随机IV
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
// 初始化密钥
SecretKey secretKey = new SecretKeySpec(Base64.getDecoder().decode(systemKey), AES_KEY_ALGORITHM);
// 初始化加密器
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
// 执行加密
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// 组合IV和密文
byte[] result = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
log.error("AES加密失败", e);
throw new SecurityException("数据加密失败", e);
}
}
/**
* AES解密
* @param ciphertext Base64编码的密文
* @return 明文
*/
public String decryptAes(String ciphertext) {
try {
// 解码Base64密文
byte[] decoded = Base64.getDecoder().decode(ciphertext);
// 提取IV
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(decoded, 0, iv, 0, iv.length);
// 提取实际密文
byte[] encryptedData = new byte[decoded.length - iv.length];
System.arraycopy(decoded, iv.length, encryptedData, 0, encryptedData.length);
// 初始化密钥
SecretKey secretKey = new SecretKeySpec(Base64.getDecoder().decode(systemKey), AES_KEY_ALGORITHM);
// 初始化解密器
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
// 执行解密
byte[] plaintext = cipher.doFinal(encryptedData);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("AES解密失败", e);
throw new SecurityException("数据解密失败", e);
}
}
}

View File

@ -0,0 +1,57 @@
package com.kakarote.core.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.SecureRandom;
import java.util.Base64;
@Slf4j
@Component
public class KeyManager {
// 密钥版本
@Value("${encryption.key-version:V1}")
private String keyVersion;
// 主密钥(生产环境应从KMS获取)
@Value("${encryption.system-key}")
private String systemKey;
/**
* 生成新的AES密钥
* @return Base64编码的密钥
*/
public String generateNewAesKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256, new SecureRandom());
SecretKey secretKey = keyGenerator.generateKey();
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
} catch (Exception e) {
log.error("生成AES密钥失败", e);
throw new SecurityException("密钥生成失败", e);
}
}
/**
* 密钥轮换管理
*/
public void rotateKey() {
// 1. 生成新密钥
String newKey = generateNewAesKey();
// 2. 记录密钥版本
String newVersion = "V" + (Integer.parseInt(keyVersion.substring(1)) + 1);
// 3. 将新密钥存储到安全位置
// ... 密钥存储逻辑 ...
// 4. 异步更新数据库中所有旧版本加密数据
// ... 数据迁移逻辑 ...
log.info("密钥轮换完成,新版本: {}", newVersion);
}
}

View File

@ -0,0 +1,92 @@
package com.kakarote.core.security.converter;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import com.kakarote.core.common.Const;
import com.kakarote.core.security.EncryptionService;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@MappedTypes({String.class})
@MappedJdbcTypes({JdbcType.VARCHAR})
@Component
public class SensitiveDataConverter extends AbstractJsonTypeHandler<String> implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private EncryptionService encryptionService;
// 无参构造函数供MyBatis使用
public SensitiveDataConverter() {
}
// 带参构造函数供Spring使用
public SensitiveDataConverter(EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SensitiveDataConverter.applicationContext = applicationContext;
}
private EncryptionService getEncryptionService() {
if (encryptionService == null) {
encryptionService = applicationContext.getBean(EncryptionService.class);
}
return encryptionService;
}
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException {
if (s != null && !s.isEmpty() && !s.startsWith(Const.ENCRYPTED_PREFIX)) {
s = getEncryptionService().encryptAes(s);
}
preparedStatement.setString(i, s);
}
@Override
public String getNullableResult(ResultSet resultSet, String s) throws SQLException {
String value = resultSet.getString(s);
if (value != null && value.startsWith(Const.ENCRYPTED_PREFIX)) {
value = getEncryptionService().decryptAes(value);
}
return value;
}
@Override
public String getNullableResult(ResultSet resultSet, int i) throws SQLException {
String value = resultSet.getString(i);
if (value != null && value.startsWith(Const.ENCRYPTED_PREFIX)) {
value = getEncryptionService().decryptAes(value);
}
return value;
}
@Override
public String getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String value = callableStatement.getString(i);
if (value != null && value.startsWith(Const.ENCRYPTED_PREFIX)) {
value = getEncryptionService().decryptAes(value);
}
return value;
}
@Override
protected String parse(String json) {
return "";
}
@Override
protected String toJson(String obj) {
return "";
}
}

View File

@ -50,7 +50,7 @@ jetcache:
maxTotal: 50
host: ${spring.redis.host}
port: ${spring.redis.port}
password: ${spring.redis.password}
password:
expireAfterWriteInMillis: 1800000
crm:
@ -86,4 +86,9 @@ crm:
bucketName:
0:
1:
encryption:
system-key: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
key-version: V1
mybatis:
type-handlers-package: com.kakarote.core.security.converter

View File

@ -29,10 +29,7 @@ import com.kakarote.crm.entity.VO.CrmDataCheckVO;
import com.kakarote.crm.entity.VO.CrmInfoNumVO;
import com.kakarote.crm.entity.VO.CrmMembersSelectVO;
import com.kakarote.crm.entity.VO.CrmModelFiledVO;
import com.kakarote.crm.service.CrmUploadExcelService;
import com.kakarote.crm.service.ICrmCustomerService;
import com.kakarote.crm.service.ICrmOpenApiService;
import com.kakarote.crm.service.ICrmTeamMembersService;
import com.kakarote.crm.service.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -75,6 +72,9 @@ public class CrmCustomerController {
@Autowired
private ICrmOpenApiService crmOpenApiService;
@Autowired
private IBatchEncryptionService batchEncryptionService;
@PostMapping("/queryPageList")
@ApiOperation("查询列表页数据")
public Result<BasePage<Map<String, Object>>> queryPageList(@RequestBody CrmSearchBO search) {
@ -553,5 +553,13 @@ public class CrmCustomerController {
String customerName = crmCustomerService.getCustomerName(customerId);
return R.ok(customerName);
}
@PostMapping("/queryEncryptCustomerData")
@ApiOperation("批量加密客户数据")
@ParamAspect
public Result encryptCustomerData(@RequestParam(defaultValue = "1000") int pageSize) {
batchEncryptionService.batchEncryptCustomerData(pageSize);
return Result.ok();
}
}

View File

@ -1,11 +1,13 @@
package com.kakarote.crm.entity.PO;
import com.baomidou.mybatisplus.annotation.*;
import com.kakarote.core.security.converter.SensitiveDataConverter;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.util.Date;
@ -52,17 +54,33 @@ public class CrmCustomer implements Serializable {
private Integer contactsId;
@ApiModelProperty(value = "手机")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String mobile;
@ApiModelProperty(value = "电话")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String telephone;
@ApiModelProperty(value = "网址")
private String website;
@ApiModelProperty(value = "邮箱")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String email;
@ApiModelProperty(value = "省市区")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String address;
@ApiModelProperty(value = "详细地址")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String detailAddress;
@ApiModelProperty(value = "地理位置经度")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String lng;
@ApiModelProperty(value = "地理位置维度")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String lat;
@ApiModelProperty(value = "备注")
private String remark;
@ -73,21 +91,11 @@ public class CrmCustomer implements Serializable {
@ApiModelProperty(value = "负责人ID")
private Long ownerUserId;
@ApiModelProperty(value = "省市区")
private String address;
@ApiModelProperty(value = "定位信息")
@TableField(typeHandler = SensitiveDataConverter.class, jdbcType = JdbcType.VARCHAR)
private String location;
@ApiModelProperty(value = "详细地址")
private String detailAddress;
@ApiModelProperty(value = "地理位置经度")
private String lng;
@ApiModelProperty(value = "地理位置维度")
private String lat;
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date createTime;

View File

@ -0,0 +1,11 @@
package com.kakarote.crm.service;
import com.kakarote.core.servlet.BaseService;
public interface IBatchEncryptionService {
/**
* 批量加密客户数据
* @param pageSize 每页处理数量
*/
void batchEncryptCustomerData(int pageSize);
}

View File

@ -0,0 +1,87 @@
package com.kakarote.crm.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.kakarote.core.common.Const;
import com.kakarote.core.security.converter.SensitiveDataConverter;
import com.kakarote.core.security.EncryptionService;
import com.kakarote.crm.entity.PO.CrmCustomer;
import com.kakarote.crm.mapper.CrmCustomerMapper;
import com.kakarote.crm.service.IBatchEncryptionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class BatchEncryptionServiceImpl implements IBatchEncryptionService {
@Autowired
private CrmCustomerMapper customerMapper;
@Autowired
private EncryptionService encryptionService;
// 我们可以移除不需要的SensitiveDataConverter依赖
// @Autowired
// private SensitiveDataConverter sensitiveDataConverter;
/**
* 批量加密客户数据
* @param pageSize 每页处理数量
*/
@Override
@Transactional
public void batchEncryptCustomerData(int pageSize) {
int currentPage = 1;
boolean hasMoreData = true;
while (hasMoreData) {
// 分页查询客户数据
IPage<CrmCustomer> page = new Page<>(currentPage, pageSize);
LambdaQueryWrapper<CrmCustomer> wrapper = new LambdaQueryWrapper<>();
// 可以添加条件来过滤未加密的数据
// wrapper.isNull(CrmCustomer::getEncryptedFlag);
IPage<CrmCustomer> result = customerMapper.selectPage(page, wrapper);
List<CrmCustomer> customerList = result.getRecords();
if (customerList.isEmpty()) {
hasMoreData = false;
break;
}
// 加密每个客户的敏感数据
for (CrmCustomer customer : customerList) {
// 加密敏感字段
if (customer.getMobile() != null) {
customer.setMobile(encryptionService.encryptAes(customer.getMobile()));
}
if (customer.getTelephone() != null) {
customer.setTelephone(encryptionService.encryptAes(customer.getTelephone()));
}
if (customer.getEmail() != null) {
customer.setEmail(encryptionService.encryptAes(customer.getEmail()));
}
if (customer.getAddress() != null) {
customer.setAddress(encryptionService.encryptAes(customer.getAddress()));
}
if (customer.getDetailAddress() != null) {
customer.setDetailAddress(encryptionService.encryptAes(customer.getDetailAddress()));
}
if (customer.getLocation() != null) {
customer.setLocation(encryptionService.encryptAes(customer.getLocation()));
}
// 标记为已加密
// customer.setEncryptedFlag(1);
// 更新客户数据
customerMapper.updateById(customer);
}
currentPage++;
}
}
}

View File

@ -32,6 +32,7 @@ import com.kakarote.core.feign.crm.entity.QueryEventCrmPageBO;
import com.kakarote.core.feign.crm.entity.SimpleCrmEntity;
import com.kakarote.core.field.FieldService;
import com.kakarote.core.redis.Redis;
import com.kakarote.core.security.EncryptionService;
import com.kakarote.core.servlet.ApplicationContextHolder;
import com.kakarote.core.servlet.BaseServiceImpl;
import com.kakarote.core.servlet.upload.FileEntity;
@ -127,6 +128,8 @@ public class CrmCustomerServiceImpl extends BaseServiceImpl<CrmCustomerMapper, C
@Autowired
private FieldService fieldService;
@Autowired
private EncryptionService encryptionService;
/**
* 查询字段配置
*
@ -331,6 +334,8 @@ public class CrmCustomerServiceImpl extends BaseServiceImpl<CrmCustomerMapper, C
CrmModel crmModel;
if (id != null) {
crmModel = getBaseMapper().queryById(id, UserUtil.getUserId());
// 添加解密逻辑
decryptSensitiveData(crmModel);
crmModel.setLabel(CrmEnum.CUSTOMER.getType());
crmModel.setOwnerUserName(UserCacheUtil.getUserName(crmModel.getOwnerUserId()));
crmCustomerDataService.setDataByBatchId(crmModel);
@ -369,6 +374,17 @@ public class CrmCustomerServiceImpl extends BaseServiceImpl<CrmCustomerMapper, C
return crmModel;
}
private void decryptSensitiveData(CrmModel model) {
String[] sensitiveFields = {"mobile", "email", "idCard", "bankCard"};
for (String field : sensitiveFields) {
Object value = model.get(field);
if (value instanceof String) {
model.put(field, encryptionService.decryptAes((String) value));
System.out.println(encryptionService.decryptAes((String) value));
}
}
}
/**
* 保存或新增信息
*

View File

@ -26,7 +26,6 @@ import com.kakarote.core.feign.admin.entity.SimpleUser;
import com.kakarote.core.feign.admin.service.AdminService;
import com.kakarote.core.feign.crm.entity.BiAuthority;
import com.kakarote.core.feign.crm.entity.BiParams;
import com.kakarote.core.feign.crm.service.CrmUserAnalyseService;
import com.kakarote.core.servlet.ApplicationContextHolder;
import com.kakarote.core.utils.BiTimeUtil;
import com.kakarote.core.utils.UserCacheUtil;

View File

@ -4,7 +4,7 @@ import cn.hutool.core.date.DateUnit;
import cn.hutool.core.lang.UUID;
import com.aliyun.oss.ServiceException;
import com.ctc.wstx.util.DataUtil;
import com.foresee.platform.api.exception.PlatformApiException;
//import com.foresee.platform.api.exception.PlatformApiException;
import com.kakarote.core.exception.CrmException;
import com.kakarote.crm.util.AecUtils;
import com.kakarote.crm.util.xml.XmlUtils;
@ -80,9 +80,6 @@ public class WebServiceUtil {
}else {
return XmlUtils.xmlToJson(ss);
}
}catch (PlatformApiException ex){
log.error("响应报文body解密失败"+responseStr,ex);
throw new CrmException(500,"解密失败");
}catch (JAXBException ex){
log.error("响应报文xml格式错误"+responseStr,ex);
throw new CrmException(500,"xml格式错误");

View File

@ -1,5 +1,6 @@
package com.kakarote.crm;
import com.kakarote.core.security.EncryptionService;
import com.kakarote.crm.entity.PO.*;
import com.kakarote.crm.service.*;
import com.kakarote.crm.util.AecUtils;
@ -24,6 +25,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
@SpringBootTest
public class testQyjxfp {
@Autowired
EncryptionService encryptionService;
@Autowired
private ICrmQyjxfpService iCrmQyjxfpService;
@ -478,6 +482,13 @@ public class testQyjxfp {
System.out.println ("respJson = " + respJson);
}
@Test
public void encryptionService(){
String s = encryptionService.encryptAes("江西方欣信息技术有限公司1");
System.out.println("加密后数据:"+s);
String s1 = encryptionService.decryptAes(s);
System.out.println("解密数据:"+s1);
}
}

View File

@ -55,3 +55,7 @@ crm:
upgradeFile:
#升级文件地址
url: D:/工具/version.json
encryption:
system-key: AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
key-version: V1

View File

@ -11,7 +11,7 @@ spring:
pool:
max-active: 300
datasource:
url: jdbc:${DATASOURCE_DBTYPE:mysql}://${DATASOURCE_HOST:127.0.0.1}:${DATASOURCE_PORT:3306}/wk_crm_single?characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false&serverTimezone=Asia/Shanghai&useAffectedRows=true
url: jdbc:${DATASOURCE_DBTYPE:mysql}://${DATASOURCE_HOST:127.0.0.1}:${DATASOURCE_PORT:3307}/wk_crm_single?characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false&serverTimezone=Asia/Shanghai&useAffectedRows=true
username: ${DATASOURCE_USERNAME:devuser}
password: ${DATASOURCE_PASSWORD:ckly@9069&Uk}
elasticsearch: