数据脱敏
敏感数据在存储过程中为是否为明文, 分为两种
- 落地脱敏: 存储的都是明文, 返回之前做脱敏处理
- 不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密
落地脱敏
这里指的是数据库中存储的是明文数据, 返回给前端的时候脱敏
MyBatis插件脱敏
Mybatis插件的相关介绍
Interceptor接口
Mybatis 中使用插件, 需要实现拦截器接口org.apache.ibatis.plugin.Interceptor
java
public interface Interceptor {
// 需要实现这个方法
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
Invocation类
这个类包含了一些拦截对象的信息
java
/**
* 拦截类
*/
public class Invocation {
// 拦截的对象
private final Object target;
// 拦截target中的具体方法, 也就是说Mybatis插件的粒度是精确到方法级别的
private final Method method;
// 拦截到的参数
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
// 执行被拦截到的方法, 你可以在执行的前后做一些事情
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}
拦截签名
Mybatis插件的粒度是精确到方法级别的, 那么疑问来了, 插件如何知道轮到它工作?
签名机制解决的就是这个问题, 通过在插件接口上使用注解@Intercepts
标注来解决这个问题
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
/**
* 返回要拦截的方法签名
*
* @return 方法签名
*/
Signature[] value();
}
java
/**
* 这个注解用于标识方法签名
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 返回java类型
*
* @return java类型
*/
Class<?> type();
/**
* 返回方法名
*
* @return 方法名
*/
String method();
/**
* 返回方法参数的java类型
*
* @return 方法参数的java类型
*/
Class<?>[] args();
}
插件的作用域
Mybatis 插件能拦截哪些对象/Mybatis插件能在哪个生命周期阶段起作用?
如下
-
Executor 是SQL 执行器, 包含了组装参数, 组装结果集到返回值以及执行SQL 的过程, 粒度比较粗
update
: insert, delete, update语句query
: query语句flushStatements
: 刷新Statementcommit
: 提交事务rollback
: 回滚事务getTransaction
: 获取事务close
: 关闭事务isClosed
: 判断是否事务
-
StatementHandler 用来处理 SQL 的执行过程, 我们可以在这里重写SQL非常常用
prepare
: 预编译SQLparametersize
: 设置参数, 即是SQL的占位符进行赋值batch
: 批处理update
: insert, delete, update语句query
: query语句
-
ParameterHandler 用来处理传入SQL的参数, 我们可以重写参数的处理规则
-
getParameterObject()
: 获取参数 -
setParameters()
: 设置参数
-
-
ResultSetHandler 用于处理结果集, 我们可以重写结果集的组装规则
handleResultSets()
: 处理结果集handleCursorResultSets()
: 批量处理结果集handleOutputParameters()
: 处理存储过程的参数
MetaObject
Mybatis 提供了一个工具类org.apache.ibatis.reflection.MetaObject
。它通过反射来读取和修改对象的元信息。我们可以利用它来处理四大对象的一些属性, 这是Mybatis插件开发的一个常用工具类。
- Object getValue(String name) 根据名称获取对象的属性值, 支持OGNL表达式。
- void setValue(String name, Object value) 设置某个属性的值。
- Class<?> getSetterType(String name) 获取setter方法的入参类型。
- Class<?> getGetterType(String name) 获取getter方法的返回值类型
通常情况下, 我们会选择使用静态方法SystemMetaObject.forObject(Object object)
来实例化MetaObject
对象
java
public final class SystemMetaObject {
public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
// 这里组合一个MetaObject
public static final MetaObject NULL_META_OBJECT = MetaObject.forObject(new NullObject(), DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
private SystemMetaObject() {
// 防止静态类的实例化
// Prevent Instantiation of Static Class
}
private static class NullObject {
}
public static MetaObject forObject(Object object) {
return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
}
}
Mybatis插件脱敏
脱敏策略
java
import java.util.function.Function;
/**
* 具体策略的函数
**/
@FunctionalInterface
public interface Desensitizer extends Function<String,String> {
}
脱敏枚举
java
import cn.hutool.core.util.DesensitizedUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 脱敏策略, 枚举类, 针对不同的数据定制特定的策略
*/
@Getter
@AllArgsConstructor
public enum SensitiveStrategy {
// ------------ 枚举 start ------------
/**
* 身份证脱敏: 显示前3位, 后4位
*/
ID_CARD("identify", "身份证号", str -> DesensitizedUtil.idCardNum(str, 3, 4)),
/**
* 银行卡脱敏: 显示前4位, 后4位
*/
ACCNO("account_no", "账户号", DesensitizedUtil::bankCard),
/**
* 手机号脱敏: 显示前3位, 后4位
*/
PHONE("phone", "手机号", DesensitizedUtil::mobilePhone),
/**
* 地址脱敏: 显示前8位
*/
ADDRESS("address", "地址", str -> DesensitizedUtil.address(str, 8)),
/**
* 邮箱脱敏: 邮箱前缀仅显示第一个字母, 前缀其他隐藏
*/
EMAIL("email", "邮箱", DesensitizedUtil::email),
BANK_CARD2("bankcard", "银行卡号", str -> {
return str.trim();
}),
/**
* 银行卡: 显示前4位, 后4位
*/
BANK_CARD("bankcard", "银行卡号", DesensitizedUtil::bankCard);
// ------------ 枚举 end ------------
// ------------ 字段 start ------------
/**
* 脱敏类型
*/
private final String type;
/**
* 脱敏类型描述
*/
private final String desc;
/**
* 脱敏策略
*/
private final Desensitizer desensitizer;
// ------------ 字段 end ------------
}
脱敏注解
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
SensitiveStrategy strategy();
}
拦截签名
由于确定要在ORM之后进行拦截, 也就是Mybatis返回结果集的时候做拦截处理, 将数据脱敏, 那么拦截时机就是ResultSetHandler, 拦截的方法就是handleResultSets, 拦截签名代码如下
java
@Intercepts(@Signature(type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}))
实现Mybatis的Interceptor
下边有两个拦截器, 拦截时期有些不同, 但是都是可以的, 选择启动一个即可
ResultSetHandler#handleResultSets
java
@Slf4j
@Intercepts(
@Signature(type = ResultSetHandler.class, method = "handleResultSets",
args = {Statement.class})
)
public class SensitiveInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
log.debug("进入数据脱敏拦截器...");
if (result instanceof List) {
List<?> records = (List<?>) result;
records.forEach(this::sensitive);
return records;
} else if (result instanceof Map) {
Map<?, ?> records = (Map<?, ?>) result;
records.values().forEach(this::sensitive);
return records;
} else {
log.info("数据脱敏失败, 脱敏的数据: {}", result);
}
return result;
}
/**
* 数据脱敏
* @param source 要脱敏的数据
*/
private void sensitive(Object source) {
// 拿到返回值类型
Class<?> sourceClass = source.getClass();
// 初始化返回值类型的 MetaObject
MetaObject metaObject = SystemMetaObject.forObject(source);
// 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理
Stream.of(sourceClass.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(Sensitive.class))
.forEach(field -> doSensitive(metaObject, field));
}
/**
* @param metaObject metaObject工具类
* @param field 脱敏字段
*/
private void doSensitive(MetaObject metaObject, Field field) {
// 拿到属性名
String name = field.getName();
// 获取属性值
Object value = metaObject.getValue(name);
// 只有字符串类型才能脱敏 而且不能为null
if (String.class == metaObject.getGetterType(name) && value != null) {
String str = (String) value;
Sensitive sensitive = field.getAnnotation(Sensitive.class);
// 获取对应的脱敏策略 并进行脱敏
SensitiveStrategy type = sensitive.strategy();
Object o = type.getDesensitizer().apply(str);
// 把脱敏后的值塞回去
metaObject.setValue(name, o);
}
}
}
Executor#query
java
@Slf4j
@Component
@Intercepts({
// 拦截query
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SensitiveInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.debug("进入数据脱敏拦截器前...");
// 脱敏入库
Object result = invocation.proceed();
// 数据
Object result = invocation.proceed();
log.debug("进入数据脱敏拦截器...");
if (result instanceof List) {
List<?> records = (List<?>) result;
records.forEach(this::sensitive);
return records;
} else if (result instanceof Map) {
Map<?, ?> records = (Map<?, ?>) result;
records.values().forEach(this::sensitive);
return records;
} else {
log.info("数据脱敏失败, 脱敏的数据: {}", result);
}
return result;
}
/**
* 数据脱敏
* @param source 要脱敏的数据
*/
private void sensitive(Object source) {
// 拿到返回值类型
Class<?> sourceClass = source.getClass();
// 初始化返回值类型的 MetaObject
MetaObject metaObject = SystemMetaObject.forObject(source);
// 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理
Stream.of(sourceClass.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(Sensitive.class))
.forEach(field -> this.doSensitive(metaObject, field));
}
/**
* @param metaObject metaObject工具类
* @param field 脱敏字段
*/
private void doSensitive(MetaObject metaObject, Field field) {
// 拿到属性名
String name = field.getName();
// 获取属性值
Object value = metaObject.getValue(name);
// 只有字符串类型才能脱敏 而且不能为null
if (String.class == metaObject.getGetterType(name) && value != null) {
String str = (String) value;
Sensitive sensitive = field.getAnnotation(Sensitive.class);
// 获取对应的脱敏策略 并进行脱敏
SensitiveStrategy type = sensitive.strategy();
Object o = type.getDesensitizer().apply(str);
// 把脱敏后的值塞回去
metaObject.setValue(name, o);
}
}
}
Jackson序列化中脱敏
脱敏策略
同上
脱敏枚举
同上
ORM查询出来后需要部分逻辑处理, 如果此时脱敏了, 那么就没法处理该逻辑, 脱敏放置在JSON序列化后较为合适
自定义脱敏序列化
java
/**
* 自定义脱敏序列化
* JsonSerializer<String>: 指定String 类型
* serialize()方法用于将修改后的数据载入
*/
@Slf4j
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveStrategy strategy;
/**
* 执行脱敏序列化逻辑
*/
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
try {
SensitiveService sensitiveService = SpringUtils.getBean(SensitiveService.class);
// 开启了脱敏
if (ObjectUtil.isNotNull(sensitiveService) && sensitiveService.isSensitive()) {
// 用指定的脱敏策略脱敏
gen.writeString(this.strategy.desensitizer().apply(value));
} else {
// 不脱敏
gen.writeString(value);
}
} catch (BeansException e) {
log.error("脱敏策略未指定, 将不进行脱敏操作, 待脱敏数据为: {}", e.getMessage());
gen.writeString(value);
}
}
/**
* 获取实体类上的@Sensitive注解并根据条件初始化对应的JsonSerializer对象
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
this.strategy = annotation.strategy();
return this;
}
return prov.findValueSerializer(property.getType(), property);
}
}
Jackson相关注解和使用参考Jackson 进阶之自定义序列化器
脱敏注解
java
/**
* 自定义jackson注解, 标注在属性上
*/
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
SensitiveTypeEnum strategy();
}
@JacksonAnnotationsInside
: 将多个注解组合到一起, 这里将把上面自定义的JSON序列化和脱敏策略绑定到一起
@JsonSerialize
: 声明使用自定义的序列化方法SensitiveJsonSerializerJackSon相关注解和使用参考Jackson 进阶之自定义序列化器
使用如下
java
@Data
public class User {
/**
* 电话号码
*/
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phoneNumber;
// ......
}
Mybatis插件脱敏和Jackson序列化脱敏对比
相对于Mybatis插件脱敏, Jackson脱敏则是更加好
假设查询列中有个手机号, ORM之后需要对手机号进行一些判断, 但是手机号已经脱敏, 不足以用于判断, 那么此时就是很麻烦的
而JSON之后序列化则是解决了这个问题, ORM之后手机号还是没有脱敏的, 此时可以继续对手机号做业务逻辑判断, 而将数据返回给前端之前, Spring会默认执行JSON序列化, 而此时进行脱敏, 那么最终返回给前端的效果还是脱敏的
不落地脱敏
指的是数据库中存储的是密文数据, 相对于上述明文存储的数据, 安全性大大增强, 即是发生了拖库, 黑客获取到用户的敏感信息也是加密的, 也没法进一步损害客户利益
配置脱敏
介绍
Java解密工具类jasypt实现脱敏
该工具提供了单密钥对称加密
和非对称加密
两种脱敏方式
单密钥对称加密: 一个密钥加盐, 可以同时用作内容的加密和解密依据
非对称加密: 公钥和私钥两个密钥, 公钥加密, 私钥解密
引入依赖
引入jasypt依赖实现单密钥对称加密
xml
<!--配置文件加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
总配置
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--配置文件加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!-- druid数据源驱动 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
yaml配置
脱敏的一些配置
yaml
# 密钥对安全性要求比较高, 不建议直接显示在项目中, 可以通过启动时-D参数注入, 或者放在配置中心
# 例如password, prefix, suffix, algorithm都简易-D参数注入, 最低最低要求password要通过-D注入
# 密钥相关配置
jasypt:
encryptor:
# 秘钥配置项, 密钥不支持中文
password: whitebrocade
property:
# 前缀, 后缀
# 和要加密的元素拼接, 例如加密值为12345678, 12是前缀, 78是后缀, 3456是特有的值 那么配置了前后缀就是12345678 对拼接的字符串进行加密
prefix: "12"
suffix: "78"
# 加密算法, 默认是PBEWITHMD5ANDDES
algorithm: PBEWithMD5AndDES
例如启动程序命令如下
sh
java -jar -Djasypt.encryptor.password=whitebrocad jasypt-demo.jar
java -jar -Djasypt.encryptor.password=whitebrocad -Djasypt.encryptor.property.prefix="12" -Djasypt.encryptor.property.suffix="78" -Djasypt.encryptor.algorithm=PBEWithMD5AndDES jasypt-demo.jar
使用流程
假设现在要对MySQL的密码进行进行脱敏
yaml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# 对MySQL的密码进行加密脱敏
password: 12345678
jasypt:
encryptor:
password: whitebrocade
property:
prefix: "12"
suffix: "78"
algorithm: PBEWithMD5AndDES
首先明确的是, 12345678
是不能直接显示, 所以这里的password是一个加密值, 需要提前生成
生成方式如下
-
代码API生成
java@Autowired private StringEncryptor stringEncryptor; public void encrypt(String content) { String encryptStr = stringEncryptor.encrypt(content); System.out.println("加密后的内容: " + encryptStr); }
-
Java命令生成
shjava -cp E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="12345678" password=whitebrocade algorithm=PBEWithMD5AndDES
E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar
: 为jasypt核心jar包: 这个路径是你jasypt的在maven中保存的路径, 根据自己的存储情况而定input
: 待加密文本, 这里传入12345678
password
: 秘钥, 为whitebrocade, 秘钥随意, 需要注意秘钥的密码强度以及秘钥的保密
algorithm
: 为使用的加密算法, 建议不要用默认的加密算法, 加大破解难度
OUTPUT是加密后的密码, 注意了, 每次生成的效果都不一样, 但是都是可以解密的
将生成的密码0jSWFsiP9ZVKg3USneAl76beGfuovVlG
复制到yaml中, 如下
yaml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# 对MySQL
password: ENC(0jSWFsiP9ZVKg3USneAl76beGfuovVlG)
jasypt:
encryptor:
password: whitebrocade
property:
prefix: "12"
suffix: "78"
algorithm: PBEWithMD5AndDES
表示一个加密操作, 那么此时需要加密的内容就是prefix+phone+suffix拼接成的内容, 即ENC(prefix+phone+suffix), 这里的前缀和后缀起了一个盐值的作用
ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候
jasypt
将保持原值,不进行解密
相关测试
相关相关测试代码
java
@Controller
public class MyTestController {
@Autowired
private StringEncryptor stringEncryptor;
@Autowired
JdbcTemplate jdbcTemplate;
@ResponseBody
@RequestMapping("/test")
public void encrypt(){
String content = "12345678";
String encryptStr = stringEncryptor.encrypt(content);
System.out.println("加密后的内容:" + encryptStr);
String decryptStr = stringEncryptor.decrypt(encryptStr);
System.out.println("解密后的内容:" + decryptStr);
this.list();
}
/**
* 查询数据库信息
*/
public void list(){
// 数据库中有t1表, 并且有数据
String sql="select * from t1";
List<Map<String,Object>> list_map = jdbcTemplate.queryForList(sql);
System.out.println("list_map = " + list_map);
}
}
运行结果如下, 发现确实可以连接数据库
敏感字段脱敏
生产环境用户的隐私数据,比如手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理
AOP脱敏
入库前的脱敏, 查询时的反向解密, 一前一后适合使用AOP来实现
这里是全脱敏, 不支持模糊查询!
模糊查询可以通过分词密文映射表查询, 后续再说
自定义注解
自定义两个注解@EncryptField
、@EncryptMethod
分别用在字段属性和方法
java
@Documented
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
String[] value() default "";
}
java
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {
String type() default ENCRYPT;
}
定义常量
java
public interface EncryptConstant {
// 加密
String ENCRYPT = "encrypt";
// 解密
String DECRYPT = "decrypt";
}
切面类
java
@Slf4j
@Aspect
@Component
public class EncryptHandler {
@Autowired
private StringEncryptor stringEncryptor;
@Pointcut("@annotation(com.whitebrocade.jasyptdemo.demos.anno.EncryptMethod)")
public void pointCut() {
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
// 加密
this.encrypt(joinPoint);
// 解密
Object decrypt = this.decrypt(joinPoint);
return decrypt;
}
/**
* 加密
*/
public void encrypt(ProceedingJoinPoint joinPoint) {
try {
Object[] objects = joinPoint.getArgs();
if (objects.length != 0) {
for (Object o : objects) {
if (o instanceof String) {
this.encryptStr(o);
} else {
this.handler(o, ENCRYPT);
}
//TODO 其余类型自己看实际情况加
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 解密
*/
public Object decrypt(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
Object obj = joinPoint.proceed();
if (obj != null) {
if (obj instanceof String) {
this.decryptStr(obj);
} else {
result = this.handler(obj, DECRYPT);
}
// TODO 其余类型自己看实际情况加
}
} catch (Throwable e) {
log.error("解密失败", e);
throw new RuntimeException();
}
return result;
}
/**
* 解密或者解密
* @param obj 要加密/解密的元素
* @param type 加密/解密
* @return 加密/解密后的内容
*/
private Object handler(Object obj, String type) throws IllegalAccessException {
if (Objects.isNull(obj)) {
return null;
}
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
// 获取EncryptField标识的注解
boolean hasSecureField = field.isAnnotationPresent(EncryptField.class);
if (hasSecureField) {
field.setAccessible(true);
String realValue = (String) field.get(obj);
String value;
if (DECRYPT.equals(type)) {
value = stringEncryptor.decrypt(realValue);
} else {
value = stringEncryptor.encrypt(realValue);
}
field.set(obj, value);
}
}
return obj;
}
/**
* 字符串内容加密
* @param realValue 字符串
* @return 加密后的字符串
*/
public String encryptStr(Object realValue) {
String value = null;
try {
value = stringEncryptor.encrypt(String.valueOf(realValue));
} catch (Exception e) {
log.error("加密失败", e);
return value;
}
return value;
}
/**
* 字符串内容解密
* @param realValue 要解密的字符串
* @return 解密后的字符串
*/
public String decryptStr(Object realValue) {
String value = String.valueOf(realValue);
try {
value = stringEncryptor.decrypt(value);
} catch (Exception e) {
log.error("解密失败", e);
return value;
}
return value;
}
}
实体类
java
@Data
public class UserVo implements Serializable {
private Long userId;
@EncryptField
private String mobile;
@EncryptField
private String address;
private String age;
}
测试类
java
@RestController
public class MyTestController {
@EncryptMethod
@PostMapping(value = "/test")
@ResponseBody
public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) {
System.out.println("前端传入参数user: " + JSONUtil.toJsonStr(user));
return this.insertUser(user, name);
}
private UserVo insertUser(UserVo user, String name) {
System.out.println("加密后的数据:user" + JSONUtil.toJsonStr(user));
System.out.println("加密后的数据:name" + name);
return user;
}
}
测试
测试数据
测试结果
总结
发现前端传递的数据接受的时候就加密了, 如果需要在业务中做判断, 那么是比较麻烦的
Mybatis插件加密
- 切入时机: Mybatis设置参数时对敏感数据进行加密
- 解密时机: Mybatis返回结果集的时候
前期准备
相关SQL
mysql
CREATE TABLE student(
id VARCHAR(50) COMMENT '学生ID',
sname VARCHAR(100) COMMENT '学生姓名',
classId VARCHAR(100) COMMENT '班级ID',
birthday VARCHAR(100) COMMENT '学生生日',
email VARCHAR(100) COMMENT '学生电子邮箱'
);
INSERT INTO student(id,sname,classId,birthday,email)
VALUES(1,'tom',101,1016,'1@163.com'),(2,'jack',101,511,'2@163.com'),
(3,'lucy',101,1016,'3@163.com'),(4,'amy',103,615,'4@163.com');
pom配置
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--配置文件加密-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!-- druid数据源驱动 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
相关代码
yaml配置
yaml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# ENC中的值是可以不断替换的
password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)
jasypt:
encryptor:
password: whitebrocade
mybatis:
mapper-locations: classpath:mapper/*.xml
# application.yml
logging:
level:
com.whitebrocade.jasyptdemo.demos: debug
# -----------------
# 加密配置
whitebrocade:
crypto:
secret-key: whitebrocade1234
algorithm: AES
注解
java
import java.lang.annotation.*;
/**
* 该注解有两种使用方式
* 1 配合@SensitiveData加在类中的字段上
* 2 直接在Mapper中的方法参数上使用
**/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
java
import java.lang.annotation.*;
/**
* 该注解定义在类上
* 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解
* 这个注解要配合EncryptTransaction注解
**/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
java
import java.lang.annotation.*;
/**
* 该注解有两种使用方式
* 1 配合@SensitiveData加在类中的字段上
* 2 直接在Mapper中的方法参数上使用
**/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
实体类
java
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData;
import lombok.Data;
import java.io.Serializable;
/**
* 与数据库表结构相同
*/
@Data
@SensitiveData
public class StudentInfo implements Serializable {
private String id;
@EncryptTransaction
private String sname;
private String classId;
private String birthday;
private String email;
}
Mapper
java
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.domain.StudentInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface StudentMapper {
/**
* 根据学生ID查询学生信息
*/
StudentInfo getInfo(@EncryptTransaction String id);
/**
* 根据姓名查用户
*/
StudentInfo getInfoByName(@EncryptTransaction @Param("sname") String sname);
/**
* 插入新学生信息
*/
void insertInfo(@EncryptTransaction StudentInfo studentInfo);
/**
* 根据ID删除学生信息
*/
int deleteById(int id);
/**
* 根据id修改学生信息
*/
int updateById(@EncryptTransaction StudentInfo studentInfo);
/**
* 查询全部学生信息
*/
List<StudentInfo> selectAll();
}
Mapper的xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper">
<select id="getInfo" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
select *
from student
where id=#{id}
</select>
<select id="getInfoByName" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
select *
from student
where sname=#{sname}
</select>
<insert id="insertInfo" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
insert into student(id,sname,classId,birthday,email)
values (#{id},#{sname},#{classId},#{birthday},#{email});
</insert>
<delete id="deleteById">
delete
from student
where id=#{id}
</delete>
<update id="updateById" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
update student
set sname = #{sname},classId = #{classId},
birthday = #{birthday}, email = #{email}
where id = #{id}
</update>
<select id="selectAll" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">
select *
from student
</select>
</mapper>
加密拦截类(核心)
java
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 加密拦截
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters",
args = PreparedStatement.class),
})
public class EncryptInterceptor implements Interceptor {
@Autowired
private Encoder encoder;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
//若指定ResultSetHandler ,这里则能强转为ResultSetHandler
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对像,即 mapper 中 paramsType 的实例
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
// 取出参数
// sname -> abc pararm1 -> abc
Object parameterObject = parameterField.get(parameterHandler);
// Class<ParameterHandler> handlerClass = ParameterHandler.class;
Field mappedStatementFiled = parameterHandler.getClass().getDeclaredField("mappedStatement");
mappedStatementFiled.setAccessible(true);
MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler);
// 方法全限定类名 com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper.getInfoByName
String methodFullClassName = mappedStatement.getId();
// 获取方法所在的类对象,这里是com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper
String mapperClassName = methodFullClassName.substring(0, methodFullClassName.lastIndexOf('.'));
Class<?> mapperClass = Class.forName(mapperClassName);
// 简单方法名 getInfoByName
String methodSimpleName = methodFullClassName.substring(methodFullClassName.lastIndexOf('.') + 1);
// 通过方法名找到指定的Method
Method[] methods = mapperClass.getDeclaredMethods();
Method method = null;
for (Method m : methods) {
if (m.getName().equals(methodSimpleName)) {
method = m;
break;
}
}
// 找到@EncryptTransaction的Mapper方法
List<String> paramNames = null;
if (ObjUtil.isNotNull(method)) {
// 获取参数上的所有注解
Annotation[][] pa = method.getParameterAnnotations();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < pa.length; i++) {
for (Annotation annotation : pa[i]) {
if (paramNames == null) {
paramNames = new ArrayList<>();
}
if (annotation instanceof EncryptTransaction) {
// 如果参数有@EncryptTransaction注解,则将参数名添加到集合中
paramNames.add(parameters[i].getName());
}
// 如果有@Param注解,则将参数名添加到集合中
if (annotation instanceof Param) {
paramNames.add(parameters[i].getName());
continue;
}
}
}
}
// 外界传入参数不为空
if (ObjUtil.isNotNull(parameterObject)) {
String entityClassName = null;
// 之所以要分成几种类型,是因为查看通过返回值获取类型,增改可以传递的实体类获取类型,而删除传递为id, 返回值也不是我们所需要的
// 查询类型
if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
// 获取实体类的类名
// com.whitebrocade.jasyptdemo.demos.domain.StudentInfo
entityClassName = mappedStatement.getResultMaps().get(0).getType().getName();
} else if(mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT)
|| mappedStatement.getSqlCommandType().equals(SqlCommandType.UPDATE)) { // 增,改都是获取注解上的类型
Annotation[][] pa = method.getParameterAnnotations();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < pa.length; i++) {
for (Annotation annotation : pa[i]) {
// 只有@EncryptTransaction注解的参数,才会被加密
if (annotation instanceof EncryptTransaction) {
entityClassName = parameters[i].getType().getTypeName();
}
}
}
} else if (mappedStatement.getSqlCommandType().equals(SqlCommandType.DELETE)) { // 通常来说,都是根据id删除,并且id类型都是int, long为主
// 直接放行
return invocation.proceed();
}
Class<?> entityClass = Class.forName(entityClassName);
// 对类字段进行加密
// 校验该实例的类是否被@SensitiveData所注解
SensitiveData sensitiveData = AnnotationUtil.getAnnotation(entityClass, SensitiveData.class);
if (ObjUtil.isNotNull(sensitiveData)) {
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = entityClass.getDeclaredFields();
// 对外界参数进行加密
parameterObject = this.encrypt(declaredFields, parameterObject);
}
// 将加密后的参数代替原来的参数
if (CollUtil.isNotEmpty(paramNames)) {
// 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射
Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql");
boundSqlField.setAccessible(true);
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
// 改写的参数设置到原parameterHandler对象
parameterField.set(parameterHandler, parameterObject);
parameterHandler.setParameters(ps);
}
}
// 执行查询
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 加密
* @param declaredFields 对象的字段
* @param paramsObject Mybatis传入参数
* @return 加密后的对象
*/
private Object encrypt(Field[] declaredFields, Object paramsObject) {
// 取出所有被EncryptTransaction注解的字段
for (Field field : declaredFields) {
EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
if (!Objects.isNull(encryptTransaction)) {
field.setAccessible(true);
// 字段名
String paramName = field.getName();
Object obj = null;
Map<String, Object> map = null;
if (paramsObject instanceof String) {
// 表示只传有一个参数
obj = (String) paramsObject;
} else if (paramsObject instanceof Map) {
map = (Map<String, Object>) paramsObject;
// 获取该字段对应的参数,非空就跳过
obj = map.get(paramName);
} else { // 如果是具体的实体对象,就转换成map
map = BeanUtil.beanToMap(paramsObject);
// 获取该字段对应的参数
obj = map.get(paramName);
}
// 为空跳过
if (Objects.isNull(obj)) {
continue;
}
// 字段类型
Class<?> paramClass = field.getType();
// 暂时只实现String类型的加密
// 如果字段类型是字符串,且传入参数是类型, 那么就转换成字符串
if (paramClass == String.class && obj instanceof String) {
String value = (String) obj;
//加密
try {
// 加密
String encryptStr = encoder.encrypt(value);
if (paramsObject instanceof String) {
paramsObject = encryptStr;
return encryptStr;
} else if (paramsObject instanceof Map) {
map.put(paramName, encryptStr);
} else { // 实体类对象
map.put(paramName, encryptStr);
paramsObject = BeanUtil.toBean(map, paramsObject.getClass());
}
} catch (Exception e) {
log.error("加密错误", e);
throw new RuntimeException("加密错误", e);
}
}
}
}
return paramsObject;
}
}
解密拦截(核心)
java
import cn.hutool.core.util.ObjUtil;
import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction;
import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
/**
* 解密拦截
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryInterceptor implements Interceptor {
@Autowired
private Encoder encoder;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 取出查询的结果
Object resultObject = invocation.proceed();
if (Objects.isNull(resultObject)) {
return null;
}
// 基于selectList
if (resultObject instanceof ArrayList) {
@SuppressWarnings("unchecked")
ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject;
if (! CollectionUtils.isEmpty(resultList) && this.needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
//逐一解密
this.decrypt(result);
}
}
// 基于selectOne
} else {
if (this.needToDecrypt(resultObject)) {
this.decrypt(resultObject);
}
}
return resultObject;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 是否需要加密,通过判断实体类是否添加@SensitiveData注解
* @param object 实体类
* @return 有添加@SensitiveData注解返回true, 没有返回false
*/
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class);
return ObjUtil.isNotNull(sensitiveData);
}
/**
* 解密
* @param result 要解密的对象
* @return 解密后的对象
* @param <T> 对象的类型
* @throws IllegalAccessException
*/
private <T> T decrypt(T result) throws IllegalAccessException {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
// 取出所有被EncryptTransaction注解的字段
EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class);
if (!Objects.isNull(encryptTransaction)) {
field.setAccessible(true);
Object object = field.get(result);
// String的解密
if (object instanceof String) {
String value = (String) object;
// 对注解的字段进行逐一解密
try {
String decryptStr = encoder.decrypt(value);
field.set(result, decryptStr);
} catch (Exception e) {
log.error("解密失败", e);
throw new RuntimeException("解密失败");
}
}
}
}
return result;
}
}
加密/解密辅助类
java
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 脱敏加密/解密
* 加密模式为ECB, 所以不支持加盐
*/
@Data
@Slf4j
@Component
public class Encoder {
/**
* 密钥建议就是从参数中读取
*/
@Value("${whitebrocade.crypto.secret-key}")
private Object secretKey;
/**
* 对称加密的算法
*/
@Value("${whitebrocade.crypto.algorithm}")
private Object algorithm;
/**
* 缓存
*/
private SymmetricCrypto crypto;
/**
* 获取SymmetricCrypto
*/
private SymmetricCrypto getSymmetricCrypto() {
if (ObjUtil.isNotNull(crypto)) {
return crypto;
}
this.initSymmetricCrypto();
return crypto;
}
/**
* 初始化SymmetricCrypto
*/
private void initSymmetricCrypto() {
// 如果KEY的长度不为16, 24, 32那么提示错误
// 密钥要求程度就如此,遵守它即可,不用多想
String tempSecretKey = String.valueOf(secretKey);
if (! (tempSecretKey.length() == 16 || tempSecretKey.length() == 24 || tempSecretKey.length() == 32)) {
throw new RuntimeException("secret-key字符串的长度必须为16,24,32长度");
}
// 获取加密算法
String tempAlgorithm = String.valueOf(algorithm);
SymmetricAlgorithm symmetricAlgorithm = SymmetricAlgorithm.valueOf(tempAlgorithm);
if (ObjUtil.isNull(symmetricAlgorithm)) {
throw new RuntimeException("symmetricAlgorithm算法不存在,算法名区分大小写,请参考cn.hutool.crypto.symmetric.SymmetricAlgorithm中算法进行配置");
}
// AES加密
byte[] bytes = SecureUtil
.generateKey(symmetricAlgorithm.getValue(), tempSecretKey.getBytes())
.getEncoded();
// 构建
crypto = new SymmetricCrypto(symmetricAlgorithm, bytes);
}
/**
* 加密
*/
public String encrypt(String content) {
SymmetricCrypto crypto = this.getSymmetricCrypto();
String encryptStr = crypto.encryptBase64(content);
return encryptStr;
}
/**
* 解密
*/
public String decrypt(String content) {
SymmetricCrypto crypto = this.getSymmetricCrypto();
String decryptStr = crypto.decryptStr(content);
return decryptStr;
}
}
Controller
java
@RestController
public class MyTestController {
@Autowired
private StudentMapper studentMapper;
@ResponseBody
@RequestMapping("/getInfo")
public void getInfo(@Param("id") String id) {
StudentInfo stu = studentMapper.getInfo(id);
System.out.println("stu = " + stu);
}
// http://localhost:8080/test5?sname=tom
@ResponseBody
@RequestMapping("/getInfoByName")
public StudentInfo getInfoByName(@Param("sname") String sname) {
StudentInfo stu = studentMapper.getInfoByName(sname);
System.out.println("stu = " + stu);
return stu;
}
@ResponseBody
@PostMapping("/insertInfo")
public StudentInfo insertInfo(@RequestBody StudentInfo studentInfo) {
studentMapper.insertInfo(studentInfo);
return studentInfo;
}
@ResponseBody
@PostMapping("/updateById")
public StudentInfo updateById(@RequestBody StudentInfo studentInfo) {
studentMapper.updateById(studentInfo);
return studentInfo;
}
@ResponseBody
@GetMapping("/selectAll")
public List<StudentInfo> selectAll() {
return studentMapper.selectAll();
}
@ResponseBody
@DeleteMapping("/deleteById")
public void deleteById(int id) {
studentMapper.deleteById(id);
}
}
需要注意的是,上述代码中不要引入Mybatis-plus,还未适配
jasypt对盐值,密钥等相关进行加密
再补充一下,既然我们直接将盐值,密钥等写入yaml中不安全,那么我们就可以借助之前的jasypt对这些信息进行加密,也就实现了密钥轮替,安全性提高了
- 对Myabtis加密脱敏的密钥加密
sh
java -cp E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="whitebrocade1234" password=whitebrocade algorithm=PBEWithMD5AndDES
对Myabtis加密脱敏所使用的算法进行加密
sh
java -cp E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="AES" password=whitebrocade algorithm=PBEWithMD5AndDES
修改后的yaml配置如下
yaml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
# url其实加密都不错的
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# ENC中的值是可以不断替换的
password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)
mybatis:
mapper-locations: classpath:mapper/*.xml
# application.yml
logging:
level:
com.whitebrocade.jasyptdemo.demos: debug
# -----------------
# Mybatis的脱敏加密配置
whitebrocade:
crypto:
secret-key: ENC(pKsZAaYDoBw2UaTS4/1R06LFavC/qlQjgb2eM3d2dVs=)
algorithm: ENC(f8muaLy4uX7/X3mG6rOwTg==)
# 这里的password建议外部传入
jasypt:
encryptor:
password: whitebrocade
效果如下, 正常查询能显示
实际中数据库就是加密了
Sharding-JDBC脱敏
-
数据源配置:是指DataSource的配置。
-
加密器配置:是指使用什么加密策略进行加解密。目前ShardingSphere内置了两种加解密策略:AES/MD5。用户还可以通过实现ShardingSphere提供的接口,自行实现一套加解密算法
- 后续我们实现ShardingSphere提供的接口, 通过SPI机制专配,SPI相关介绍见-->JDK和Spring的SPI机制原理分析
-
脱敏表配置:用于告诉ShardingSphere数据表里哪个列用于存储密文数据(cipherColumn)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)
-
查询属性的配置:当底层数据库表里同时存储了明文数据、密文数据后,该属性开关用于决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据通过Encrypt-JDBC解密后返回。
新增SPI配置
-
新增
resources/META-INF/services
目录下 -
该目录下新增配置,配置文件名为
org.apache.shardingsphere.encrypt.strategy.spi.Encryptor
-
配置文件里的内容,放入自定义的加密策略的类的全路径,和要使用官方内置的加密策略的类的全路径
- 内置的加密策略为:
AESEncryptor
和MD5Encryptor
- 自定义加密策略为:
CustomEncryptor
propertiesorg.apache.shardingsphere.encrypt.strategy.impl.AESEncryptor org.apache.shardingsphere.encrypt.strategy.impl.MD5Encryptor com.whitebrocade.jasyptdemo.demos.encryptor.CustomEncryptor com.whitebrocade.jasyptdemo.demos.encryptor.CustomQueryAssistedEncryptor
- 内置的加密策略为:
相关SQ
mysql
CREATE TABLE `t_user` (
`user_id` int NOT NULL COMMENT '用户Encoder {id',
`user_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名称',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码明文',
`password_encrypt` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码密文',
`password_assisted` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '辅助查询列',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;Encoder {
实体类
java
@Data
public class UserEntity {
private Integer userId;
private String userName;
private String password;
private String passwordEncrypt;
private String passwordAssisted;
}
Mapper
java
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper {
@Insert("insert into t_user(user_id,user_name,password) values(#{userId},#{userName},#{password})")
void insertUser(UserEntity userEntity);
@Select("select * from t_user where user_name=#{userName} and password=#{password}")
@Results({
@Result(column = "user_id", property = "userId"),
@Result(column = "user_name", property = "userName"),
@Result(column = "password", property = "password"),
@Result(column = "password_assisted", property = "passwordAssisted")
})
List<UserEntity> getUserInfo(@Param("userName") String userName, @Param("password") String password);
}
yaml
yaml
spring:
# 分库分表下的脱敏
shardingsphere:
datasource:
names: demo
demo:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1.101:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# ENC中的值是可以不断替换的
password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)
encrypt:
encryptors:
my-encryptor:
# 加密算法类型
type: CustomEncryptor
# type: CustomQueryAssistedEncryptor
# 要加密的表
tables:
t_user:
columns:
password:
# 真实列
plain-column: password
# 加密列
cipher-column: password_encrypt
# 辅助查询列
# assisted-query-column: password_assisted
# 加密算法
encryptor: my-encryptor
# 查询是否使用密文列 ture显示cipher-column false显示plain-column
props:
query.with.cipher.column: true
# 加密配置
whitebrocade:
crypto:
# 密钥,16/24/32字节
secret-key: ENC(pKsZAaYDoBw2UaTS4/1R06LFavC/qlQjgb2eM3d2dVs=)
algorithm: ENC(f8muaLy4uX7/X3mG6rOwTg==)
# Mybatis XML配置
mybatis:
mapper-locations: classpath:mapper/*.xml
# application.yml
logging:
level:
com.whitebrocade.jasyptdemo.demos: debug
# 加密
jasypt:
encryptor:
password: whitebrocade
加密/解密辅助类
java
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 脱敏加密/解密
* 加密模式为ECB, 所以不支持加盐
*/
@Data
@Slf4j
@Component
pEncoder {ublic class Encoder {
/**
* 密钥建议就是从参数中读取
*/
@Value("${whitebrocade.crypto.secret-key}")
private Object secretKey;
/**
* 对称加密的算法
*/
@Value("${whitebrocade.crypto.algorithm}")
private Object algorithm;
/**
* 缓存
*/
private SymmetricCrypto crypto;
/**
* 获取SymmetricCrypto
*/
private SymmetricCrypto getSymmetricCrypto() {
if (ObjUtil.isNotNull(crypto)) {
return crypto;
}
this.initSymmetricCrypto();
return crypto;
}
/**
* 初始化SymmetricCrypto
*/
private void initSymmetricCrypto() {
// 如果KEY的长度不为16, 24, 32那么提示错误
// 密钥要求程度就如此,遵守它即可,不用多想
String tempSecretKey = String.valueOf(secretKey);
if (! (tempSecretKey.length() == 16 || tempSecretKey.length() == 24 || tempSecretKey.length() == 32)) {
throw new RuntimeException("secret-key字符串的长度必须为16,24,32长度");
}
// 获取加密算法
String tempAlgorithm = String.valueOf(algorithm);
SymmetricAlgorithm symmetricAlgorithm = SymmetricAlgorithm.valueOf(tempAlgorithm);
if (ObjUtil.isNull(symmetricAlgorithm)) {
throw new RuntimeException("symmetricAlgorithm算法不存在,算法名区分大小写,请参考cn.hutool.crypto.symmetric.SymmetricAlgorithm中算法进行配置");
}
// AES加密
byte[] bytes = SecureUtil
.generateKey(symmetricAlgorithm.getValue(), tempSecretKey.getBytes())
.getEncoded();
// 构建
crypto = new SymmetricCrypto(symmetricAlgorithm, bytes);
}
/**
* 加密
*/
public String encrypt(String content) {
SymmetricCrypto crypto = this.getSymmetricCrypto();
String encryptStr = crypto.encryptBase64(content);
return encryptStr;
}
/**
* 解密
*/
public String decrypt(String content) {
SymmetricCrypto crypto = this.getSymmetricCrypto();
String decryptStr = crypto.decryptStr(content);
return decryptStr;
}
}
自定义加密器CustomEncryptor
java
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.whitebrocade.jasyptdemo.demos.service.Encoder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.encrypt.strategy.spi.Encryptor;
import java.util.Properties;
/**
* 该种加密方式特点: 相同数据存储内容一样
*/
@Slf4j
@Getter
@Setter
public class CustomEncryptor implements Encryptor {
/**
* 加密器, 这里无法通过@Autowired注入, 通过工具类获取Bean对象进行初始化
*/
private Encoder encoder;
/**
* 算法策略类型
*/
private static final String TYPE = "CustomEncryptor";
private Properties properties = new Properties();
@Override
public void init() {
Encoder tmepEncoder = SpringUtil.getBean(Encoder.class);
if (ObjUtil.isNull(tmepEncoder)) {
log.error("Spring容器中不存在Encoder类型的Bean");
throw new RuntimeException("Spring容器中不存在Encoder类型的Bean");
}
encoder = tmepEncoder;
}
/**
* 加密
* @param plaintext 需要加密的数据
* @return 加密后的数据
*/
@Override
public String encrypt(Object plaintext) {
if (ObjUtil.isNull(plaintext)) {
return null;
}
return encoder.encrypt(String.valueOf(plaintext));
}
/**
* 解密
* @param ciphertext 需要解密的数据
* @return 解密后的数据
*/
@Override
public Object decrypt(String ciphertext) {
if (ObjUtil.isNull(ciphertext)) {
return null;
}
return encoder.decrypt(ciphertext);
}
/**
* 返回所使用的加密算法,后续配置文件中填写这个算法名
*/
@Override
public String getType() {
return TYPE;
}
@Override
public void setProperties(Properties properties) {
}
}
自定义加密器CustomQueryAssistedEncryptor
java
cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.Digester;
import cn.hutool.extra.spring.SpringUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.encrypt.strategy.spi.QueryAssistedEncryptor;
import java.util.Properties;
/**
* 该种加密方式特点: 相同数据存储会变化
*/
@Slf4j
@Getter
@Setter
public class CustomQueryAssistedEncryptor implements QueryAssistedEncryptor {
/**
* 加密器, 这里无法通过@Autowired注入, 通过工具类获取Bean对象进行初始化
*/
private Encoder encoder;
/**
* 摘要器
*/
private static final Digester digester = new Digester(DigestAlgorithm.SHA256);
/**
* 算法策略类型
*/
private static final String TYPE = "CustomQueryAssistedEncryptor";
/**
* 随机种子长度
*/
private static final int seedLength = String.valueOf(System.currentTimeMillis()).length();
private Properties properties = new Properties();
/**
* 初始化加密要用的Encoder
*/
@Override
public void init() {
// 初始化Encoder
Encoder tmepEncoder = SpringUtil.getBean(Encoder.class);
if (ObjUtil.isNull(tmepEncoder)) {
log.error("Spring容器中不存在Encoder类型的Bean");
throw new RuntimeException("Spring容器中不存在Encoder类型的Bean");
}
encoder = tmepEncoder;
}
/**
* 辅助查询列
* @param plaintext plaintext 辅助查询列对象
* @return 摘要时候的字符串
*/
@Override
public String queryAssistedEncrypt(String plaintext) {
if (ObjUtil.isNull(plaintext)) {
return null;
}
String digestHexStr = digester.digestHex(plaintext);
return digestHexStr;
}
/**
* 加密
* @param plaintext 需要加密的数据
* @return 加密后的数据
*/
@Override
public String encrypt(Object plaintext) {
if (ObjUtil.isNull(plaintext)) {
return null;
}
// 原始字符串 + 随机因子(这里采用时间戳)
plaintext = plaintext + String.valueOf(System.currentTimeMillis());
String encryptStr = encoder.encrypt(String.valueOf(plaintext));
return encryptStr;
}
/**
* 解密
* @param ciphertext 需要解密的数据
* @return 解密后的数据
*/
@Override
public Object decrypt(String ciphertext) {
if (ObjUtil.isNull(ciphertext)) {
return null;
}
String decryptStr = encoder.decrypt(ciphertext);
String rawStr = StrUtil.sub(decryptStr, 0, decryptStr.length() - seedLength);
return rawStr;
}
/**
* 返回所使用的加密算法,后续配置文件中填写这个算法名
*/
@Override
public String getType() {
return TYPE;
}
@Override
public void setProperties(Properties properties) {
}
}
测试类
java
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
@Slf4j
@SpringBootTest
class JasyptDemoApplicationTests {
@Resource
private UserMapper userMapper;
@Test
void insertUser() {
UserEntity userEntity = new UserEntity();
userEntity.setUserId(1);
userEntity.setUserName("tom");
userEntity.setPassword("123456");
userMapper.insertUser(userEntity);
}
@Test
void insertUser2() {
UserEntity userEntity = new UserEntity();
userEntity.setUserId(1);
userEntity.setUserName("tom");
userEntity.setPassword("123456");
userMapper.insertUser(userEntity);
userEntity.setUserId(2);
userMapper.insertUser(userEntity);
}
@Test
void getUserInfo() {
List<UserEntity> userEntityList = userMapper.getUserInfo("tom", "123456");
userEntityList.forEach(System.out::println);
}
}
测试CustomEncryptor
-
清空t_user表
-
修改yaml配置
- type选择 CustomEncryptor
- assisted-query-column参数注释掉
-
执行inserter()方法, 发现MySQL中新增数据
-
执行getUserInfo, 发现解密成功
测试CustomQueryAssistedEncryptor
-
清空t_user表
-
修改yaml配置
- type选择 CustomQueryAssistedEncryptor,CustomEncrypto记得注释掉
- assisted-query-column参数注释打开
-
执行inserter()2方法, 发现MySQL中新增2条数据(注意这里执行的是inster2方法), 并且即是密码都是123456,但是加密后字符串是不一样的
-
执行getUserInfo, 发现解密成功
脱敏后的模糊查询
加班加点补充中,
原理是分词密文映射表
分词密文映射表
新建一张分词密文映射表,在敏感字段数据新增、修改的后,对敏感字段进行分词组合,再对每个分词进行加密,建立起敏感字段的分词密文与目标数据行主键的关联关系;在处理模糊查询的时候,对模糊查询关键字进行加密,用加密后的模糊查询关键字,对分词密文映射表进行like查询,得到目标数据行的主键,再以目标数据行的主键为条件返回目标表进行精确查询
参考资料
数据脱敏 :: ShardingSphere (apache.org)
MyBatis 核心配置综述之 ResultSetHandler
MyBatis 核心配置综述之StatementHandler
Springboot 配置文件、隐私数据脱敏的最佳实践(原理+源码)
加密后的敏感字段还能进行模糊查询吗?该如何实现?_加密后的敏感字段还能进行模糊查询吗?该如何实现?
求求你别乱脱敏了!MyBatis 插件 + 注解轻松实现数据脱敏,So easy~! - Java技术栈
Apache ShardingSphere数据脱敏全解决方案详解(上)
ShardingSphere4.1.1:Sharding-JDBC数据加密及SPI加密策略实现
Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏