mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-08 16:57:09 +08:00
refactor(encrypt): 拆分字段加密、API 加密模块
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>top.continew.starter</groupId>
|
||||
<artifactId>continew-starter-encrypt</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-starter-encrypt-field</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>ContiNew Starter 加密模块 - 字段加密</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 加密模块 - 核心模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew.starter</groupId>
|
||||
<artifactId>continew-starter-encrypt-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus(MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-extension</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.annotation;
|
||||
|
||||
import top.continew.starter.encrypt.encryptor.IEncryptor;
|
||||
import top.continew.starter.encrypt.enums.Algorithm;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 字段加密注解
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface FieldEncrypt {
|
||||
|
||||
/**
|
||||
* 加密算法
|
||||
*/
|
||||
Algorithm value() default Algorithm.DEFAULT;
|
||||
|
||||
/**
|
||||
* 加密处理器
|
||||
* <p>
|
||||
* 优先级高于加密算法
|
||||
* </p>
|
||||
*/
|
||||
Class<? extends IEncryptor> encryptor() default IEncryptor.class;
|
||||
|
||||
/**
|
||||
* 对称加密算法密钥
|
||||
*/
|
||||
String password() default "";
|
||||
|
||||
/**
|
||||
* 非对称加密算法公钥:RSA需要
|
||||
*/
|
||||
String publicKey() default "";
|
||||
|
||||
/**
|
||||
* 非对称加密算法私钥:RSA需要
|
||||
*/
|
||||
String privateKey() default "";
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.autoconfigure;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.encrypt.field.interceptor.MyBatisDecryptInterceptor;
|
||||
import top.continew.starter.encrypt.field.interceptor.MyBatisEncryptInterceptor;
|
||||
import top.continew.starter.encrypt.field.util.EncryptHelper;
|
||||
|
||||
/**
|
||||
* 字段加密自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(FieldEncryptProperties.class)
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.ENCRYPT_FIELD, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
public class FieldEncryptAutoConfiguration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FieldEncryptAutoConfiguration.class);
|
||||
private final FieldEncryptProperties properties;
|
||||
|
||||
public FieldEncryptAutoConfiguration(FieldEncryptProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* MyBatis 加密拦截器配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MyBatisEncryptInterceptor mybatisEncryptInterceptor() {
|
||||
return new MyBatisEncryptInterceptor();
|
||||
}
|
||||
|
||||
/**
|
||||
* MyBatis 解密拦截器配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(MyBatisDecryptInterceptor.class)
|
||||
public MyBatisDecryptInterceptor mybatisDecryptInterceptor() {
|
||||
return new MyBatisDecryptInterceptor();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
EncryptHelper.init(properties);
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Encrypt-Field' completed initialization.");
|
||||
}
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.encrypt.autoconfigure.PasswordEncoderProperties;
|
||||
import top.continew.starter.encrypt.enums.Algorithm;
|
||||
|
||||
/**
|
||||
* 字段加密配置属性
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@ConfigurationProperties(PropertiesConstants.ENCRYPT_FIELD)
|
||||
public class FieldEncryptProperties {
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 默认算法
|
||||
*/
|
||||
private Algorithm algorithm = Algorithm.AES;
|
||||
|
||||
/**
|
||||
* 对称加密算法密钥
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 非对称加密算法公钥
|
||||
*/
|
||||
private String publicKey;
|
||||
|
||||
/**
|
||||
* 非对称加密算法私钥
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 密码编码器配置
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private PasswordEncoderProperties passwordEncoder;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Algorithm getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(Algorithm algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public void setPrivateKey(String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public PasswordEncoderProperties getPasswordEncoder() {
|
||||
return passwordEncoder;
|
||||
}
|
||||
|
||||
public void setPasswordEncoder(PasswordEncoderProperties passwordEncoder) {
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.interceptor;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
import top.continew.starter.encrypt.field.annotation.FieldEncrypt;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 字段解密拦截器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public abstract class AbstractMyBatisInterceptor {
|
||||
|
||||
private static final Map<Class<?>, List<Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Map<String, FieldEncrypt>> ENCRYPT_PARAM_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取所有字符串类型、需要加/解密的、有值字段
|
||||
*
|
||||
* @param obj 对象
|
||||
* @return 字段列表
|
||||
*/
|
||||
protected List<Field> getEncryptFields(Object obj) {
|
||||
if (obj == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return this.getEncryptFields(obj.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字符串类型、需要加/解密的、有值字段
|
||||
*
|
||||
* @param clazz 类型对象
|
||||
* @return 字段列表
|
||||
*/
|
||||
protected List<Field> getEncryptFields(Class<?> clazz) {
|
||||
return CLASS_FIELD_CACHE.computeIfAbsent(clazz, key -> Arrays.stream(ReflectUtil.getFields(clazz))
|
||||
.filter(field -> String.class.equals(field.getType()))
|
||||
.filter(field -> field.getAnnotation(FieldEncrypt.class) != null)
|
||||
.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加密参数
|
||||
*
|
||||
* @param mappedStatement 映射语句
|
||||
* @return 获取加密参数
|
||||
*/
|
||||
protected Map<String, FieldEncrypt> getEncryptParameters(MappedStatement mappedStatement) {
|
||||
String mappedStatementId = mappedStatement.getId();
|
||||
return ENCRYPT_PARAM_CACHE.computeIfAbsent(mappedStatementId, key -> {
|
||||
Method method = this.getMethod(mappedStatementId);
|
||||
if (method == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, FieldEncrypt> encryptMap = new HashMap<>();
|
||||
Parameter[] parameters = method.getParameters();
|
||||
for (int i = 0; i < parameters.length; i++) {
|
||||
Parameter parameter = parameters[i];
|
||||
FieldEncrypt fieldEncrypt = parameter.getAnnotation(FieldEncrypt.class);
|
||||
if (fieldEncrypt == null) {
|
||||
continue;
|
||||
}
|
||||
String parameterName = this.getParameterName(parameter);
|
||||
encryptMap.put(parameterName, fieldEncrypt);
|
||||
if (String.class.equals(parameter.getType())) {
|
||||
encryptMap.put("param" + (i + 1), fieldEncrypt);
|
||||
}
|
||||
}
|
||||
return encryptMap;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取映射方法
|
||||
*
|
||||
* @param mappedStatementId 映射语句 ID
|
||||
* @return 映射方法
|
||||
*/
|
||||
private Method getMethod(String mappedStatementId) {
|
||||
String className = CharSequenceUtil.subBefore(mappedStatementId, StringConstants.DOT, true);
|
||||
String methodName = CharSequenceUtil.subAfter(mappedStatementId, StringConstants.DOT, true);
|
||||
try {
|
||||
Method[] methods = ReflectUtil.getMethods(Class.forName(className));
|
||||
return Stream.of(methods).filter(method -> method.getName().equals(methodName)).findFirst().orElse(null);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new BaseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数名称
|
||||
*
|
||||
* @param parameter 参数
|
||||
* @return 参数名称
|
||||
*/
|
||||
public String getParameterName(Parameter parameter) {
|
||||
Param param = parameter.getAnnotation(Param.class);
|
||||
return param != null ? param.value() : parameter.getName();
|
||||
}
|
||||
}
|
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.interceptor;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.apache.ibatis.executor.resultset.ResultSetHandler;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.apache.ibatis.plugin.Intercepts;
|
||||
import org.apache.ibatis.plugin.Invocation;
|
||||
import org.apache.ibatis.plugin.Signature;
|
||||
import org.apache.ibatis.type.SimpleTypeRegistry;
|
||||
import top.continew.starter.encrypt.field.annotation.FieldEncrypt;
|
||||
import top.continew.starter.encrypt.field.util.EncryptHelper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 字段解密拦截器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
|
||||
public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implements Interceptor {
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
// 获取执行结果
|
||||
Object obj = invocation.proceed();
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
// 确保目标是 ResultSetHandler
|
||||
if (!(invocation.getTarget() instanceof ResultSetHandler)) {
|
||||
return obj;
|
||||
}
|
||||
// 处理查询结果
|
||||
if (obj instanceof List<?> resultList) {
|
||||
// 处理列表结果
|
||||
this.decryptList(resultList);
|
||||
} else if (obj instanceof Map<?, ?> map) {
|
||||
// 处理Map结果
|
||||
this.decryptMap(map);
|
||||
} else {
|
||||
// 处理单个对象结果
|
||||
this.decryptObject(obj);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密列表结果
|
||||
*
|
||||
* @param resultList 结果列表
|
||||
*/
|
||||
private void decryptList(List<?> resultList) {
|
||||
if (CollUtil.isEmpty(resultList)) {
|
||||
return;
|
||||
}
|
||||
for (Object result : resultList) {
|
||||
decryptObject(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密Map结果
|
||||
*
|
||||
* @param resultMap 结果Map
|
||||
*/
|
||||
private void decryptMap(Map<?, ?> resultMap) {
|
||||
if (CollUtil.isEmpty(resultMap)) {
|
||||
return;
|
||||
}
|
||||
new HashSet<>(resultMap.values()).forEach(this::decryptObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密单个对象结果
|
||||
*
|
||||
* @param result 结果对象
|
||||
*/
|
||||
private void decryptObject(Object result) {
|
||||
if (ObjectUtil.isNull(result)) {
|
||||
return;
|
||||
}
|
||||
// String、Integer、Long 等简单类型对象无需处理
|
||||
if (SimpleTypeRegistry.isSimpleType(result.getClass())) {
|
||||
return;
|
||||
}
|
||||
// 获取所有字符串类型、需要解密的、有值字段
|
||||
List<Field> fieldList = super.getEncryptFields(result);
|
||||
if (fieldList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 解密处理
|
||||
for (Field field : fieldList) {
|
||||
Object fieldValue = ReflectUtil.getFieldValue(result, field);
|
||||
if (fieldValue == null) {
|
||||
continue;
|
||||
}
|
||||
String strValue = String.valueOf(fieldValue);
|
||||
if (CharSequenceUtil.isBlank(strValue)) {
|
||||
continue;
|
||||
}
|
||||
ReflectUtil.setFieldValue(result, field, EncryptHelper.decrypt(strValue, field
|
||||
.getAnnotation(FieldEncrypt.class)));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.interceptor;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.encrypt.field.annotation.FieldEncrypt;
|
||||
import top.continew.starter.encrypt.field.util.EncryptHelper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 字段加密拦截器
|
||||
*
|
||||
* @author Charles7c
|
||||
* @author lishuyan
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implements InnerInterceptor {
|
||||
|
||||
private static final Pattern PARAM_PAIRS_PATTERN = Pattern
|
||||
.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");
|
||||
|
||||
@Override
|
||||
public void beforeQuery(Executor executor,
|
||||
MappedStatement mappedStatement,
|
||||
Object parameterObject,
|
||||
RowBounds rowBounds,
|
||||
ResultHandler resultHandler,
|
||||
BoundSql boundSql) {
|
||||
if (parameterObject == null) {
|
||||
return;
|
||||
}
|
||||
if (parameterObject instanceof Map parameterMap) {
|
||||
this.encryptQueryParameter(parameterMap, mappedStatement);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) {
|
||||
if (parameterObject == null) {
|
||||
return;
|
||||
}
|
||||
if (parameterObject instanceof Map parameterMap) {
|
||||
// 带别名方法(使用 @Param 注解的场景)
|
||||
this.encryptMap(parameterMap, mappedStatement);
|
||||
} else {
|
||||
// 无别名方法(例如:MP insert 等方法)
|
||||
this.encryptEntity(super.getEncryptFields(parameterObject), parameterObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密查询参数(针对 Map 类型参数)
|
||||
*
|
||||
* @param parameterMap 参数
|
||||
* @param mappedStatement 映射语句
|
||||
*/
|
||||
private void encryptQueryParameter(Map<String, Object> parameterMap, MappedStatement mappedStatement) {
|
||||
Map<String, FieldEncrypt> encryptParameterMap = super.getEncryptParameters(mappedStatement);
|
||||
for (Map.Entry<String, Object> parameterEntrySet : parameterMap.entrySet()) {
|
||||
String parameterName = parameterEntrySet.getKey();
|
||||
Object parameterValue = parameterEntrySet.getValue();
|
||||
if (parameterValue == null || ClassUtil.isBasicType(parameterValue
|
||||
.getClass()) || parameterValue instanceof AbstractWrapper) {
|
||||
continue;
|
||||
}
|
||||
if (parameterValue instanceof String str) {
|
||||
FieldEncrypt fieldEncrypt = encryptParameterMap.get(parameterName);
|
||||
if (fieldEncrypt != null) {
|
||||
parameterMap.put(parameterName, this.doEncrypt(str, fieldEncrypt));
|
||||
}
|
||||
} else {
|
||||
// 实体参数
|
||||
this.encryptEntity(super.getEncryptFields(parameterValue), parameterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理实体加密
|
||||
*
|
||||
* @param fieldList 加密字段列表
|
||||
* @param entity 实体
|
||||
*/
|
||||
private void encryptEntity(List<Field> fieldList, Object entity) {
|
||||
for (Field field : fieldList) {
|
||||
Object fieldValue = ReflectUtil.getFieldValue(entity, field);
|
||||
if (fieldValue == null) {
|
||||
continue;
|
||||
}
|
||||
String strValue = String.valueOf(fieldValue);
|
||||
if (CharSequenceUtil.isBlank(strValue)) {
|
||||
continue;
|
||||
}
|
||||
ReflectUtil.setFieldValue(entity, field, EncryptHelper.encrypt(strValue, field
|
||||
.getAnnotation(FieldEncrypt.class)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密 Map 类型数据(使用 @Param 注解的场景)
|
||||
*
|
||||
* @param parameterMap 参数
|
||||
* @param mappedStatement 映射语句
|
||||
*/
|
||||
private void encryptMap(Map<String, Object> parameterMap, MappedStatement mappedStatement) {
|
||||
Object parameter;
|
||||
// 别名带有 et(针对 MP 的 updateById、update 等方法)
|
||||
if (parameterMap.containsKey(Constants.ENTITY) && (parameter = parameterMap.get(Constants.ENTITY)) != null) {
|
||||
this.encryptEntity(super.getEncryptFields(parameter), parameter);
|
||||
}
|
||||
// 别名带有 ew(针对 MP 的 UpdateWrapper、LambdaUpdateWrapper 等参数)
|
||||
if (parameterMap.containsKey(Constants.WRAPPER) && (parameter = parameterMap.get(Constants.WRAPPER)) != null) {
|
||||
this.encryptUpdateWrapper(parameter, mappedStatement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 UpdateWrapper 类型参数加密(针对 MP 的 UpdateWrapper、LambdaUpdateWrapper 等参数)
|
||||
*
|
||||
* @param parameter Wrapper 参数
|
||||
* @param mappedStatement 映射语句
|
||||
* @author cary
|
||||
* @author wangshaopeng@talkweb.com.cn(<a
|
||||
* href="https://blog.csdn.net/tianmaxingkonger/article/details/130986784">基于Mybatis-Plus拦截器实现MySQL数据加解密</a>)
|
||||
* @since 2.1.1
|
||||
*/
|
||||
private void encryptUpdateWrapper(Object parameter, MappedStatement mappedStatement) {
|
||||
if (parameter instanceof AbstractWrapper updateWrapper) {
|
||||
String sqlSet = updateWrapper.getSqlSet();
|
||||
if (CharSequenceUtil.isBlank(sqlSet)) {
|
||||
return;
|
||||
}
|
||||
// 将 name=#{ew.paramNameValuePairs.xxx},age=#{ew.paramNameValuePairs.xxx} 切出来
|
||||
String[] elArr = sqlSet.split(StringConstants.COMMA);
|
||||
Map<String, String> propMap = new HashMap<>(elArr.length);
|
||||
Arrays.stream(elArr).forEach(el -> {
|
||||
String[] elPart = el.split(StringConstants.EQUALS);
|
||||
propMap.put(elPart[0], elPart[1]);
|
||||
});
|
||||
// 获取加密字段
|
||||
Class<?> entityClass = this.getEntityClass(updateWrapper, mappedStatement);
|
||||
List<Field> encryptFieldList = super.getEncryptFields(entityClass);
|
||||
for (Field field : encryptFieldList) {
|
||||
FieldEncrypt fieldEncrypt = field.getAnnotation(FieldEncrypt.class);
|
||||
String el = propMap.get(field.getName());
|
||||
if (CharSequenceUtil.isBlank(el)) {
|
||||
continue;
|
||||
}
|
||||
Matcher matcher = PARAM_PAIRS_PATTERN.matcher(el);
|
||||
if (matcher.matches()) {
|
||||
String valueKey = matcher.group(1);
|
||||
Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
|
||||
updateWrapper.getParamNameValuePairs().put(valueKey, this.doEncrypt(value, fieldEncrypt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加密
|
||||
*
|
||||
* @param parameterValue 参数值
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
*/
|
||||
private Object doEncrypt(Object parameterValue, FieldEncrypt fieldEncrypt) {
|
||||
if (ObjectUtil.isNull(parameterValue)) {
|
||||
return null;
|
||||
}
|
||||
String strValue = String.valueOf(parameterValue);
|
||||
if (CharSequenceUtil.isBlank(strValue)) {
|
||||
return null;
|
||||
}
|
||||
return EncryptHelper.encrypt(strValue, fieldEncrypt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体类
|
||||
*
|
||||
* @param wrapper 查询或更新包装器
|
||||
* @param mappedStatement 映射语句
|
||||
* @return 实体类
|
||||
*/
|
||||
private Class<?> getEntityClass(AbstractWrapper wrapper, MappedStatement mappedStatement) {
|
||||
// 尝试从 Wrapper 中获取实体类
|
||||
Class<?> entityClass = wrapper.getEntityClass();
|
||||
if (entityClass != null) {
|
||||
return entityClass;
|
||||
}
|
||||
// 从映射语句中获取实体类
|
||||
return mappedStatement.getParameterMap().getType();
|
||||
}
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.starter.encrypt.field.util;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.encrypt.context.CryptoContext;
|
||||
import top.continew.starter.encrypt.encryptor.IEncryptor;
|
||||
import top.continew.starter.encrypt.enums.Algorithm;
|
||||
import top.continew.starter.encrypt.field.annotation.FieldEncrypt;
|
||||
import top.continew.starter.encrypt.field.autoconfigure.FieldEncryptProperties;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 加密助手
|
||||
*
|
||||
* @author lishuyan
|
||||
* @since 2.13.2
|
||||
*/
|
||||
public class EncryptHelper {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EncryptHelper.class);
|
||||
|
||||
/**
|
||||
* 默认加密配置
|
||||
*/
|
||||
private static FieldEncryptProperties defaultProperties;
|
||||
|
||||
/**
|
||||
* 加密器缓存
|
||||
*/
|
||||
private static final Map<Integer, IEncryptor> ENCRYPTOR_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private EncryptHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认配置
|
||||
*
|
||||
* @param properties 加密配置
|
||||
*/
|
||||
public static void init(FieldEncryptProperties properties) {
|
||||
defaultProperties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册加密执行者到缓存
|
||||
* <p>
|
||||
* 计算 CryptoContext 对象的hashCode作为缓存中的key,通过hashCode查询缓存中存在则直接返回,不存在则创建并缓存
|
||||
* </p>
|
||||
*
|
||||
* @param cryptoContext 加密执行者需要的相关配置参数
|
||||
* @return 加密执行者
|
||||
*/
|
||||
public static IEncryptor registerAndGetEncryptor(CryptoContext cryptoContext) {
|
||||
int key = cryptoContext.hashCode();
|
||||
return ENCRYPTOR_CACHE.computeIfAbsent(key, k -> cryptoContext.getEncryptor().equals(IEncryptor.class)
|
||||
? ReflectUtil.newInstance(cryptoContext.getAlgorithm().getEncryptor(), cryptoContext)
|
||||
: ReflectUtil.newInstance(cryptoContext.getEncryptor(), cryptoContext));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段上的 @FieldEncrypt 注解
|
||||
*
|
||||
* @param obj 对象
|
||||
* @param fieldName 字段名称
|
||||
* @return 字段上的 @FieldEncrypt 注解
|
||||
* @throws NoSuchFieldException /
|
||||
*/
|
||||
public static FieldEncrypt getFieldEncrypt(Object obj, String fieldName) throws NoSuchFieldException {
|
||||
Field field = obj.getClass().getDeclaredField(fieldName);
|
||||
return field.getAnnotation(FieldEncrypt.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密方法
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @param fieldEncrypt 待加密字段注解
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String encrypt(String value, FieldEncrypt fieldEncrypt) {
|
||||
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null) {
|
||||
return value;
|
||||
}
|
||||
String ciphertext = value;
|
||||
try {
|
||||
CryptoContext cryptoContext = buildCryptoContext(fieldEncrypt);
|
||||
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
|
||||
ciphertext = encryptor.encrypt(ciphertext);
|
||||
} catch (Exception e) {
|
||||
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密方法
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String encrypt(String value) {
|
||||
if (CharSequenceUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
String ciphertext = value;
|
||||
try {
|
||||
CryptoContext cryptoContext = buildCryptoContext();
|
||||
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
|
||||
ciphertext = encryptor.encrypt(ciphertext);
|
||||
} catch (Exception e) {
|
||||
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密方法
|
||||
*
|
||||
* @param value 待解密字符串
|
||||
* @param fieldEncrypt 待解密字段注解
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public static String decrypt(String value, FieldEncrypt fieldEncrypt) {
|
||||
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null) {
|
||||
return value;
|
||||
}
|
||||
String plaintext = value;
|
||||
try {
|
||||
CryptoContext cryptoContext = buildCryptoContext(fieldEncrypt);
|
||||
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
|
||||
plaintext = encryptor.decrypt(plaintext);
|
||||
} catch (Exception e) {
|
||||
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密方法
|
||||
*
|
||||
* @param value 待解密字符串
|
||||
* @return 解密后的字符串
|
||||
*/
|
||||
public static String decrypt(String value) {
|
||||
if (CharSequenceUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
String plaintext = value;
|
||||
try {
|
||||
CryptoContext cryptoContext = buildCryptoContext();
|
||||
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
|
||||
plaintext = encryptor.decrypt(plaintext);
|
||||
} catch (Exception e) {
|
||||
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
|
||||
}
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建加密上下文
|
||||
*
|
||||
* @param fieldEncrypt 字段加密注解
|
||||
* @return 加密上下文
|
||||
*/
|
||||
private static CryptoContext buildCryptoContext(FieldEncrypt fieldEncrypt) {
|
||||
CryptoContext cryptoContext = new CryptoContext();
|
||||
cryptoContext.setAlgorithm(fieldEncrypt.value() == Algorithm.DEFAULT
|
||||
? defaultProperties.getAlgorithm()
|
||||
: fieldEncrypt.value());
|
||||
cryptoContext.setEncryptor(fieldEncrypt.encryptor().equals(IEncryptor.class)
|
||||
? IEncryptor.class
|
||||
: fieldEncrypt.encryptor());
|
||||
cryptoContext.setPassword(fieldEncrypt.password().isEmpty()
|
||||
? defaultProperties.getPassword()
|
||||
: fieldEncrypt.password());
|
||||
cryptoContext.setPrivateKey(fieldEncrypt.privateKey().isEmpty()
|
||||
? defaultProperties.getPrivateKey()
|
||||
: fieldEncrypt.privateKey());
|
||||
cryptoContext.setPublicKey(fieldEncrypt.publicKey().isEmpty()
|
||||
? defaultProperties.getPublicKey()
|
||||
: fieldEncrypt.publicKey());
|
||||
return cryptoContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建加密上下文
|
||||
*
|
||||
* @return 加密上下文
|
||||
*/
|
||||
private static CryptoContext buildCryptoContext() {
|
||||
CryptoContext cryptoContext = new CryptoContext();
|
||||
cryptoContext.setAlgorithm(defaultProperties.getAlgorithm());
|
||||
cryptoContext.setEncryptor(IEncryptor.class);
|
||||
cryptoContext.setPassword(defaultProperties.getPassword());
|
||||
cryptoContext.setPrivateKey(defaultProperties.getPrivateKey());
|
||||
cryptoContext.setPublicKey(defaultProperties.getPublicKey());
|
||||
return cryptoContext;
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
top.continew.starter.encrypt.field.autoconfigure.FieldEncryptAutoConfiguration
|
Reference in New Issue
Block a user