做网站广告中敏感词会涉及到工商,wordpress主题 dux,wordpress关键词标签,网站模板 酒店 中文数据脱敏
敏感数据在存储过程中为是否为明文, 分为两种
落地脱敏: 存储的都是明文, 返回之前做脱敏处理不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密
落地脱敏
这里指的是数据库中存储的是明文数据, 返回给前端的时候脱…数据脱敏
敏感数据在存储过程中为是否为明文, 分为两种
落地脱敏: 存储的都是明文, 返回之前做脱敏处理不落地脱敏: 存储前就脱敏, 使用时解密, 即用户数据进入系统, 脱敏存储到数据库中, 查询时反向解密
落地脱敏
这里指的是数据库中存储的是明文数据, 返回给前端的时候脱敏
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 FunctionString,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({// 拦截querySignature(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序列化后较为合适
自定义脱敏序列化
/*** 自定义脱敏序列化* JsonSerializerString: 指定String 类型* serialize()方法用于将修改后的数据载入*/
Slf4j
public class SensitiveJsonSerializer extends JsonSerializerString 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依赖实现单密钥对称加密 !--配置文件加密--dependencygroupIdcom.github.ulisesbocchio/groupIdartifactIdjasypt-spring-boot-starter/artifactIdversion2.1.0/version/dependency总配置
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency!--配置文件加密--dependencygroupIdcom.github.ulisesbocchio/groupIdartifactIdjasypt-spring-boot-starter/artifactIdversion2.1.0/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.6/version/dependency!-- druid数据源驱动 --dependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.6/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependency/dependenciesyaml配置
脱敏的一些配置
# 密钥对安全性要求比较高, 不建议直接显示在项目中, 可以通过启动时-D参数注入, 或者放在配置中心
# 例如password, prefix, suffix, algorithm都简易-D参数注入, 最低最低要求password要通过-D注入
# 密钥相关配置
jasypt:encryptor:# 秘钥配置项, 密钥不支持中文password: whitebrocadeproperty:# 前缀, 后缀 # 和要加密的元素拼接, 例如加密值为12345678, 12是前缀, 78是后缀, 3456是特有的值 那么配置了前后缀就是12345678 对拼接的字符串进行加密prefix: 12suffix: 78# 加密算法, 默认是PBEWITHMD5ANDDESalgorithm: PBEWithMD5AndDES例如启动程序命令如下
java -jar -Djasypt.encryptor.passwordwhitebrocad jasypt-demo.jarjava -jar -Djasypt.encryptor.passwordwhitebrocad -Djasypt.encryptor.property.prefix12 -Djasypt.encryptor.property.suffix78 -Djasypt.encryptor.algorithmPBEWithMD5AndDES jasypt-demo.jar使用流程
假设现在要对MySQL的密码进行进行脱敏
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/demo?useSSLfalseuseUnicodetruecharacterEncodingUTF-8autoReconnecttrueze oDateTimeBehaviorconvertToNullserverTimezoneAsia/Shanghaiusername: root# 对MySQL的密码进行加密脱敏password: 12345678jasypt:encryptor:password: whitebrocadeproperty:prefix: 12suffix: 78algorithm: 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 input12345678 passwordwhitebrocade algorithmPBEWithMD5AndDESE:\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?useSSLfalseuseUnicodetruecharacterEncodingUTF-8autoReconnecttrueze oDateTimeBehaviorconvertToNullserverTimezoneAsia/Shanghaiusername: root# 对MySQLpassword: ENC(0jSWFsiP9ZVKg3USneAl76beGfuovVlG)jasypt:encryptor:password: whitebrocadeproperty:prefix: 12suffix: 78algorithm: PBEWithMD5AndDES表示一个加密操作, 那么此时需要加密的内容就是prefixphonesuffix拼接成的内容, 即ENC(prefixphonesuffix), 这里的前缀和后缀起了一个盐值的作用 ENC(XXX格式主要为了便于识别该值是否需要解密如不按照该格式配置在加载配置项的时候jasypt将保持原值不进行解密 相关测试
相关相关测试代码
Controller
public class MyTestController {Autowiredprivate StringEncryptor stringEncryptor;AutowiredJdbcTemplate jdbcTemplate;ResponseBodyRequestMapping(/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 sqlselect * from t1;ListMapString,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 { EncryptMethodPostMapping(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,1163.com),(2,jack,101,511,2163.com),(3,lucy,101,1016,3163.com),(4,amy,103,615,4163.com);pom配置
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency!--配置文件加密--dependencygroupIdcom.github.ulisesbocchio/groupIdartifactIdjasypt-spring-boot-starter/artifactIdversion2.1.0/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.6/version/dependency!-- druid数据源驱动 --dependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.6/version/dependencydependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.26/version/dependency!-- Mybatis --dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion2.3.1/version/dependency
/dependencies相关代码
yaml配置
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/demo?useSSLfalseuseUnicodetruecharacterEncodingUTF-8autoReconnecttrueze oDateTimeBehaviorconvertToNullserverTimezoneAsia/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);/*** 查询全部学生信息*/ListStudentInfo selectAll();
}
Mapper的xml
?xml version1.0 encodingUTF-8?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.whitebrocade.jasyptdemo.demos.mapper.StudentMapperselect idgetInfo resultTypecom.whitebrocade.jasyptdemo.demos.domain.StudentInfoselect *from studentwhere id#{id}/selectselect idgetInfoByName resultTypecom.whitebrocade.jasyptdemo.demos.domain.StudentInfoselect *from studentwhere sname#{sname}/selectinsert idinsertInfo parameterTypecom.whitebrocade.jasyptdemo.demos.domain.StudentInfoinsert into student(id,sname,classId,birthday,email)values (#{id},#{sname},#{classId},#{birthday},#{email});/insertdelete iddeleteByIddeletefrom studentwhere id#{id}/deleteupdate idupdateById parameterTypecom.whitebrocade.jasyptdemo.demos.domain.StudentInfoupdate studentset sname #{sname},classId #{classId},birthday #{birthday}, email #{email}where id #{id}/updateselect idselectAll resultTypecom.whitebrocade.jasyptdemo.demos.domain.StudentInfoselect *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);// ClassParameterHandler 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方法ListString 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;MapString, Object map null;if (paramsObject instanceof String) {// 表示只传有一个参数obj (String) paramsObject;} else if (paramsObject instanceof Map) {map (MapString, 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)ArrayListObjects resultList (ArrayListObjects) 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字符串的长度必须为162432长度);}// 获取加密算法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;ResponseBodyRequestMapping(/getInfo)public void getInfo(Param(id) String id) {StudentInfo stu studentMapper.getInfo(id);System.out.println(stu stu);}// http://localhost:8080/test5?snametomResponseBodyRequestMapping(/getInfoByName)public StudentInfo getInfoByName(Param(sname) String sname) {StudentInfo stu studentMapper.getInfoByName(sname);System.out.println(stu stu);return stu;}ResponseBodyPostMapping(/insertInfo)public StudentInfo insertInfo(RequestBody StudentInfo studentInfo) {studentMapper.insertInfo(studentInfo);return studentInfo;}ResponseBodyPostMapping(/updateById)public StudentInfo updateById(RequestBody StudentInfo studentInfo) {studentMapper.updateById(studentInfo);return studentInfo;}ResponseBodyGetMapping(/selectAll)public ListStudentInfo selectAll() {return studentMapper.selectAll();}ResponseBodyDeleteMapping(/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 inputwhitebrocade1234 passwordwhitebrocade algorithmPBEWithMD5AndDES对Myabtis加密脱敏所使用的算法进行加密
java -cp E:\software\Maven\repository\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI inputAES passwordwhitebrocade algorithmPBEWithMD5AndDES修改后的yaml配置如下
spring:datasource:driver-class-name: com.mysql.jdbc.Driver# url其实加密都不错的url: jdbc:mysql://127.0.0.1:3306/demo?useSSLfalseuseUnicodetruecharacterEncodingUTF-8autoReconnecttrueze oDateTimeBehaviorconvertToNullserverTimezoneAsia/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)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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)})ListUserEntity 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?useSSLfalseuseUnicodetruecharacterEncodingUTF-8autoReconnecttrueze oDateTimeBehaviorconvertToNullserverTimezoneAsia/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字符串的长度必须为162432长度);}// 获取加密算法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() {ListUserEntity userEntityList userMapper.getUserInfo(tom, 123456);userEntityList.forEach(System.out::println);}
}测试CustomEncryptor 清空t_user表 修改yaml配置 type选择 CustomEncryptorassisted-query-column参数注释掉 执行inserter()方法, 发现MySQL中新增数据 执行getUserInfo, 发现解密成功
测试CustomQueryAssistedEncryptor 清空t_user表 修改yaml配置 type选择 CustomQueryAssistedEncryptorCustomEncrypto记得注释掉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 配置文件、隐私数据脱敏的最佳实践原理源码
加密后的敏感字段还能进行模糊查询吗该如何实现_加密后的敏感字段还能进行模糊查询吗?该如何实现?
淘宝密文字段检索方案
mybatis4—自定义拦截器下对象详解
求求你别乱脱敏了MyBatis 插件 注解轻松实现数据脱敏So easy~! - Java技术栈
一种使用mybatis进行脱敏的思路
Apache ShardingSphere数据脱敏全解决方案详解(上)
ShardingSphere4.1.1Sharding-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.morning.rwlnk.cn.gov.cn.rwlnk.cn http://www.morning.pjzcp.cn.gov.cn.pjzcp.cn http://www.morning.lbssg.cn.gov.cn.lbssg.cn http://www.morning.qqhmg.cn.gov.cn.qqhmg.cn http://www.morning.hnrpk.cn.gov.cn.hnrpk.cn http://www.morning.lbssg.cn.gov.cn.lbssg.cn http://www.morning.qrqcr.cn.gov.cn.qrqcr.cn http://www.morning.zwppm.cn.gov.cn.zwppm.cn http://www.morning.plqhb.cn.gov.cn.plqhb.cn http://www.morning.kqxng.cn.gov.cn.kqxng.cn http://www.morning.drqrl.cn.gov.cn.drqrl.cn http://www.morning.mhwtq.cn.gov.cn.mhwtq.cn http://www.morning.gryzk.cn.gov.cn.gryzk.cn http://www.morning.bqfpm.cn.gov.cn.bqfpm.cn http://www.morning.jkfyt.cn.gov.cn.jkfyt.cn http://www.morning.gsqw.cn.gov.cn.gsqw.cn http://www.morning.sgjw.cn.gov.cn.sgjw.cn http://www.morning.kmcby.cn.gov.cn.kmcby.cn http://www.morning.nlcw.cn.gov.cn.nlcw.cn http://www.morning.kncrc.cn.gov.cn.kncrc.cn http://www.morning.tjkth.cn.gov.cn.tjkth.cn http://www.morning.lwqst.cn.gov.cn.lwqst.cn http://www.morning.jjhrj.cn.gov.cn.jjhrj.cn http://www.morning.xkbdx.cn.gov.cn.xkbdx.cn http://www.morning.jqwpw.cn.gov.cn.jqwpw.cn http://www.morning.ghgck.cn.gov.cn.ghgck.cn http://www.morning.duqianw.com.gov.cn.duqianw.com http://www.morning.plchy.cn.gov.cn.plchy.cn http://www.morning.smpmn.cn.gov.cn.smpmn.cn http://www.morning.qgkcs.cn.gov.cn.qgkcs.cn http://www.morning.llqch.cn.gov.cn.llqch.cn http://www.morning.wkqrp.cn.gov.cn.wkqrp.cn http://www.morning.qwrb.cn.gov.cn.qwrb.cn http://www.morning.qyhcg.cn.gov.cn.qyhcg.cn http://www.morning.gpfuxiu.cn.gov.cn.gpfuxiu.cn http://www.morning.syrzl.cn.gov.cn.syrzl.cn http://www.morning.lltdf.cn.gov.cn.lltdf.cn http://www.morning.wtcbl.cn.gov.cn.wtcbl.cn http://www.morning.xppj.cn.gov.cn.xppj.cn http://www.morning.trkl.cn.gov.cn.trkl.cn http://www.morning.langlaitech.cn.gov.cn.langlaitech.cn http://www.morning.xrlwr.cn.gov.cn.xrlwr.cn http://www.morning.qpqwb.cn.gov.cn.qpqwb.cn http://www.morning.bftqc.cn.gov.cn.bftqc.cn http://www.morning.dnphd.cn.gov.cn.dnphd.cn http://www.morning.klrpm.cn.gov.cn.klrpm.cn http://www.morning.jycr.cn.gov.cn.jycr.cn http://www.morning.jbpdk.cn.gov.cn.jbpdk.cn http://www.morning.lxhrq.cn.gov.cn.lxhrq.cn http://www.morning.sxhdzyw.com.gov.cn.sxhdzyw.com http://www.morning.qkpzq.cn.gov.cn.qkpzq.cn http://www.morning.kwnbd.cn.gov.cn.kwnbd.cn http://www.morning.mdxwz.cn.gov.cn.mdxwz.cn http://www.morning.hmlpn.cn.gov.cn.hmlpn.cn http://www.morning.hlxpz.cn.gov.cn.hlxpz.cn http://www.morning.jlktz.cn.gov.cn.jlktz.cn http://www.morning.nrwr.cn.gov.cn.nrwr.cn http://www.morning.rkzb.cn.gov.cn.rkzb.cn http://www.morning.dpwcl.cn.gov.cn.dpwcl.cn http://www.morning.ypbp.cn.gov.cn.ypbp.cn http://www.morning.kxscs.cn.gov.cn.kxscs.cn http://www.morning.hxmqb.cn.gov.cn.hxmqb.cn http://www.morning.pjwrl.cn.gov.cn.pjwrl.cn http://www.morning.mdlqf.cn.gov.cn.mdlqf.cn http://www.morning.pqbkk.cn.gov.cn.pqbkk.cn http://www.morning.kfysh.com.gov.cn.kfysh.com http://www.morning.smtrp.cn.gov.cn.smtrp.cn http://www.morning.cjmmn.cn.gov.cn.cjmmn.cn http://www.morning.nccqs.cn.gov.cn.nccqs.cn http://www.morning.nnhrp.cn.gov.cn.nnhrp.cn http://www.morning.gqddl.cn.gov.cn.gqddl.cn http://www.morning.nzqmw.cn.gov.cn.nzqmw.cn http://www.morning.rzjfn.cn.gov.cn.rzjfn.cn http://www.morning.wrwcf.cn.gov.cn.wrwcf.cn http://www.morning.rsxw.cn.gov.cn.rsxw.cn http://www.morning.wlnr.cn.gov.cn.wlnr.cn http://www.morning.qstkk.cn.gov.cn.qstkk.cn http://www.morning.jxrpn.cn.gov.cn.jxrpn.cn http://www.morning.qzmnr.cn.gov.cn.qzmnr.cn http://www.morning.jpgfq.cn.gov.cn.jpgfq.cn