房产网站建设价格/今日新闻国际最新消息
数据脱敏
敏感数据在存储过程中为是否为明文, 分为两种
- 落地脱敏: 存储的都是明文, 返回之前做脱敏处理
- 不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密
落地脱敏
这里指的是数据库中存储的是明文数据, 返回给前端的时候脱敏
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插件能在哪个生命周期阶段起作用?
如下
-
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
对象
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
: 声明使用自定义的序列化方法SensitiveJsonSerializerJackSon相关注解和使用参考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是一个加密值, 需要提前生成
生成方式如下
-
代码API生成
-
@Autowired private StringEncryptor stringEncryptor;public void encrypt(String content) {String encryptStr = stringEncryptor.encrypt(content);System.out.println("加密后的内容: " + encryptStr); }
-
-
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配置
-
新增
resources/META-INF/services
目录下 -
该目录下新增配置,配置文件名为
org.apache.shardingsphere.encrypt.strategy.spi.Encryptor
-
配置文件里的内容,放入自定义的加密策略的类的全路径,和要使用官方内置的加密策略的类的全路径
- 内置的加密策略为:
AESEncryptor
和MD5Encryptor
- 自定义加密策略为:
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
-
清空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查询,得到目标数据行的主键,再以目标数据行的主键为条件返回目标表进行精确查询
参考资料
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天,感觉可以收拾工位了
加密后的数据如何进行模糊查询
相关文章:

Java数据脱敏
数据脱敏 敏感数据在存储过程中为是否为明文, 分为两种 落地脱敏: 存储的都是明文, 返回之前做脱敏处理不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密 落地脱敏 这里指的是数据库中存储的是明文数据, 返回给前端的时候脱…...

【Java Web】三大域对象
目录 一、域对象概述 二、三大域对象 三、域对象使用相关API 一、域对象概述 一些可用于存储数据和传递数据的对象被称为域对象,根据传递数据范围的不同,我们称之为不同的域,不同的域对象代表不同的域,共享数据的范围也不同。 二、…...

【Linux】进程信号_3
文章目录 八、进程信号2. 信号的保存3. 信号的处理 未完待续 八、进程信号 2. 信号的保存 实际执行信号的处理动作称为信号递达(Delivery) 信号从产生到递达之间的状态,称为信号未决(Pending)。 进程可以选择阻塞 (Block )某个信号。 被阻塞的信号产生时将保持在未决状态,直到…...

LongRAG:利用长上下文大语言模型提升检索生成效果
一、前言 前面我们已经介绍了多种检索增强生成 (RAG) 技术,基本上在保证数据质量的前提下,检索增强生成(RAG)技术能够有效提高检索效率和质量,相对于大模型微调技术,其最大的短板还是在于有限的上下文窗口…...

go中的方法 func-----数据类型
本文是java学习者学go种产生的容易记混点的笔记,所以有其他编译语言的基础更好 go的方法有点像js 基础 func main() {fmt.Println("Starting")var p *string new(string)*p "hello world"demo : "demo"fmt.Println(*&demo) //这样既然也…...

408计算机网络--物理层
一、物理层概述 物理层是干嘛使得? 物理层解决如何在连接各种计算机的传输媒体上传输数据比特流,而不是指具体的传输媒体。 物理层主要任务是确定与传输媒体接口有关的一些特性。定义标准可以理解为插排上的两孔三孔 机械特性:定义物理连接…...

十年,亚马逊云科技合作伙伴网络开启AI新征程
“十年之前,你不认识我,我不认识你,因为云计算我们携手并肩;十年之后,我们仍是伙伴,更是朋友,因为人工智能再次起程。”这就是今天的亚马逊云科技与其合作伙伴的真实写照。 2024年是亚马逊云科技…...

基于Spring Boot的在线医疗咨询平台的设计与实现【附源码】
基于Spring Boot的在线医疗咨询平台的设计与实现 Design and implementation of the computer hardware mall based on Spring Boot Candidate: Supervisor: April 20th, 2024 学位论文原创性声明 本人郑重声明:所呈交的论文是本人在导师…...

星坤Type-A连接器:创新快充技术,引领电子连接!
快速发展的电子时代,消费者对电子设备的性能和便利性有着更高的要求。特别是在充电和数据传输方面,快充技术和高速传输已成为市场的新宠。中国星坤公司推出的Type-A连接器系列,以其卓越的性能和创新的设计,满足了市场对高效、稳定…...

入门JavaWeb之 Response 下载文件
web 服务器接收到客户端的 http 请求 针对这个请求,分别创建一个代表请求的 HttpServletRequest 对象,代表响应的 HttpServletResponse 对象 获取客户端请求过来的参数:HttpServletRequest 给客户端响应一些信息:HttpServletRe…...

Java自定义注解校验token并直接返回给前端状态
自定义注解 CheckToken import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public int…...

C++ | Leetcode C++题解之第200题岛屿数量
题目: 题解: class Solution { private:void dfs(vector<vector<char>>& grid, int r, int c) {int nr grid.size();int nc grid[0].size();grid[r][c] 0;if (r - 1 > 0 && grid[r-1][c] 1) dfs(grid, r - 1, c);if (r …...

Linux安全配置
Linux系统审计信息有:系统启动日志(boot.log)、记录用户执行命令日志(acct/pacct)、记录使用su命令的使用(sulog)、记录当前登录的用户信息(utmp)、用户每次登陆和退出信…...

vue实现不预览PDF的情况下打印pdf文件
前景:默认情况,实现打印需要根据预览的内容进行打印。 但是当只有打印按钮存在,不预览文件内容的情况下,实现打印的话,可以通过后端接口返回服务器上PDF的地址,前端通过隐藏的iframe标签中src可实现预览功能 主要是根据…...

C++ | Leetcode C++题解之第199题二叉树的右视图
题目: 题解: class Solution { public:vector<int> rightSideView(TreeNode* root) {unordered_map<int, int> rightmostValueAtDepth;int max_depth -1;stack<TreeNode*> nodeStack;stack<int> depthStack;nodeStack.push(ro…...

[leetcode]圆圈中最后剩下的数字/ 破冰游戏
. - 力扣(LeetCode) class Solution {int f(int num, int target) {if (num 1) {return 0;}int x f(num - 1, target);return (target x) % num;} public:int iceBreakingGame(int num, int target) {return f(num, target);} };...

mysql数据库的管理
目录 一、常用的数据类型 二、MySQ数据库基础操作 1、登录数据库 2、查看当前的 MySQL 版本信息及连接用户名 3、查看当前服务器中的数据库 4.查看数据库中包含的表 5.查看表的结构(字段) 6、MySQL的6大约束属性 三、SQL…...

Java项目分层(持续更新中)
第一次更新时间2024.6.26 分包 实体类 功能类 工具类 分层 实体类层 我们要操作的对象,Book,Student... 控制层 控制请求转发 业务层 处理业务 数据层 连接数据库 处理数据 工具类层 JDBC等工具类 测试层 最终启动项目 明确我们所要做的业务之后&a…...

2024年软件测试面试题大全【答案+文档】
🍅 视频学习:文末有免费的配套视频可观看 🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 一、面试基础题 简述测试流程: 1、阅读相关技术文档(如产品PRD、UI设计…...

数据赋能(131)——体系:数据转换——概述、关注焦点
概述 数据转换是指将数据从一种格式、结构或类型转换为另一种格式、结构或类型的过程。 数据转换操作属于数据整理过程。 它通常涉及数据清洗、数据映射、数据合并、数据拆分等操作,以确保数据的正确性和一致性。 数据转换的目的在于将原始数据转换为更易于处理…...

【自然语言处理系列】掌握jieba分词器:从基础到实战,深入文本分析与词云图展示
本文旨在全面介绍jieba分词器的功能与应用,从分词器的基本情况入手,逐步解析全模式与精确模式的不同应用场景。文章进一步指导读者如何通过添加自定义词典优化分词效果,以及如何利用jieba分词器进行关键词抽取和词性标注,为后续的…...

TikTok短视频矩阵系统
随着数字化时代的到来,短视频已成为人们获取信息、娱乐消遣的重要渠道。TikTok,作为全球最受欢迎的短视频平台之一,其背后的短视频矩阵系统是支撑其成功的关键因素。本文将深入探讨TikTok短视频矩阵系统的构成、功能以及它在新媒体时代中的影…...

码题杯:我会修改图
原题链接:码题集OJ-我会修改图 题目大意:给你一张n个点(编号为1∼n),m条边(编号为1∼m)的无向图,图上每个点都有一个点权,权值分别为a1,a2,…,an&…...

MongoDB Map-Reduce 简介
MongoDB Map-Reduce 简介 MongoDB 是一个流行的 NoSQL 数据库,它使用文档存储数据,这些数据以 JSON 格式存储。MongoDB 提供了多种数据处理方法,其中 Map-Reduce 是一种用于批量处理和聚合数据的功能强大的工具。Map-Reduce 允许用户对大量数…...

某平台小程序逆向思路整理
一、下载软件 devtools 二、强制打开控制台 根据返回的数据我们得知数据被加密了 找到这个加密的js 发现加密的位置 打断点进入这个加密的方法 之后自定义js。python调用解密即可。...

黑马苍穹外卖6 清理redis缓存+Spring Cache+购物车的增删改查
缓存菜品 后端服务都去查询数据库,对数据库访问压力增大。 解决方式:使用redis来缓存菜品,用内存比磁盘性能更高。 key :dish_分类id String key “dish_” categoryId; RestController("userDishController") RequestMapping…...

鸿蒙开发系统基础能力:【@ohos.systemTime (设置系统时间)】
设置系统时间 本模块用来设置、获取当前系统时间,设置、获取当前系统日期和设置、获取当前系统时区。 说明: 本模块首批接口从API version 7开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。 导入模块 import systemTime …...

CVE-2020-26048(文件上传+SQL注入)
简介 CuppaCMS是一套内容管理系统(CMS)。 CuppaCMS 2019-11-12之前版本存在安全漏洞,攻击者可利用该漏洞在图像扩展内上传恶意文件,通过使用文件管理器提供的重命名函数的自定义请求,可以将图像扩展修改为PHP…...

【面试题】信息系统安全运维要做什么
信息系统安全运维是确保信息系统稳定、可靠、安全运行的一系列活动和措施。 其主要包括以下几个方面: 1.系统监控: 实时监测信息系统的运行状态,如服务器的性能指标、网络流量、应用程序的运行情况等。通过监控工具,及时发现系统…...

引导过程与服务器控制
一、引导过程 1.开机自检 服务器主机开机以后,将根据主板 BIOS 中的设置对 CPU(Central Processing Unit, 中央处理器)、内存、显卡、键盘等设备进行初步检测,检测成功后根据预设的启动顺序移 交系统控制权,…...