Java数据脱敏

数据脱敏

敏感数据在存储过程中为是否为明文, 分为两种

  • 落地脱敏: 存储的都是明文, 返回之前做脱敏处理
  • 不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密

落地脱敏

这里指的是数据库中存储的是明文数据, 返回给前端的时候脱敏

MyBatis插件脱敏

Mybatis插件的相关介绍
Interceptor接口

Mybatis中使用插件, 需要实现拦截器接口org.apache.ibatis.plugin.Interceptor

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类

这个类包含了一些拦截对象的信息

/**
* 拦截类
*/
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标注来解决这个问题

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {/*** 返回要拦截的方法签名** @return 方法签名*/Signature[] value();
}
/**
* 这个注解用于标识方法签名
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {/*** 返回java类型** @return java类型*/Class<?> type();/*** 返回方法名** @return 方法名*/String method();/*** 返回方法参数的java类型** @return 方法参数的java类型*/Class<?>[] args();
}
插件的作用域

Mybatis插件能拦截哪些对象/Mybatis插件能在哪个生命周期阶段起作用?

如下

  • ExecutorSQL执行器, 包含了组装参数, 组装结果集到返回值以及执行SQL的过程, 粒度比较粗

    • update: insert, delete, update语句
    • query: query语句
    • flushStatements: 刷新Statement
    • commit: 提交事务
    • rollback: 回滚事务
    • getTransaction: 获取事务
    • close: 关闭事务
    • isClosed: 判断是否事务
  • StatementHandler 用来处理 SQL 的执行过程, 我们可以在这里重写SQL非常常用

    • prepare: 预编译SQL
    • parametersize: 设置参数, 即是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对象

public final class SystemMetaObject {public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();// 这里组合一个MetaObjectpublic 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插件脱敏
脱敏策略
import java.util.function.Function;/*** 具体策略的函数**/
@FunctionalInterface
public interface Desensitizer extends Function<String,String> {}
脱敏枚举
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 ------------
}
脱敏注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {SensitiveStrategy strategy();
}
拦截签名

由于确定要在ORM之后进行拦截, 也就是Mybatis返回结果集的时候做拦截处理, 将数据脱敏, 那么拦截时机就是ResultSetHandler, 拦截的方法就是handleResultSets, 拦截签名代码如下

@Intercepts(@Signature(type = ResultSetHandler.class,method = "handleResultSets",args = {Statement.class}))
实现Mybatis的Interceptor

下边有两个拦截器, 拦截时期有些不同, 但是都是可以的, 选择启动一个即可

ResultSetHandler#handleResultSets

@Slf4j
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets",args = {Statement.class})
)
public class SensitiveInterceptor implements Interceptor {@Overridepublic 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();// 初始化返回值类型的 MetaObjectMetaObject 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);// 只有字符串类型才能脱敏  而且不能为nullif (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

@Slf4j
@Component
@Intercepts({// 拦截query@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SensitiveInterceptor implements Interceptor {@Overridepublic 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();// 初始化返回值类型的 MetaObjectMetaObject 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);// 只有字符串类型才能脱敏  而且不能为nullif (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序列化后较为合适

自定义脱敏序列化
/*** 自定义脱敏序列化* JsonSerializer<String>: 指定String 类型* serialize()方法用于将修改后的数据载入*/
@Slf4j
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {private SensitiveStrategy strategy;/*** 执行脱敏序列化逻辑*/@Overridepublic 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对象*/@Overridepublic 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 进阶之自定义序列化器

脱敏注解
/*** 自定义jackson注解, 标注在属性上*/
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {SensitiveTypeEnum strategy();
}

@JacksonAnnotationsInside: 将多个注解组合到一起, 这里将把上面自定义的JSON序列化和脱敏策略绑定到一起

@JsonSerialize: 声明使用自定义的序列化方法SensitiveJsonSerializer

JackSon相关注解和使用参考Jackson 进阶之自定义序列化器

使用如下
@Data
public class User {/*** 电话号码*/@Sensitive(strategy = SensitiveStrategy.PHONE)private String phoneNumber;// ......
}

Mybatis插件脱敏和Jackson序列化脱敏对比

相对于Mybatis插件脱敏, Jackson脱敏则是更加好

假设查询列中有个手机号, ORM之后需要对手机号进行一些判断, 但是手机号已经脱敏, 不足以用于判断, 那么此时就是很麻烦的

而JSON之后序列化则是解决了这个问题, ORM之后手机号还是没有脱敏的, 此时可以继续对手机号做业务逻辑判断, 而将数据返回给前端之前, Spring会默认执行JSON序列化, 而此时进行脱敏, 那么最终返回给前端的效果还是脱敏的

在这里插入图片描述

不落地脱敏

指的是数据库中存储的是密文数据, 相对于上述明文存储的数据, 安全性大大增强, 即是发生了拖库, 黑客获取到用户的敏感信息也是加密的, 也没法进一步损害客户利益

配置脱敏

介绍

Java解密工具类jasypt实现脱敏

该工具提供了单密钥对称加密非对称加密两种脱敏方式

单密钥对称加密: 一个密钥加盐, 可以同时用作内容的加密和解密依据

非对称加密: 公钥和私钥两个密钥, 公钥加密, 私钥解密

引入依赖

引入jasypt依赖实现单密钥对称加密

 <!--配置文件加密--><dependency><groupId>com.github.ulisesbocchio</groupId><artifactId>jasypt-spring-boot-starter</artifactId><version>2.1.0</version></dependency>

总配置

<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配置

脱敏的一些配置

# 密钥对安全性要求比较高, 不建议直接显示在项目中, 可以通过启动时-D参数注入, 或者放在配置中心
# 例如password, prefix, suffix, algorithm都简易-D参数注入, 最低最低要求password要通过-D注入
# 密钥相关配置
jasypt:encryptor:# 秘钥配置项, 密钥不支持中文password: whitebrocadeproperty:# 前缀, 后缀 # 和要加密的元素拼接, 例如加密值为12345678, 12是前缀, 78是后缀, 3456是特有的值 那么配置了前后缀就是12345678  对拼接的字符串进行加密prefix: "12"suffix: "78"# 加密算法, 默认是PBEWITHMD5ANDDESalgorithm: PBEWithMD5AndDES

例如启动程序命令如下

java -jar -Djasypt.encryptor.password=whitebrocad jasypt-demo.jarjava -jar -Djasypt.encryptor.password=whitebrocad -Djasypt.encryptor.property.prefix="12" -Djasypt.encryptor.property.suffix="78" -Djasypt.encryptor.algorithm=PBEWithMD5AndDES jasypt-demo.jar
使用流程

假设现在要对MySQL的密码进行进行脱敏

spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaiusername: root# 对MySQL的密码进行加密脱敏password: 12345678jasypt:encryptor:password: whitebrocadeproperty:prefix: "12"suffix: "78"algorithm: PBEWithMD5AndDES

首先明确的是, 12345678是不能直接显示, 所以这里的password是一个加密值, 需要提前生成

生成方式如下

  1. 代码API生成

    • @Autowired
      private StringEncryptor stringEncryptor;public void encrypt(String content) {String encryptStr = stringEncryptor.encrypt(content);System.out.println("加密后的内容: " + encryptStr);
      }
      
  2. Java命令生成

    • java -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中, 如下

spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaiusername: root# 对MySQLpassword: ENC(0jSWFsiP9ZVKg3USneAl76beGfuovVlG)jasypt:encryptor:password: whitebrocadeproperty:prefix: "12"suffix: "78"algorithm: PBEWithMD5AndDES

表示一个加密操作, 那么此时需要加密的内容就是prefix+phone+suffix拼接成的内容, 即ENC(prefix+phone+suffix), 这里的前缀和后缀起了一个盐值的作用

ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候jasypt将保持原值,不进行解密

相关测试

相关相关测试代码

@Controller
public class MyTestController {@Autowiredprivate StringEncryptor stringEncryptor;@AutowiredJdbcTemplate 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分别用在字段属性和方法

@Documented
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {String[] value() default "";
}
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {String type() default ENCRYPT;
}
定义常量
public interface EncryptConstant {// 加密String ENCRYPT = "encrypt";// 解密String DECRYPT = "decrypt";
}
切面类
@Slf4j
@Aspect
@Component
public class EncryptHandler {@Autowiredprivate 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;}
}
实体类
@Data
public class UserVo implements Serializable {private Long userId;@EncryptFieldprivate String mobile;@EncryptFieldprivate String address;private String age;
}
测试类
@RestController
public class MyTestController {    @EncryptMethod@PostMapping(value = "/test")@ResponseBodypublic 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
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配置
<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配置
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaiusername: root# ENC中的值是可以不断替换的password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD)jasypt:encryptor:password: whitebrocademybatis:mapper-locations: classpath:mapper/*.xml# application.yml
logging:level:com.whitebrocade.jasyptdemo.demos: debug# -----------------
# 加密配置
whitebrocade:crypto:secret-key: whitebrocade1234algorithm: AES
注解
import java.lang.annotation.*;
/*** 该注解有两种使用方式* 1 配合@SensitiveData加在类中的字段上* 2 直接在Mapper中的方法参数上使用**/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
import java.lang.annotation.*;
/*** 该注解定义在类上* 插件通过扫描类对象是否包含这个注解来决定是否继续扫描其中的字段注解* 这个注解要配合EncryptTransaction注解**/
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
import java.lang.annotation.*;
/*** 该注解有两种使用方式* 1 配合@SensitiveData加在类中的字段上* 2 直接在Mapper中的方法参数上使用**/
@Documented
@Inherited
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptTransaction {
}
实体类
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;@EncryptTransactionprivate String sname;private String classId;private String birthday;private String email;
}
Mapper
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 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 studentwhere id=#{id}</select><select id="getInfoByName" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">select *from studentwhere 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">deletefrom studentwhere id=#{id}</delete><update id="updateById" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo">update studentset 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>
加密拦截类(核心)
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 {@Autowiredprivate Encoder encoder;@Overridepublic Object intercept(Invocation invocation) throws Throwable {//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler//若指定ResultSetHandler ,这里则能强转为ResultSetHandlerParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();// 获取参数对像,即 mapper 中 paramsType 的实例Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");parameterField.setAccessible(true);// 取出参数// sname -> abc pararm1 -> abcObject 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.getInfoByNameString methodFullClassName = mappedStatement.getId();// 获取方法所在的类对象,这里是com.whitebrocade.jasyptdemo.demos.mapper.StudentMapperString mapperClassName = methodFullClassName.substring(0, methodFullClassName.lastIndexOf('.'));Class<?> mapperClass = Class.forName(mapperClassName);// 简单方法名 getInfoByNameString methodSimpleName = methodFullClassName.substring(methodFullClassName.lastIndexOf('.') + 1);// 通过方法名找到指定的MethodMethod[] 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.StudentInfoentityClassName = 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();}@Overridepublic 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 { // 如果是具体的实体对象,就转换成mapmap = 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;}
}
解密拦截(核心)
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 {@Autowiredprivate Encoder encoder;@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 取出查询的结果Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}// 基于selectListif (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;}@Overridepublic 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;}
}
加密/解密辅助类
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
@RestController
public class MyTestController {  @Autowiredprivate 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加密脱敏的密钥加密
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加密脱敏所使用的算法进行加密

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配置如下

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/Shanghaiusername: 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配置
  1. 新增resources/META-INF/services目录下

  2. 该目录下新增配置,配置文件名为org.apache.shardingsphere.encrypt.strategy.spi.Encryptor

  3. 配置文件里的内容,放入自定义的加密策略的类的全路径,和要使用官方内置的加密策略的类的全路径

    • 内置的加密策略为:AESEncryptorMD5Encryptor
    • 自定义加密策略为:CustomEncryptor
    org.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
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 {
实体类
@Data
public class UserEntity {private Integer userId;private String userName;private String password;private String passwordEncrypt;private String passwordAssisted;
}
Mapper
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
spring:# 分库分表下的脱敏shardingsphere:datasource:names: demodemo:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1.101:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaiusername: 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-columnprops: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
加密/解密辅助类
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
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();@Overridepublic 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 加密后的数据*/@Overridepublic String encrypt(Object plaintext) {if (ObjUtil.isNull(plaintext)) {return null;}return encoder.encrypt(String.valueOf(plaintext));}/*** 解密* @param ciphertext 需要解密的数据* @return 解密后的数据*/@Overridepublic Object decrypt(String ciphertext) {if (ObjUtil.isNull(ciphertext)) {return null;}return encoder.decrypt(ciphertext);}/*** 返回所使用的加密算法,后续配置文件中填写这个算法名*/@Overridepublic String getType() {return TYPE;}@Overridepublic void setProperties(Properties properties) {}
}
自定义加密器CustomQueryAssistedEncryptor
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*/@Overridepublic void init() {// 初始化EncoderEncoder 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 摘要时候的字符串*/@Overridepublic String queryAssistedEncrypt(String plaintext) {if (ObjUtil.isNull(plaintext)) {return null;}String digestHexStr = digester.digestHex(plaintext);return digestHexStr;}/*** 加密* @param plaintext 需要加密的数据* @return 加密后的数据*/@Overridepublic 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 解密后的数据*/@Overridepublic 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;}/*** 返回所使用的加密算法,后续配置文件中填写这个算法名*/@Overridepublic String getType() {return TYPE;}@Overridepublic void setProperties(Properties properties) {}
}
测试类
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 {@Resourceprivate UserMapper userMapper;@Testvoid insertUser() {UserEntity userEntity = new UserEntity();userEntity.setUserId(1);userEntity.setUserName("tom");userEntity.setPassword("123456");userMapper.insertUser(userEntity);}@Testvoid insertUser2() {UserEntity userEntity = new UserEntity();userEntity.setUserId(1);userEntity.setUserName("tom");userEntity.setPassword("123456");userMapper.insertUser(userEntity);userEntity.setUserId(2);userMapper.insertUser(userEntity);}@Testvoid getUserInfo() {List<UserEntity> userEntityList = userMapper.getUserInfo("tom", "123456");userEntityList.forEach(System.out::println);}
}
测试CustomEncryptor
  1. 清空t_user表

  2. 修改yaml配置

    • type选择 CustomEncryptor
    • assisted-query-column参数注释掉
  3. 执行inserter()方法, 发现MySQL中新增数据
    在这里插入图片描述

  4. 执行getUserInfo, 发现解密成功
    在这里插入图片描述

测试CustomQueryAssistedEncryptor
  1. 清空t_user表

  2. 修改yaml配置

    • type选择 CustomQueryAssistedEncryptor,CustomEncrypto记得注释掉
    • assisted-query-column参数注释打开
  3. 执行inserter()2方法, 发现MySQL中新增2条数据(注意这里执行的是inster2方法), 并且即是密码都是123456,但是加密后字符串是不一样的
    在这里插入图片描述

  4. 执行getUserInfo, 发现解密成功
    在这里插入图片描述

脱敏后的模糊查询

加班加点补充中,

原理是分词密文映射表

分词密文映射表

新建一张分词密文映射表,在敏感字段数据新增、修改的后,对敏感字段进行分词组合,再对每个分词进行加密,建立起敏感字段的分词密文与目标数据行主键的关联关系;在处理模糊查询的时候,对模糊查询关键字进行加密,用加密后的模糊查询关键字,对分词密文映射表进行like查询,得到目标数据行的主键,再以目标数据行的主键为条件返回目标表进行精确查询

参考资料

Jackson 进阶之自定义序列化器

自己动手编写一个Mybatis插件:mybatis脱敏插件

改造了以前写的数据脱敏插件, 更好用了

一个注解优雅的实现 接口数据脱敏-腾讯云开发者社区

数据脱敏 :: ShardingSphere (apache.org)

MyBatis 核心配置概述之 Executor

MyBatis 核心配置综述之 ResultSetHandler

MyBatis 核心配置综述之StatementHandler

大厂也在用的 6种 数据脱敏方案, 别做泄密内鬼

Springboot 配置文件、隐私数据脱敏的最佳实践(原理+源码)

加密后的敏感字段还能进行模糊查询吗?该如何实现?_加密后的敏感字段还能进行模糊查询吗?该如何实现?

淘宝密文字段检索方案

mybatis(4)—自定义拦截器(下)对象详解

求求你别乱脱敏了!MyBatis 插件 + 注解轻松实现数据脱敏,So easy~! - Java技术栈

一种使用mybatis进行脱敏的思路

Apache ShardingSphere数据脱敏全解决方案详解(上)

ShardingSphere4.1.1:Sharding-JDBC数据加密及SPI加密策略实现

【Java】YAML读写常用工具包及使用示例

使用Hutool对AES加密解密

如何使用hutool进行AES加密和解密?

浅析AES加密工作模式 EBC/CBC 模式了解及具体如何进行补位、AES加密报错java.security.InvalidAlgorithmParameterException: ECB mode cannot use IV处理

java实现对称加密—基本实现

加密的手机号,如何模糊查询?

(四)、Sharding-JDBC数据脱敏

ShardingJDBC源码阅读(十)数据脱敏实战

被问懵了,加密后的数据如何进行模糊查询?

Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏

老大一个接口加解密临时任务丢了过来,我却肝了3天,感觉可以收拾工位了

加密后的数据如何进行模糊查询

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/361748.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电阻屏和电容屏

目录 一、电阻屏 1.欧姆定律 2.电阻屏原理 &#xff08;1&#xff09;测量 X 坐标 &#xff08;2&#xff09;测量 Y 坐标 3.电阻屏数据 二、电阻屏 1.原理 2.电容屏数据 &#xff08;1&#xff09;Type A &#xff08;2&#xff09;Type B 3.电容屏的实验数据 一、…

Jira实践案例分享:小米集团如何通过API请求优化、数据治理与AI智能客服等,实现Jira系统的高效运维

日前&#xff0c;Atlassian中国合作伙伴企业日活动在上海成功举办。活动以“AI协同 创未来——如何利用人工智能提升团队协作&#xff0c;加速产品交付”为主题&#xff0c;深入探讨了AI技术在团队协作与产品交付中的创新应用与实践&#xff0c;吸引了众多业内专家、企业客户及…

文心智能体平台介绍和应用:制作你的智能体(运维小帮手)

这是我自己制作的智能体 大家可以了解一下&#xff01; 运维小帮手&#xff01;https://mbd.baidu.com/ma/s/tE19dqvr 文心智能体平台官网首页 点击跳转&#xff01;https://agents.baidu.com/ 什么是智能体平台&#xff1f; 文心智能体平台&#xff08;Wenxin Intelligen…

关于Vite+Vue+Ts WebStorm路径别名的问题

一、准备一个项目 二、在 vite.config.js 中添加 resolve: {alias: {: /src}} 三、tsconfig.app.json中添加代码 //添加代码"baseUrl": ".","paths": {"/*": ["src/*"]}把src的一个文件修改路径为开头 四、安装插件 npm i …

第 11 课:组件介绍与自定义开发

本讲主要介绍了隐语的组件标准、已有的组件能力以及进一步的自定义开发流程。经过本讲的学习&#xff0c;可以为将隐语集成到任意调度系统&#xff0c;基于Kusica/SecretPad进行二次开发&#xff0c;以及参与隐语开放标准共建建立基础。 一、隐语开放标准 隐语提出的适用于隐私…

仓库管理系统

摘 要 随着电子商务的快速发展和物流行业的蓬勃发展&#xff0c;仓库管理成为了企业重要的一环。仓库管理涉及到商品的入库、出库、库存管理等一系列操作&#xff0c;对于企业的运营效率和成本控制具有重要影响。传统的仓库管理方式往往依赖于人工操作和纸质记录&#xff0c;存…

【Windows 常用工具系列 17 -- windows bat 脚本多参数处理】

请阅读【嵌入式开发学习必备专栏】 文章目录 bat 脚本命令行参数使用示例多参数处理使用示例遍历所有参数 bat 脚本命令行参数 在Windows批处理&#xff08;.bat&#xff09;脚本中接收命令行参数是一个常见的需求&#xff0c;这样的脚本能够根据提供的参数执行不同的操作。命…

Flutter页面状态保留策略

目的: 防止每次点击底部按钮都进行一次页面渲染和网络请求 1. 使用IndexedStack 简单,只需要把被渲染的组件外部套一层IndexedStack即可 缺点: 在应用启动的时候,所有需要保存状态的页面都会直接被渲染,保存起来. 对性能有影响 2. 使用PageController 实现较为复杂,但是不用…

# 深入理解 Java 虚拟机 (二)

深入理解 Java 虚拟机 &#xff08;二&#xff09; Java内存模型 主内存与工作内存 所有的变量存储在主内存&#xff08;虚拟机内存的一部分&#xff09;每条线程有自己的工作内存&#xff0c;线程对变量的所有操作&#xff08;读取、赋值&#xff09;都必须在工作内存中进行…

Vue移动端动态表单生成组件

FormCreate 是一个可以通过 JSON 生成具有动态渲染、数据收集、验证和提交功能的表单生成组件。支持6个UI框架&#xff0c;适配移动端&#xff0c;并且支持生成任何 Vue 组件。内置20种常用表单组件和自定义组件&#xff0c;再复杂的表单都可以轻松搞定。 帮助文档 | 源码下载…

数据库系统概论(第5版教材)

第一章 绪论 1、数据(Data)是描述事物的符号记录&#xff1b; 2、数据库系统的构成&#xff1a;数据库 、数据库管理系统&#xff08;及其开发工具&#xff09; 、应用程序和数据库管理员&#xff1b; 3、数据库是长期存储在计算机内、有组织、可共享的大量数据的集合&…

【Linux】线程Thread

&#x1f525;博客主页&#xff1a; 我要成为C领域大神&#x1f3a5;系列专栏&#xff1a;【C核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 ​ ​ 线程概述 …

【C语言】解决C语言报错:Double Free

文章目录 简介什么是Double FreeDouble Free的常见原因如何检测和调试Double Free解决Double Free的最佳实践详细实例解析示例1&#xff1a;重复调用free函数示例2&#xff1a;多次释放全局变量指针示例3&#xff1a;函数间传递和释放指针 进一步阅读和参考资料总结 简介 Doub…

nuget 包修改默认存放路径

平时使用 nuget packages 时&#xff0c;都是下载包文件到本地。 默认是在C盘&#xff0c;时间一久容量会高达几十个G&#xff0c;这样会拖慢系统运行效率。 这时需要修改包的下载位置。 打开nuget 包配置文件&#xff1a;Nuget.config 路径在 C:\Users\{UserName}\AppData…

网约车停运损失费:2、协商过程

目录 &#x1f345;点击这里查看所有博文 随着自己工作的进行&#xff0c;接触到的技术栈也越来越多。给我一个很直观的感受就是&#xff0c;某一项技术/经验在刚开始接触的时候都记得很清楚。往往过了几个月都会忘记的差不多了&#xff0c;只有经常会用到的东西才有可能真正记…

鸿蒙开发Ability Kit(程序框架服务):【FA模型切换Stage模型指导】 配置文件差异

配置文件的差异 FA模型应用在[config.json文件]中描述应用的基本信息&#xff0c;一个应用工程中可以创建多个Module&#xff0c;每个Module中都有一份config.json文件。config.json由app、deviceConfig和module三部分组成&#xff0c;app标签用于配置应用级别的属性&#xff…

探索 JQuery EasyUI:构建简单易用的前端页面

介绍 当我们站在网页开发的浩瀚世界中&#xff0c;眼花缭乱的选择让我们难以抉择。而就在这纷繁复杂的技术海洋中&#xff0c;JQuery EasyUI 如一位指路明灯&#xff0c;为我们提供了一条清晰的航线。 1.1 什么是 JQuery EasyUI&#xff1f; JQuery EasyUI&#xff0c;简单来…

从写下第1个脚本到年薪40W,我的测试开发心路历程!

对于任何职业来说&#xff0c;薪资始终都会是众多追求的重要部分。前几年测试行业还是风口&#xff0c;但是随着不断新鲜血液的加入&#xff0c;再加上就业大环境不好&#xff0c;企业也都在“降本增效”。目前内卷也是越来越激烈。不得不承认当下的现状&#xff0c;已经不仅仅…

Cadence计算器函数leafValue

与getData结合使用 leafValue( getData(“/output” ?result “dc”) 转自eetop https://bbs.eetop.cn/thread-931912-1-1.html

DM达梦数据库数学函数整理

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…