服务启动
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user