知识 - Encrypt
加解密基础
本内容将介绍接口数据加解密以及数据库字段值加解密。
加密主要利用了 hutool 提供的加密方法,其中有对称加密和非对称加密两种。
依赖:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.76</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>枚举
加解密支持的算法枚举类
@Getter
@AllArgsConstructor
public enum AlgorithmType {
/**
* 默认走yml配置
*/
DEFAULT(null),
/**
* base64
*/
BASE64(Base64Encryptor.class),
/**
* aes
*/
AES(AesEncryptor.class),
/**
* rsa
*/
RSA(RsaEncryptor.class),
/**
* sm2
*/
SM2(Sm2Encryptor.class),
/**
* sm4
*/
SM4(Sm4Encryptor.class);
private final Class<? extends AbstractEncryptor> clazz;
}加解密算法
先提供 Encryptor 接口。
public interface Encryptor {
/**
* 获得当前算法
*/
AlgorithmType algorithm();
/**
* 加密
*
* @param value 待加密字符串
* @param encodeType 加密后的编码格式
* @return 加密后的字符串
*/
String encrypt(String value, EncodeType encodeType);
/**
* 解密
*
* @param value 待加密字符串
* @return 解密后的字符串
*/
String decrypt(String value);
}基础实现类
public abstract class AbstractEncryptor implements Encryptor {
public AbstractEncryptor(EncryptContext context) {
// 用户配置校验与配置注入
}
}Base64 加解密类
public class Base64Encryptor extends AbstractEncryptor {
public Base64Encryptor(EncryptContext context) {
super(context);
}
@Override
public AlgorithmType algorithm() {
return AlgorithmType.BASE64;
}
@Override
public String encrypt(String value, EncodeType encodeType) {
return EncryptHelper.encryptByBase64(value);
}
@Override
public String decrypt(String value) {
return EncryptHelper.decryptByBase64(value);
}
}AES 算法实现
public class AesEncryptor extends AbstractEncryptor {
private final EncryptContext context;
public AesEncryptor(EncryptContext context) {
super(context);
this.context = context;
}
/**
* 获得当前算法
*/
@Override
public AlgorithmType algorithm() {
return AlgorithmType.AES;
}
/**
* 加密
*
* @param value 待加密字符串
* @param encodeType 加密后的编码格式
*/
@Override
public String encrypt(String value, EncodeType encodeType) {
if (encodeType == EncodeType.HEX) {
return EncryptHelper.encryptByAesHex(value, context.getPassword());
} else {
return EncryptHelper.encryptByAes(value, context.getPassword());
}
}
/**
* 解密
*
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
return EncryptHelper.decryptByAes(value, context.getPassword());
}
}RSA 算法
public class RsaEncryptor extends AbstractEncryptor {
private final EncryptContext context;
public RsaEncryptor(EncryptContext context) {
super(context);
String privateKey = context.getPrivateKey();
String publicKey = context.getPublicKey();
if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
throw new IllegalArgumentException("RSA公私钥均需要提供,公钥加密,私钥解密。");
}
this.context = context;
}
/**
* 获得当前算法
*/
@Override
public AlgorithmType algorithm() {
return AlgorithmType.RSA;
}
/**
* 加密
*
* @param value 待加密字符串
* @param encodeType 加密后的编码格式
*/
@Override
public String encrypt(String value, EncodeType encodeType) {
if (encodeType == EncodeType.HEX) {
return EncryptHelper.encryptByRsaHex(value, context.getPublicKey());
} else {
return EncryptHelper.encryptByRsa(value, context.getPublicKey());
}
}
/**
* 解密
*
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
return EncryptHelper.decryptByRsa(value, context.getPrivateKey());
}
}SM2 算法
public class Sm2Encryptor extends AbstractEncryptor {
private final EncryptContext context;
public Sm2Encryptor(EncryptContext context) {
super(context);
String privateKey = context.getPrivateKey();
String publicKey = context.getPublicKey();
if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
throw new IllegalArgumentException("SM2公私钥均需要提供,公钥加密,私钥解密。");
}
this.context = context;
}
/**
* 获得当前算法
*/
@Override
public AlgorithmType algorithm() {
return AlgorithmType.SM2;
}
/**
* 加密
*
* @param value 待加密字符串
* @param encodeType 加密后的编码格式
*/
@Override
public String encrypt(String value, EncodeType encodeType) {
if (encodeType == EncodeType.HEX) {
return EncryptHelper.encryptBySm2Hex(value, context.getPublicKey());
} else {
return EncryptHelper.encryptBySm2(value, context.getPublicKey());
}
}
/**
* 解密
*
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
return EncryptHelper.decryptBySm2(value, context.getPrivateKey());
}
}SM4 算法
public class Sm4Encryptor extends AbstractEncryptor {
private final EncryptContext context;
public Sm4Encryptor(EncryptContext context) {
super(context);
this.context = context;
}
/**
* 获得当前算法
*/
@Override
public AlgorithmType algorithm() {
return AlgorithmType.SM4;
}
/**
* 加密
*
* @param value 待加密字符串
* @param encodeType 加密后的编码格式
*/
@Override
public String encrypt(String value, EncodeType encodeType) {
if (encodeType == EncodeType.HEX) {
return EncryptHelper.encryptBySm4Hex(value, context.getPassword());
} else {
return EncryptHelper.encryptBySm4(value, context.getPassword());
}
}
/**
* 解密
*
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
return EncryptHelper.decryptBySm4(value, context.getPassword());
}
}加解密工具类
上面封装了很多的算法类,每个算法有自己的 API,那么封装了一个加解密工具类,对外提供 API 调用每个算法的 API。
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EncryptHelper {
/**
* 公钥
*/
public static final String PUBLIC_KEY = "publicKey";
/**
* 私钥
*/
public static final String PRIVATE_KEY = "privateKey";
/**
* Base64 加密
*
* @param data 待加密数据
* @return 加密后字符串
*/
public static String encryptByBase64(String data) {
return Base64.encode(data, StandardCharsets.UTF_8);
}
/**
* Base64 解密
*
* @param data 待解密数据
* @return 解密后字符串
*/
public static String decryptByBase64(String data) {
return Base64.decodeStr(data, StandardCharsets.UTF_8);
}
/**
* AES 加密
*
* @param data 待解密数据
* @param password 秘钥字符串
* @return 加密后字符串, 采用Base64编码
*/
public static String encryptByAes(String data, String password) {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("AES 需要传入秘钥信息");
}
// aes 算法的秘钥要求是 16 位、24 位、32 位
int[] array = {16, 24, 32};
if (!ArrayUtil.contains(array, password.length())) {
throw new IllegalArgumentException("AES 秘钥长度要求为 16 位、24 位、32 位");
}
return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
}
/**
* AES 加密
*
* @param data 待解密数据
* @param password 秘钥字符串
* @return 加密后字符串, 采用Hex编码
*/
public static String encryptByAesHex(String data, String password) {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("AES 需要传入秘钥信息");
}
// aes 算法的秘钥要求是 16 位、24 位、32 位
int[] array = {16, 24, 32};
if (!ArrayUtil.contains(array, password.length())) {
throw new IllegalArgumentException("AES 秘钥长度要求为 16 位、24 位、32 位");
}
return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
}
/**
* AES 解密
*
* @param data 待解密数据
* @param password 秘钥字符串
* @return 解密后字符串
*/
public static String decryptByAes(String data, String password) {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("AES 需要传入秘钥信息");
}
// aes算法的秘钥要求是16位、24位、32位
int[] array = {16, 24, 32};
if (!ArrayUtil.contains(array, password.length())) {
throw new IllegalArgumentException("AES 秘钥长度要求为 16 位、24 位、32 位");
}
return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
}
/**
* sm4 加密
*
* @param data 待加密数据
* @param password 秘钥字符串
* @return 加密后字符串, 采用Base64编码
*/
public static String encryptBySm4(String data, String password) {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("SM4 需要传入秘钥信息");
}
// sm4 算法的秘钥要求是 16 位长度
int sm4PasswordLength = 16;
if (sm4PasswordLength != password.length()) {
throw new IllegalArgumentException("SM4 秘钥长度要求为16位");
}
return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
}
/**
* sm4 加密
*
* @param data 待加密数据
* @param password 秘钥字符串
* @return 加密后字符串, 采用Base64编码
*/
public static String encryptBySm4Hex(String data, String password) {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("SM4 需要传入秘钥信息");
}
// sm4 算法的秘钥要求是 16 位长度
int sm4PasswordLength = 16;
if (sm4PasswordLength != password.length()) {
throw new IllegalArgumentException("SM4 秘钥长度要求为 16 位");
}
return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8);
}
/**
* sm4 解密
*
* @param data 待解密数据
* @param password 秘钥字符串
* @return 解密后字符串
*/
public static String decryptBySm4(String data, String password) {
if (StrUtil.isBlank(password)) {
throw new IllegalArgumentException("SM4 需要传入秘钥信息");
}
// sm4 算法的秘钥要求是 16 位长度
int sm4PasswordLength = 16;
if (sm4PasswordLength != password.length()) {
throw new IllegalArgumentException("SM4 秘钥长度要求为 16 位");
}
return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
}
/**
* 产生sm2加解密需要的公钥和私钥
*
* @return 公私钥Map
*/
public static Map<String, String> generateSm2Key() {
Map<String, String> keyMap = new HashMap<>(2);
SM2 sm2 = SmUtil.sm2();
keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64());
keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64());
return keyMap;
}
/**
* sm2 公钥加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return 加密后字符串, 采用Base64编码
*/
public static String encryptBySm2(String data, String publicKey) {
if (StrUtil.isBlank(publicKey)) {
throw new IllegalArgumentException("SM2 需要传入公钥进行加密");
}
SM2 sm2 = SmUtil.sm2(null, publicKey);
return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
}
/**
* sm2 公钥加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return 加密后字符串, 采用Hex编码
*/
public static String encryptBySm2Hex(String data, String publicKey) {
if (StrUtil.isBlank(publicKey)) {
throw new IllegalArgumentException("SM2 需要传入公钥进行加密");
}
SM2 sm2 = SmUtil.sm2(null, publicKey);
return sm2.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
}
/**
* sm2 私钥解密
*
* @param data 待加密数据
* @param privateKey 私钥
* @return 解密后字符串
*/
public static String decryptBySm2(String data, String privateKey) {
if (StrUtil.isBlank(privateKey)) {
throw new IllegalArgumentException("SM2 需要传入私钥进行解密");
}
SM2 sm2 = SmUtil.sm2(privateKey, null);
return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
}
/**
* 产生 RSA 加解密需要的公钥和私钥
*
* @return 公私钥Map
*/
public static Map<String, String> generateRsaKey() {
Map<String, String> keyMap = new HashMap<>(2);
RSA rsa = SecureUtil.rsa();
keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64());
keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64());
return keyMap;
}
/**
* rsa 公钥加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return 加密后字符串, 采用 Base64 编码
*/
public static String encryptByRsa(String data, String publicKey) {
if (StrUtil.isBlank(publicKey)) {
throw new IllegalArgumentException("RSA 需要传入公钥进行加密");
}
RSA rsa = SecureUtil.rsa(null, publicKey);
return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
}
/**
* rsa 公钥加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return 加密后字符串, 采用 Hex 编码
*/
public static String encryptByRsaHex(String data, String publicKey) {
if (StrUtil.isBlank(publicKey)) {
throw new IllegalArgumentException("RSA 需要传入公钥进行加密");
}
RSA rsa = SecureUtil.rsa(null, publicKey);
return rsa.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey);
}
/**
* rsa 私钥解密
*
* @param data 待加密数据
* @param privateKey 私钥
* @return 解密后字符串
*/
public static String decryptByRsa(String data, String privateKey) {
if (StrUtil.isBlank(privateKey)) {
throw new IllegalArgumentException("RSA 需要传入私钥进行解密");
}
RSA rsa = SecureUtil.rsa(privateKey, null);
return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
}
/**
* md5 加密
*
* @param data 待加密数据
* @return 加密后字符串, 采用 Hex 编码
*/
public static String encryptByMd5(String data) {
return SecureUtil.md5(data);
}
/**
* sha256 加密
*
* @param data 待加密数据
* @return 加密后字符串, 采用 Hex 编码
*/
public static String encryptBySha256(String data) {
return SecureUtil.sha256(data);
}
/**
* sm3 加密
*
* @param data 待加密数据
* @return 加密后字符串, 采用 Hex 编码
*/
public static String encryptBySm3(String data) {
return SmUtil.sm3(data);
}
}接口数据加解密
一般重要的数据,在前后端交互的时候需要加解密:
- 前端提交数据到后端前,先加密再提交,后端拿到数据后,要进行解密
- 后端返回数据给前端前,先加密再提交,前端拿到数据后,要进行解密
加解密有很多算法方式,这里采用 Base64 算法、AES 算法、RSA 算法、
接口数据加解密实现步骤:
- 提供
ApiEncrypt注解,在方法上使用后代表开启接口数据加密功能,里面有两个属性:request(对请求数据解密,非 GET 请求)、response(对响应的数据加密) - 利用 Filter 过滤器来对有
ApiEncrypt注解的请求数据进行解密,响应数据进行加密 - 加密解密采用 AES 算法,密钥是随机生成的 32 字符,该密钥 Base64 加密,然后又采用 RSA 公钥加密后传输。私钥在前端持有。解密密钥时,使用私钥解密。公钥为前端持有
注解
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
/**
* 请求体数据是否进行解密,默认不解密,为 true 时解密
*/
boolean request() default false;
/**
* 响应数据是否加密后返回,默认加密,为 true 时加密
*/
boolean response() default true;
}配置项
支持 Spring Boot 的 application 配置属性
@Data
@ConfigurationProperties(prefix = "api-decrypt")
public class ApiDecryptProperties {
/**
* 加密开关
*/
private Boolean enabled;
/**
* 头部标识,告诉系统是否解密传过来的数据
*/
private String headerFlag;
/**
* 响应加密公钥
*/
private String publicKey;
/**
* 请求解密私钥
*/
private String privateKey;
}加解密缓存
项目初始化的时候,扫描有 ApiEncrypt 注解的属性,将其加解密算法等缓存起来,这样数据处理时,直接从缓存获取加解密信息。
@Data
public class EncryptContext {
/**
* 默认算法
*/
private AlgorithmType algorithm;
/**
* 安全秘钥
*/
private String password;
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
/**
* 编码方式,base64/hex
*/
private EncodeType encode;
}实现缓存的管理类,包括缓存 数据库字段值加密 的加解密信息
@Slf4j
@NoArgsConstructor
public class EncryptorManager {
/**
* 缓存加密器
*/
Map<EncryptContext, Encryptor> encryptorMap = new ConcurrentHashMap<>();
/**
* 类加密字段缓存
*/
Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();
/**
* 构造方法传入类加密字段缓存
*
* @param typeAliasesPackage 实体类包
*/
public EncryptorManager(String typeAliasesPackage) {
scanEncryptClasses(typeAliasesPackage);
}
/**
* 获取类加密字段缓存
*/
public Set<Field> getFieldCache(Class<?> sourceClazz) {
if (ObjectUtil.isNotNull(fieldCache)) {
return fieldCache.get(sourceClazz);
}
return null;
}
/**
* 注册加密执行者到缓存
*
* @param encryptContext 加密执行者需要的相关配置参数
*/
public Encryptor registAndGetEncryptor(EncryptContext encryptContext) {
if (encryptorMap.containsKey(encryptContext)) {
return encryptorMap.get(encryptContext);
}
Encryptor encryptor = ReflectUtil.newInstance(encryptContext.getAlgorithm().getClazz(), encryptContext);
encryptorMap.put(encryptContext, encryptor);
return encryptor;
}
/**
* 移除缓存中的加密执行者
*
* @param encryptContext 加密执行者需要的相关配置参数
*/
public void removeEncryptor(EncryptContext encryptContext) {
this.encryptorMap.remove(encryptContext);
}
/**
* 根据配置进行加密。会进行本地缓存对应的算法和对应的秘钥信息。
*
* @param value 待加密的值
* @param encryptContext 加密相关的配置信息
*/
public String encrypt(String value, EncryptContext encryptContext) {
Encryptor encryptor = this.registAndGetEncryptor(encryptContext);
return encryptor.encrypt(value, encryptContext.getEncode());
}
/**
* 根据配置进行解密
*
* @param value 待解密的值
* @param encryptContext 加密相关的配置信息
*/
public String decrypt(String value, EncryptContext encryptContext) {
Encryptor encryptor = this.registAndGetEncryptor(encryptContext);
return encryptor.decrypt(value);
}
/**
* 通过 typeAliasesPackage 设置的扫描包 扫描缓存实体
*/
private void scanEncryptClasses(String typeAliasesPackage) {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
String[] packagePatternArray = StringUtils.splitPreserveAllTokens(typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
try {
for (String packagePattern : packagePatternArray) {
String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
Resource[] resources = resolver.getResources(classpath + path + "/*.class");
for (Resource resource : resources) {
ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
Class<?> clazz = Resources.classForName(classMetadata.getClassName());
Set<Field> encryptFieldSet = getEncryptFieldSetFromClazz(clazz);
if (CollUtil.isNotEmpty(encryptFieldSet)) {
fieldCache.put(clazz, encryptFieldSet);
}
}
}
} catch (Exception e) {
log.error("初始化数据安全缓存时出错:{}", e.getMessage());
}
}
/**
* 获得一个类的加密字段集合
*/
private Set<Field> getEncryptFieldSetFromClazz(Class<?> clazz) {
Set<Field> fieldSet = new HashSet<>();
// 判断clazz如果是接口,内部类,匿名类就直接返回
if (clazz.isInterface() || clazz.isMemberClass() || clazz.isAnonymousClass()) {
return fieldSet;
}
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
fieldSet.addAll(Arrays.asList(fields));
clazz = clazz.getSuperclass();
}
fieldSet = fieldSet.stream().filter(field ->
field.isAnnotationPresent(EncryptField.class) && field.getType() == String.class)
.collect(Collectors.toSet());
for (Field field : fieldSet) {
field.setAccessible(true);
}
return fieldSet;
}
}过滤器
需要拦截 Request 和 Response 里的数据来进行处理,而 Spring MVC 只允许获取一次 Request 对象,如果不做如何处理,直接拦截到 Request 对象,那么项目里将无法二次使用 Request,因为 Request 的流被读取后,将无法二次读取,于是我们需要包装 Request 一层,读取流对象后,返回一个新的流对象,这个新的流对象就是原本的流对象(代理),这样将包装类返回给项目的时候,依然可以再次获取 Request 对象。Response 同理。
请求体包装类,实现解密功能
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
private final byte[] body;
/**
* 采用 AES 加密数据,加密使用的密钥由 RSA 加密而得到
*
* @param request request
* @param privateKey RSA 私钥 (用于解密 AES 秘钥)
* @param headerFlag 请求头标志
*/
public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
super(request);
// 获取 AES 密码,该密码采用 RSA 加密
String headerRsa = request.getHeader(headerFlag);
String decryptAes = EncryptHelper.decryptByRsa(headerRsa, privateKey);
// 解密 AES 密码
String aesPassword = EncryptHelper.decryptByBase64(decryptAes);
request.setCharacterEncoding("UTF-8");
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 解密 body 采用 AES 加密
String decryptBody = EncryptHelper.decryptByAes(requestBody, aesPassword);
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public int getContentLength() {
return body.length;
}
@Override
public long getContentLengthLong() {
return body.length;
}
@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public int available() {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}响应体包装类,实现加密功能
public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream byteArrayOutputStream;
private final ServletOutputStream servletOutputStream;
private final PrintWriter printWriter;
public EncryptResponseBodyWrapper(HttpServletResponse response) throws IOException {
super(response);
this.byteArrayOutputStream = new ByteArrayOutputStream();
this.servletOutputStream = this.getOutputStream();
this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
}
/**
* 获取加密内容,加密采用 AES 加密,加密用的密钥由 RSA 公钥生成
*
* @param servletResponse response
* @param publicKey RSA 公钥 (用于加密 AES 秘钥)
* @param headerFlag 请求头标志
* @return 加密内容
*/
public String getEncryptContent(HttpServletResponse servletResponse, String publicKey, String headerFlag) throws IOException {
// 生成秘钥
String aesPassword = RandomUtil.randomString(32);
// 秘钥使用 Base64 编码
String encryptAes = EncryptHelper.encryptByBase64(aesPassword);
// Rsa 公钥加密 Base64 编码
String encryptPassword = EncryptHelper.encryptByRsa(encryptAes, publicKey);
// 设置响应头
servletResponse.addHeader("Access-Control-Expose-Headers", headerFlag);
servletResponse.setHeader(headerFlag, encryptPassword);
servletResponse.setHeader("Access-Control-Allow-Origin", "*");
servletResponse.setHeader("Access-Control-Allow-Methods", "*");
servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
// 获取原始内容
String originalBody = this.getContent();
// 对内容进行加密
return EncryptHelper.encryptByAes(originalBody, aesPassword);
}
@Override
public PrintWriter getWriter() {
return printWriter;
}
@Override
public void flushBuffer() throws IOException {
if (servletOutputStream != null) {
servletOutputStream.flush();
}
if (printWriter != null) {
printWriter.flush();
}
}
@Override
public void reset() {
byteArrayOutputStream.reset();
}
public byte[] getResponseData() throws IOException {
flushBuffer();
return byteArrayOutputStream.toByteArray();
}
public String getContent() throws IOException {
flushBuffer();
return byteArrayOutputStream.toString();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
byteArrayOutputStream.write(b, off, len);
}
};
}
}过滤器实现类
是西安 Servlet 提供的 Filter 接口来是实现拦截功能。
public class CryptoFilter implements Filter {
private final ApiDecryptProperties properties;
public CryptoFilter(ApiDecryptProperties properties) {
this.properties = properties;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
// 获取加密注解
ApiEncrypt apiEncrypt = getApiEncryptAnnotation(servletRequest);
boolean requestFlag = Objects.nonNull(apiEncrypt) && apiEncrypt.request();
boolean responseFlag = Objects.nonNull(apiEncrypt) && apiEncrypt.response();
ServletRequest requestWrapper = null;
ServletResponse responseWrapper = null;
EncryptResponseBodyWrapper responseBodyWrapper = null;
// 是否执行解密 && 是否为 put 或者 post 请求
if (requestFlag && (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod()))) {
// 是否存在解密标头
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
if (StringUtil.hasText(headerValue)) {
// 请求解密
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
} else if (requestFlag) {
// 是否有注解,有就报错,没有放行
HandlerExceptionResolver exceptionResolver = SpringHelper.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
exceptionResolver.resolveException(
servletRequest, servletResponse, null,
new ServiceException(ResponseStatusEnum.REQ_REJECT.getCode(), ResponseStatusEnum.REQ_REJECT.getStatus(), "没有访问权限,请联系管理员授权"));
return;
}
}
// 判断是否响应加密
if (responseFlag) {
responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
responseWrapper = responseBodyWrapper;
}
chain.doFilter(
ObjectUtil.defaultIfNull(requestWrapper, request),
ObjectUtil.defaultIfNull(responseWrapper, response));
if (responseFlag) {
servletResponse.reset();
// 对原始内容加密
String encryptContent = responseBodyWrapper.getEncryptContent(
servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
// 对加密后的内容写出
servletResponse.getWriter().write(encryptContent);
}
}
/**
* 获取 ApiEncrypt 注解
*/
private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
RequestMappingHandlerMapping handlerMapping = SpringHelper.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
// 获取注解
try {
HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
if (Objects.nonNull(mappingHandler)) {
Object handler = mappingHandler.getHandler();
// 从 handler 获取注解
if (handler instanceof HandlerMethod handlerMethod) {
return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
@Override
public void destroy() {
}
}容器装配
最后我们将实现的过滤器功能注入到 Spring 的容器里。
@AutoConfiguration
@EnableConfigurationProperties(ApiDecryptProperties.class)
@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
public class ApiDecryptAutoConfiguration {
@Bean
public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new CryptoFilter(properties));
registration.addUrlPatterns("/*");
registration.setName("cryptoFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
}数据库字段值加解密
- 实现 EncryptField 注解,在字段使用后代表开启字段值加解密功能,具体加密规则在 EncryptField 里传入
- 实现 Mybatis insert、update 拦截器和 select 拦截器,前者对有 EncryptField 注解的字段值进行加密,后者对有 EncryptField 注解的字段值进行解密
- 内置缓存功能,在项目启动的时候,扫描实体类,将带有 EncryptField 注解的字段在 EncryptorManager 类里进行缓存,在第 2 步的拦截器中,进行获取并处理加解密
注意事项:第 3 点说明了需要扫描实体类并缓存,因此使用该功能,需要在 application 配置文件指定有 EncryptField 的实体类的包名。
注解
数据库数据加解密注解
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
/**
* 加密算法
*/
AlgorithmType algorithm() default AlgorithmType.DEFAULT;
/**
* 秘钥。AES、SM4 需要
*/
String password() default "";
/**
* 公钥。RSA、SM2 需要
*/
String publicKey() default "";
/**
* 私钥。RSA、SM2 需要
*/
String privateKey() default "";
/**
* 编码方式。对加密算法为 base64 不起作用
*/
EncodeType encode() default EncodeType.DEFAULT;
}枚举
编码类型枚举类
public enum EncodeType {
/**
* 默认使用 yml 配置
*/
DEFAULT,
/**
* base64 编码
*/
BASE64,
/**
* 16 进制编码
*/
HEX;
}配置项
支持 Spring Boot 的 application 配置属性
@Data
@ConfigurationProperties(prefix = "mybatis-encryptor")
public class EncryptorProperties {
/**
* 过滤开关
*/
private Boolean enable;
/**
* 默认算法
*/
private AlgorithmType algorithm;
/**
* 安全秘钥
*/
private String password;
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
/**
* 编码方式,base64/hex
*/
private EncodeType encode;
/**
* 加密实体类包路径
*/
private String classPackage;
}Mybatis 拦截器
Mybatis 提供了进和出数据库的拦截器 Interceptor 接口,需要自定义类实现它,这样 Mybatis 会在数据进出数据库时候调用。
MybatisDecryptInterceptor 类需要对 Insert、Update 到数据库的数据进行拦截,然后加密
@Slf4j
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
})
@AllArgsConstructor
public class MybatisDecryptInterceptor implements Interceptor {
private final EncryptorManager encryptorManager;
private final EncryptorProperties defaultProperties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行 mysql 执行结果
Object result = invocation.proceed();
if (result == null) {
return null;
}
decryptHandler(result);
return result;
}
/**
* 解密对象
*
* @param sourceObject 待加密对象
*/
private void decryptHandler(Object sourceObject) {
if (Objects.isNull(sourceObject)) {
return;
}
if (sourceObject instanceof Map<?, ?> map) {
new HashSet<>(map.values()).forEach(this::decryptHandler);
return;
}
if (sourceObject instanceof List<?> list) {
if(CollUtil.isEmpty(list)) {
return;
}
// 判断第一个元素是否含有注解。如果没有直接返回,提高效率
Object firstItem = list.get(0);
if (Objects.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
return;
}
list.forEach(this::decryptHandler);
return;
}
// 不在缓存中的类,就是没有加密注解的类(当然也有可能是typeAliasesPackage写错)
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
if(Objects.isNull(fields)){
return;
}
try {
for (Field field : fields) {
field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));
}
} catch (Exception e) {
log.error("处理解密字段时出错", e);
}
}
/**
* 字段值进行加密。通过字段的批注注册新的加密算法
*
* @param value 待加密的值
* @param field 待加密字段
* @return 加密后结果
*/
private String decryptField(String value, Field field) {
if (Objects.isNull(value)) {
return null;
}
EncryptField encryptField = field.getAnnotation(EncryptField.class);
EncryptContext encryptContext = new EncryptContext();
encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
encryptContext.setPassword(!StringUtil.hasText(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
encryptContext.setPrivateKey(!StringUtil.hasText(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
encryptContext.setPublicKey(!StringUtil.hasText(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
return this.encryptorManager.decrypt(value, encryptContext);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}MybatisEncryptInterceptor 类需要对 Select 出数据库的数据进行拦截,然后解密
@Slf4j
@Intercepts({@Signature(
type = ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class})
})
@AllArgsConstructor
public class MybatisEncryptInterceptor implements Interceptor {
private final EncryptorManager encryptorManager;
private final EncryptorProperties defaultProperties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation;
}
@Override
public Object plugin(Object target) {
if (target instanceof ParameterHandler parameterHandler) {
// 进行加密操作
Object parameterObject = parameterHandler.getParameterObject();
if (Objects.nonNull(parameterObject) && !(parameterObject instanceof String)) {
encryptHandler(parameterObject);
}
}
return target;
}
/**
* 加密对象
*
* @param sourceObject 待加密对象
*/
private void encryptHandler(Object sourceObject) {
if (Objects.isNull(sourceObject)) {
return;
}
if (sourceObject instanceof Map<?, ?> map) {
new HashSet<>(map.values()).forEach(this::encryptHandler);
return;
}
if (sourceObject instanceof List<?> list) {
if(CollUtil.isEmpty(list)) {
return;
}
// 判断第一个元素是否含有注解。如果没有直接返回,提高效率
Object firstItem = list.get(0);
if (Objects.isNull(firstItem) || CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
return;
}
list.forEach(this::encryptHandler);
return;
}
// 不在缓存中的类,就是没有加密注解的类(当然也有可能是 typeAliasesPackage 写错)
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
if(Objects.isNull(fields)){
return;
}
try {
for (Field field : fields) {
field.set(sourceObject, encryptField(Convert.toStr(field.get(sourceObject)), field));
}
} catch (Exception e) {
log.error("处理加密字段时出错", e);
}
}
/**
* 字段值进行加密。通过字段的批注注册新的加密算法
*
* @param value 待加密的值
* @param field 待加密字段
* @return 加密后结果
*/
private String encryptField(String value, Field field) {
if (Objects.isNull(value)) {
return null;
}
EncryptField encryptField = field.getAnnotation(EncryptField.class);
EncryptContext encryptContext = new EncryptContext();
encryptContext.setAlgorithm(encryptField.algorithm() == AlgorithmType.DEFAULT ? defaultProperties.getAlgorithm() : encryptField.algorithm());
encryptContext.setEncode(encryptField.encode() == EncodeType.DEFAULT ? defaultProperties.getEncode() : encryptField.encode());
encryptContext.setPassword(!StringUtil.hasText(encryptField.password()) ? defaultProperties.getPassword() : encryptField.password());
encryptContext.setPrivateKey(!StringUtil.hasText(encryptField.privateKey()) ? defaultProperties.getPrivateKey() : encryptField.privateKey());
encryptContext.setPublicKey(!StringUtil.hasText(encryptField.publicKey()) ? defaultProperties.getPublicKey() : encryptField.publicKey());
return this.encryptorManager.encrypt(value, encryptContext);
}
@Override
public void setProperties(Properties properties) {
}
}容器装配
将实现了 Mybatis 的拦截器注入到 Spring 容器里。
@AutoConfiguration(after = MybatisPlusAutoConfiguration.class)
@EnableConfigurationProperties(EncryptorProperties.class)
@ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true")
@Slf4j
@RequiredArgsConstructor
public class EncryptorAutoConfiguration {
private final EncryptorProperties properties;
@Bean
public EncryptorManager encryptorManager() {
return new EncryptorManager(properties.getClassPackage());
}
@Bean
public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorManager encryptorManager) {
return new MybatisEncryptInterceptor(encryptorManager, properties);
}
@Bean
public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) {
return new MybatisDecryptInterceptor(encryptorManager, properties);
}
}扫描容器装配类
Spring Boot 3.x 需要在 resource 下建立 META-INF/spring 路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,内容为
cn.youngkbt.encrypt.config.EncryptorAutoConfiguration
cn.youngkbt.encrypt.config.ApiDecryptAutoConfiguration这样 Spring 会自动扫描该文件的两个容器装配类,将里面涉及的类注入到 Spring 容器。
使用案例
接口数据加解密使用:
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Validated
public class AuthController {
@PostMapping("/login")
@ApiEncrypt(request = true, response = false)
public String login(@Valid @RequestBody LoginUserDTO loginUserDTO) {
return "执行登录";
}
}使用了 ApiEncrypt 注解,代表前端发生的数据需要进行解密,后端返回的数据不需要加密。
数据库字段值加解密使用:
@Data
@TableName("test_encrypt")
public class DemoEncryptPO {
@TableId
private Long id;
@EncryptField(algorithm = AlgorithmType.RSA,
privateKey = """
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCcHvPEBJOit8uM
OJcL/6LwxBGrslfVU6iL/ka7W8Ux5ifr/lJUe+e0atMFb9hYL5VbfbexdI189uEu
UCT9U9aDgPW8A8SWg7DR0mXT/+tEnnl/RdsaeFke4HTDwOZJaBP4ljE0+pJnH5Xj
qxT803qo5EtXOdjv6fyysutpsAYLPu65nwWnKIGLyFvdvHrNAsM6i2TSWqpU6AAK
6EN3Jht83T1PvJfBL7AanjhUGMSbCAzl6w5sh9IJw0SnOA1Nk1/nRqWwJSJYf8Ri
3WvuRPUq8rjwU1tpvFNndlNtrtZeheCxBTAqpdq3RIHMkYHb/A7rfhinEqAJ+J6m
atYJ/tdFAgMBAAECggEAf6iboVwwsKxjh6w6brhBL2jUHjZ9tdVri9WHVurKi2vs
lv9lqOmMZK25EcgL+sgl9CMPks6AZ3+kT+9+35qvXOaViYD3PjA+5MOLOlgYtAp1
xMmFSNbB4Qqez/arF7lAI1QEtpJyJlrggMLDLZ1rvjC3HoMRzdPiKuk8sDYcKxKq
j1JIEULy3DYdPvrFI2FmcOsMtikP2bbKY4USStgA8r5KF7CtjJjjyjrkYwc61IIJ
haL+pFQf0Im+BEjPWoNXUK3LHNTIq8S8BirzibZmTfZCfnZzi7olvzJRc+l3XTz3
SgQb9Y/A3KOlrXd7bse+OzkuSDq9izR2x4ejayWO/QKBgQDPRTF7BBhrS6aq4Rkm
crtbYyHZ68kfyROZ3EIi+I/7nBPqnDaFYceEAIN2P6/25PPLBiEi5fI5DMnvzIT1
2PoFGSKTh6Y34VL6oFn3Fd5uHdmpLguaHu/lSN1M+Gry9E/mqh2zijZJLCKKWgY8
LX66eNnVdMftSMHFOSSli0KmHwKBgQDA00XwAls5r6/ClwxR7hDEGZ2iQ7L8UZWP
iT1aCPkKRvjjsO0TDyxmO+KJy9tMB1B49xpyPkApzErqueLyEEWYB0i3kSrTtGzM
J1MjMSPkRkgoJH4x4MW03eHPkDVa5q+oDBhVMX0yEQXWZ1iC3saRSgPlybE68Mja
FgwP2qpuGwKBgFPZ6T+cE4jsrPt6XyNXzQYWn646nj4WqbBYFAVzy0P+C2yhT8k8
GmwDjSt8bmKSkzIyQ5uLrSd5TgSOF8ghxFvlpEBM42i95kTwNBUqqrafqtuvfhAW
rfRzOtwVr6akQeLONX/ZzUZi7YJNEzKrMRadJ3scaHlNMt7n1DSIlyj/AoGABhEf
rDOGx0Pd1dOG0bUZ1fGwYgCbSxEOEZwR0BlkLIybHB7e9rCNhxHvSMKfPb8lKwkr
Tdjjj+0bllMO7urQJb5k7VGl9U9B9RJvrTXImVAUyR6M0ejuj4hDqJIy+48yi6kF
wvhxpfefJWXPBR8ZREz93mcAKoiU6Te0XXNV1W0CgYBVIsQGigYQTDiUpkL0mBHH
+10kMc/NWgOYPKJ5R8sOIC0YdDem368Udg6g0naOpyIQX9HR7knERtw9/fwWWdcs
mCP8Xuw4gHoOjjlVWPGM4Gx5f7+MPxxdwUrTf0jnnuNWuSysQ+cfGkbuyot/8Ar7
zE90AMVK2EgAPpuYtxv9gA==
""",
publicKey = """
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnB7zxASTorfLjDiXC/+i
8MQRq7JX1VOoi/5Gu1vFMeYn6/5SVHvntGrTBW/YWC+VW323sXSNfPbhLlAk/VPW
g4D1vAPEloOw0dJl0//rRJ55f0XbGnhZHuB0w8DmSWgT+JYxNPqSZx+V46sU/NN6
qORLVznY7+n8srLrabAGCz7uuZ8FpyiBi8hb3bx6zQLDOotk0lqqVOgACuhDdyYb
fN09T7yXwS+wGp44VBjEmwgM5esObIfSCcNEpzgNTZNf50alsCUiWH/EYt1r7kT1
KvK48FNbabxTZ3ZTba7WXoXgsQUwKqXat0SBzJGB2/wO634YpxKgCfiepmrWCf7X
RQIDAQAB
"""
)
private String testKey;
@EncryptField(algorithm = AlgorithmType.AES, password = "10rfylhtccpuyke5")
private String value;
}使用 EncryptField 注解,代表 value 在存入数据库的时候采用 AES 加密,读取时采用 AES 解密。其中 password 为密钥,privateKey 为私钥,publicKey wei 公钥