服务启动

This commit is contained in:
2025-12-05 15:34:02 +08:00
parent ab8be1a832
commit a8233ceb72
55 changed files with 2886 additions and 494 deletions

View File

@@ -30,5 +30,19 @@
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!-- MyBatis (用于类型处理器) -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Boot (用于加密工具) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,190 @@
package org.xyzh.common.utils.crypto;
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.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
/**
* AES-256-GCM 敏感数据加密工具类
* 用于加密手机号、身份证号等敏感信息
*
* @author yslg
* @since 2025-12-05
*/
@Component
public class AesEncryptUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int KEY_SIZE = 256; // AES-256
private static final int GCM_IV_LENGTH = 12; // GCM 推荐 IV 长度
private static final int GCM_TAG_LENGTH = 128; // GCM 认证标签长度
private SecretKey secretKey;
/**
* 从配置文件读取密钥,如果没有则生成新密钥
*/
public AesEncryptUtil(@Value("${security.aes.secret-key:}") String secretKeyString) {
if (secretKeyString == null || secretKeyString.isEmpty()) {
// 生产环境应该从配置中心或密钥管理系统获取
this.secretKey = generateKey();
System.err.println("警告: 未配置 AES 密钥,使用临时生成的密钥。生产环境请配置 security.aes.secret-key");
} else {
byte[] decodedKey = Base64.getDecoder().decode(secretKeyString);
this.secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, ALGORITHM);
}
}
/**
* 生成 AES-256 密钥
*/
public static SecretKey generateKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(KEY_SIZE, new SecureRandom());
return keyGen.generateKey();
} catch (Exception e) {
throw new RuntimeException("生成 AES 密钥失败", e);
}
}
/**
* 将密钥转换为 Base64 字符串(用于配置)
*/
public static String keyToString(SecretKey key) {
return Base64.getEncoder().encodeToString(key.getEncoded());
}
/**
* 加密字符串
*
* @param plaintext 明文
* @return Base64 编码的密文(包含 IV
*/
public String encrypt(String plaintext) {
if (plaintext == null || plaintext.isEmpty()) {
return plaintext;
}
try {
// 生成随机 IV
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
// 初始化加密器
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
// 加密
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// 将 IV 和密文组合在一起:[IV(12字节)][密文]
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + ciphertext.length);
byteBuffer.put(iv);
byteBuffer.put(ciphertext);
// Base64 编码
return Base64.getEncoder().encodeToString(byteBuffer.array());
} catch (Exception e) {
throw new RuntimeException("AES 加密失败", e);
}
}
/**
* 解密字符串
*
* @param ciphertext Base64 编码的密文(包含 IV
* @return 明文
*/
public String decrypt(String ciphertext) {
if (ciphertext == null || ciphertext.isEmpty()) {
return ciphertext;
}
try {
// Base64 解码
byte[] decoded = Base64.getDecoder().decode(ciphertext);
// 提取 IV 和密文
ByteBuffer byteBuffer = ByteBuffer.wrap(decoded);
byte[] iv = new byte[GCM_IV_LENGTH];
byteBuffer.get(iv);
byte[] ciphertextBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(ciphertextBytes);
// 初始化解密器
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
// 解密
byte[] plaintext = cipher.doFinal(ciphertextBytes);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES 解密失败", e);
}
}
/**
* 加密手机号保留前3后4位
* 例如13812345678 -> 138****5678
* 实际存储:加密后的完整手机号
*/
public String encryptPhone(String phone) {
return encrypt(phone);
}
/**
* 解密手机号
*/
public String decryptPhone(String encryptedPhone) {
return decrypt(encryptedPhone);
}
/**
* 加密身份证号保留前6后4位
* 例如110101199001011234 -> 110101********1234
* 实际存储:加密后的完整身份证号
*/
public String encryptIdCard(String idCard) {
return encrypt(idCard);
}
/**
* 解密身份证号
*/
public String decryptIdCard(String encryptedIdCard) {
return decrypt(encryptedIdCard);
}
/**
* 脱敏显示手机号
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
/**
* 脱敏显示身份证号
*/
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 18) {
return idCard;
}
return idCard.substring(0, 6) + "********" + idCard.substring(14);
}
}

View File

@@ -0,0 +1,76 @@
package org.xyzh.common.utils.crypto;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MyBatis 加密字段类型处理器
* 自动对数据库字段进行加密/解密
*
* 使用方式:
* 在实体类字段上添加注解:
* @TableField(typeHandler = EncryptedStringTypeHandler.class)
* private String phone;
*
* @author yslg
* @since 2025-12-05
*/
@MappedTypes({String.class})
public class EncryptedStringTypeHandler extends BaseTypeHandler<String> {
private static AesEncryptUtil aesEncryptUtil;
/**
* 设置加密工具(由 Spring 注入)
*/
public static void setAesEncryptUtil(AesEncryptUtil util) {
aesEncryptUtil = util;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
// 写入数据库时加密
if (aesEncryptUtil != null && parameter != null) {
ps.setString(i, aesEncryptUtil.encrypt(parameter));
} else {
ps.setString(i, parameter);
}
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 从数据库读取时解密
String encrypted = rs.getString(columnName);
return decrypt(encrypted);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String encrypted = rs.getString(columnIndex);
return decrypt(encrypted);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String encrypted = cs.getString(columnIndex);
return decrypt(encrypted);
}
private String decrypt(String encrypted) {
if (aesEncryptUtil != null && encrypted != null && !encrypted.isEmpty()) {
try {
return aesEncryptUtil.decrypt(encrypted);
} catch (Exception e) {
// 如果解密失败,可能是旧数据,返回原值
return encrypted;
}
}
return encrypted;
}
}

View File

@@ -0,0 +1,110 @@
package org.xyzh.common.utils.crypto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
/**
* 敏感数据处理工具类
* 支持加密存储 + 哈希查询
*
* 数据库设计:
* - phone_encrypted: 加密后的完整手机号(用于显示和解密)
* - phone_hash: 手机号的哈希值(用于查询,建立索引)
*
* @author yslg
* @since 2025-12-05
*/
@Component
public class SensitiveDataUtil {
@Autowired
private AesEncryptUtil aesEncryptUtil;
/**
* 生成 SHA-256 哈希值(用于查询索引)
*/
public String hash(String plaintext) {
if (plaintext == null || plaintext.isEmpty()) {
return null;
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hashBytes);
} catch (Exception e) {
throw new RuntimeException("哈希计算失败", e);
}
}
/**
* 处理手机号:返回加密值和哈希值
*/
public PhoneData processPhone(String phone) {
if (phone == null || phone.isEmpty()) {
return new PhoneData(null, null);
}
return new PhoneData(
aesEncryptUtil.encryptPhone(phone),
hash(phone)
);
}
/**
* 处理身份证号:返回加密值和哈希值
*/
public IdCardData processIdCard(String idCard) {
if (idCard == null || idCard.isEmpty()) {
return new IdCardData(null, null);
}
return new IdCardData(
aesEncryptUtil.encryptIdCard(idCard),
hash(idCard)
);
}
/**
* 手机号数据对象
*/
public static class PhoneData {
private String encrypted; // 加密后的值(存储在 phone_encrypted
private String hash; // 哈希值(存储在 phone_hash用于查询
public PhoneData(String encrypted, String hash) {
this.encrypted = encrypted;
this.hash = hash;
}
public String getEncrypted() {
return encrypted;
}
public String getHash() {
return hash;
}
}
/**
* 身份证号数据对象
*/
public static class IdCardData {
private String encrypted; // 加密后的值
private String hash; // 哈希值
public IdCardData(String encrypted, String hash) {
this.encrypted = encrypted;
this.hash = hash;
}
public String getEncrypted() {
return encrypted;
}
public String getHash() {
return hash;
}
}
}