赞
踩
敏感数据在存储过程中为是否为明文, 分为两种
这里指的是数据库中存储的是明文数据, 返回给前端的时候脱敏
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
}
}
这个类包含了一些拦截对象的信息
/** * 拦截类 */ 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()
: 处理存储过程的参数Mybatis提供了一个工具类org.apache.ibatis.reflection.MetaObject
。它通过反射来读取和修改对象的元信息。我们可以利用它来处理四大对象的一些属性, 这是Mybatis插件开发的一个常用工具类。
通常情况下, 我们会选择使用静态方法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(); // 这里组合一个MetaObject public static final MetaObject NULL_META_OBJECT = MetaObject.forObject(new NullObject(), DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); private SystemMetaObject() { // 防止静态类的实例化 // Prevent Instantiation of Static Class } private static class NullObject { } public static MetaObject forObject(Object object) { return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory()); } }
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}))
下边有两个拦截器, 拦截时期有些不同, 但是都是可以的, 选择启动一个即可
ResultSetHandler#handleResultSets
@Slf4j @Intercepts( @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) ) public class SensitiveInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); log.debug("进入数据脱敏拦截器..."); if (result instanceof List) { List<?> records = (List<?>) result; records.forEach(this::sensitive); return records; } else if (result instanceof Map) { Map<?, ?> records = (Map<?, ?>) result; records.values().forEach(this::sensitive); return records; } else { log.info("数据脱敏失败, 脱敏的数据: {}", result); } return result; } /** * 数据脱敏 * @param source 要脱敏的数据 */ private void sensitive(Object source) { // 拿到返回值类型 Class<?> sourceClass = source.getClass(); // 初始化返回值类型的 MetaObject MetaObject metaObject = SystemMetaObject.forObject(source); // 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理 Stream.of(sourceClass.getDeclaredFields()) .filter(field -> field.isAnnotationPresent(Sensitive.class)) .forEach(field -> doSensitive(metaObject, field)); } /** * @param metaObject metaObject工具类 * @param field 脱敏字段 */ private void doSensitive(MetaObject metaObject, Field field) { // 拿到属性名 String name = field.getName(); // 获取属性值 Object value = metaObject.getValue(name); // 只有字符串类型才能脱敏 而且不能为null if (String.class == metaObject.getGetterType(name) && value != null) { String str = (String) value; Sensitive sensitive = field.getAnnotation(Sensitive.class); // 获取对应的脱敏策略 并进行脱敏 SensitiveStrategy type = sensitive.strategy(); Object o = type.getDesensitizer().apply(str); // 把脱敏后的值塞回去 metaObject.setValue(name, o); } } }
Executor#query
@Slf4j @Component @Intercepts({ // 拦截query @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class SensitiveInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { log.debug("进入数据脱敏拦截器前..."); // 脱敏入库 Object result = invocation.proceed(); // 数据 Object result = invocation.proceed(); log.debug("进入数据脱敏拦截器..."); if (result instanceof List) { List<?> records = (List<?>) result; records.forEach(this::sensitive); return records; } else if (result instanceof Map) { Map<?, ?> records = (Map<?, ?>) result; records.values().forEach(this::sensitive); return records; } else { log.info("数据脱敏失败, 脱敏的数据: {}", result); } return result; } /** * 数据脱敏 * @param source 要脱敏的数据 */ private void sensitive(Object source) { // 拿到返回值类型 Class<?> sourceClass = source.getClass(); // 初始化返回值类型的 MetaObject MetaObject metaObject = SystemMetaObject.forObject(source); // 捕捉到属性上的标记注解 @Sensitive 并进行对应的脱敏处理 Stream.of(sourceClass.getDeclaredFields()) .filter(field -> field.isAnnotationPresent(Sensitive.class)) .forEach(field -> this.doSensitive(metaObject, field)); } /** * @param metaObject metaObject工具类 * @param field 脱敏字段 */ private void doSensitive(MetaObject metaObject, Field field) { // 拿到属性名 String name = field.getName(); // 获取属性值 Object value = metaObject.getValue(name); // 只有字符串类型才能脱敏 而且不能为null if (String.class == metaObject.getGetterType(name) && value != null) { String str = (String) value; Sensitive sensitive = field.getAnnotation(Sensitive.class); // 获取对应的脱敏策略 并进行脱敏 SensitiveStrategy type = sensitive.strategy(); Object o = type.getDesensitizer().apply(str); // 把脱敏后的值塞回去 metaObject.setValue(name, o); } } }
同上
同上
ORM查询出来后需要部分逻辑处理, 如果此时脱敏了, 那么就没法处理该逻辑, 脱敏放置在JSON序列化后较为合适
/** * 自定义脱敏序列化 * JsonSerializer<String>: 指定String 类型 * serialize()方法用于将修改后的数据载入 */ @Slf4j public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer { private SensitiveStrategy strategy; /** * 执行脱敏序列化逻辑 */ @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { try { SensitiveService sensitiveService = SpringUtils.getBean(SensitiveService.class); // 开启了脱敏 if (ObjectUtil.isNotNull(sensitiveService) && sensitiveService.isSensitive()) { // 用指定的脱敏策略脱敏 gen.writeString(this.strategy.desensitizer().apply(value)); } else { // 不脱敏 gen.writeString(value); } } catch (BeansException e) { log.error("脱敏策略未指定, 将不进行脱敏操作, 待脱敏数据为: {}", e.getMessage()); gen.writeString(value); } } /** * 获取实体类上的@Sensitive注解并根据条件初始化对应的JsonSerializer对象 */ @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { Sensitive annotation = property.getAnnotation(Sensitive.class); if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) { this.strategy = annotation.strategy(); return this; } return prov.findValueSerializer(property.getType(), property); } }
Jackson相关注解和使用参考Jackson 进阶之自定义序列化器
/**
* 自定义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脱敏则是更加好
假设查询列中有个手机号, 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>
脱敏的一些配置
# 密钥对安全性要求比较高, 不建议直接显示在项目中, 可以通过启动时-D参数注入, 或者放在配置中心
# 例如password, prefix, suffix, algorithm都简易-D参数注入, 最低最低要求password要通过-D注入
# 密钥相关配置
jasypt:
encryptor:
# 秘钥配置项, 密钥不支持中文
password: whitebrocade
property:
# 前缀, 后缀
# 和要加密的元素拼接, 例如加密值为12345678, 12是前缀, 78是后缀, 3456是特有的值 那么配置了前后缀就是12345678 对拼接的字符串进行加密
prefix: "12"
suffix: "78"
# 加密算法, 默认是PBEWITHMD5ANDDES
algorithm: PBEWithMD5AndDES
例如启动程序命令如下
java -jar -Djasypt.encryptor.password=whitebrocad jasypt-demo.jar
java -jar -Djasypt.encryptor.password=whitebrocad -Djasypt.encryptor.property.prefix="12" -Djasypt.encryptor.property.suffix="78" -Djasypt.encryptor.algorithm=PBEWithMD5AndDES jasypt-demo.jar
假设现在要对MySQL的密码进行进行脱敏
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# 对MySQL的密码进行加密脱敏
password: 12345678
jasypt:
encryptor:
password: whitebrocade
property:
prefix: "12"
suffix: "78"
algorithm: PBEWithMD5AndDES
首先明确的是, 12345678
是不能直接显示, 所以这里的password是一个加密值, 需要提前生成
生成方式如下
代码API生成
@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.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
# 对MySQL
password: ENC(0jSWFsiP9ZVKg3USneAl76beGfuovVlG)
jasypt:
encryptor:
password: whitebrocade
property:
prefix: "12"
suffix: "78"
algorithm: PBEWithMD5AndDES
表示一个加密操作, 那么此时需要加密的内容就是prefix+phone+suffix拼接成的内容, 即ENC(prefix+phone+suffix), 这里的前缀和后缀起了一个盐值的作用
ENC(XXX)格式主要为了便于识别该值是否需要解密,如不按照该格式配置,在加载配置项的时候
jasypt
将保持原值,不进行解密
相关相关测试代码
@Controller public class MyTestController { @Autowired private StringEncryptor stringEncryptor; @Autowired JdbcTemplate jdbcTemplate; @ResponseBody @RequestMapping("/test") public void encrypt(){ String content = "12345678"; String encryptStr = stringEncryptor.encrypt(content); System.out.println("加密后的内容:" + encryptStr); String decryptStr = stringEncryptor.decrypt(encryptStr); System.out.println("解密后的内容:" + decryptStr); this.list(); } /** * 查询数据库信息 */ public void list(){ // 数据库中有t1表, 并且有数据 String sql="select * from t1"; List<Map<String,Object>> list_map = jdbcTemplate.queryForList(sql); System.out.println("list_map = " + list_map); } }
运行结果如下, 发现确实可以连接数据库
生产环境用户的隐私数据,比如手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理
入库前的脱敏, 查询时的反向解密, 一前一后适合使用AOP来实现
这里是全脱敏, 不支持模糊查询!
模糊查询可以通过分词密文映射表查询, 后续再说
自定义两个注解@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 { @Autowired private StringEncryptor stringEncryptor; @Pointcut("@annotation(com.whitebrocade.jasyptdemo.demos.anno.EncryptMethod)") public void pointCut() { } @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) { // 加密 this.encrypt(joinPoint); // 解密 Object decrypt = this.decrypt(joinPoint); return decrypt; } /** * 加密 */ public void encrypt(ProceedingJoinPoint joinPoint) { try { Object[] objects = joinPoint.getArgs(); if (objects.length != 0) { for (Object o : objects) { if (o instanceof String) { this.encryptStr(o); } else { this.handler(o, ENCRYPT); } //TODO 其余类型自己看实际情况加 } } } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * 解密 */ public Object decrypt(ProceedingJoinPoint joinPoint) { Object result = null; try { Object obj = joinPoint.proceed(); if (obj != null) { if (obj instanceof String) { this.decryptStr(obj); } else { result = this.handler(obj, DECRYPT); } // TODO 其余类型自己看实际情况加 } } catch (Throwable e) { log.error("解密失败", e); throw new RuntimeException(); } return result; } /** * 解密或者解密 * @param obj 要加密/解密的元素 * @param type 加密/解密 * @return 加密/解密后的内容 */ private Object handler(Object obj, String type) throws IllegalAccessException { if (Objects.isNull(obj)) { return null; } Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { // 获取EncryptField标识的注解 boolean hasSecureField = field.isAnnotationPresent(EncryptField.class); if (hasSecureField) { field.setAccessible(true); String realValue = (String) field.get(obj); String value; if (DECRYPT.equals(type)) { value = stringEncryptor.decrypt(realValue); } else { value = stringEncryptor.encrypt(realValue); } field.set(obj, value); } } return obj; } /** * 字符串内容加密 * @param realValue 字符串 * @return 加密后的字符串 */ public String encryptStr(Object realValue) { String value = null; try { value = stringEncryptor.encrypt(String.valueOf(realValue)); } catch (Exception e) { log.error("加密失败", e); return value; } return value; } /** * 字符串内容解密 * @param realValue 要解密的字符串 * @return 解密后的字符串 */ public String decryptStr(Object realValue) { String value = String.valueOf(realValue); try { value = stringEncryptor.decrypt(value); } catch (Exception e) { log.error("解密失败", e); return value; } return value; } }
@Data
public class UserVo implements Serializable {
private Long userId;
@EncryptField
private String mobile;
@EncryptField
private String address;
private String age;
}
@RestController public class MyTestController { @EncryptMethod @PostMapping(value = "/test") @ResponseBody public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) { System.out.println("前端传入参数user: " + JSONUtil.toJsonStr(user)); return this.insertUser(user, name); } private UserVo insertUser(UserVo user, String name) { System.out.println("加密后的数据:user" + JSONUtil.toJsonStr(user)); System.out.println("加密后的数据:name" + name); return user; } }
测试数据
测试结果
发现前端传递的数据接受的时候就加密了, 如果需要在业务中做判断, 那么是比较麻烦的
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');
<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>
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai username: root # ENC中的值是可以不断替换的 password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD) jasypt: encryptor: password: whitebrocade mybatis: mapper-locations: classpath:mapper/*.xml # application.yml logging: level: com.whitebrocade.jasyptdemo.demos: debug # ----------------- # 加密配置 whitebrocade: crypto: secret-key: whitebrocade1234 algorithm: AES
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; @EncryptTransaction private String sname; private String classId; private String birthday; private String email; }
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(); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper"> <select id="getInfo" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo"> select * from student where id=#{id} </select> <select id="getInfoByName" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo"> select * from student where sname=#{sname} </select> <insert id="insertInfo" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo"> insert into student(id,sname,classId,birthday,email) values (#{id},#{sname},#{classId},#{birthday},#{email}); </insert> <delete id="deleteById"> delete from student where id=#{id} </delete> <update id="updateById" parameterType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo"> update student set sname = #{sname},classId = #{classId}, birthday = #{birthday}, email = #{email} where id = #{id} </update> <select id="selectAll" resultType="com.whitebrocade.jasyptdemo.demos.domain.StudentInfo"> select * from student </select> </mapper>
import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction; import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; /** * 加密拦截 */ @Slf4j @Component @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class), }) public class EncryptInterceptor implements Interceptor { @Autowired private Encoder encoder; @Override public Object intercept(Invocation invocation) throws Throwable { //@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler //若指定ResultSetHandler ,这里则能强转为ResultSetHandler ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget(); // 获取参数对像,即 mapper 中 paramsType 的实例 Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject"); parameterField.setAccessible(true); // 取出参数 // sname -> abc pararm1 -> abc Object parameterObject = parameterField.get(parameterHandler); // Class<ParameterHandler> handlerClass = ParameterHandler.class; Field mappedStatementFiled = parameterHandler.getClass().getDeclaredField("mappedStatement"); mappedStatementFiled.setAccessible(true); MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler); // 方法全限定类名 com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper.getInfoByName String methodFullClassName = mappedStatement.getId(); // 获取方法所在的类对象,这里是com.whitebrocade.jasyptdemo.demos.mapper.StudentMapper String mapperClassName = methodFullClassName.substring(0, methodFullClassName.lastIndexOf('.')); Class<?> mapperClass = Class.forName(mapperClassName); // 简单方法名 getInfoByName String methodSimpleName = methodFullClassName.substring(methodFullClassName.lastIndexOf('.') + 1); // 通过方法名找到指定的Method Method[] methods = mapperClass.getDeclaredMethods(); Method method = null; for (Method m : methods) { if (m.getName().equals(methodSimpleName)) { method = m; break; } } // 找到@EncryptTransaction的Mapper方法 List<String> paramNames = null; if (ObjUtil.isNotNull(method)) { // 获取参数上的所有注解 Annotation[][] pa = method.getParameterAnnotations(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < pa.length; i++) { for (Annotation annotation : pa[i]) { if (paramNames == null) { paramNames = new ArrayList<>(); } if (annotation instanceof EncryptTransaction) { // 如果参数有@EncryptTransaction注解,则将参数名添加到集合中 paramNames.add(parameters[i].getName()); } // 如果有@Param注解,则将参数名添加到集合中 if (annotation instanceof Param) { paramNames.add(parameters[i].getName()); continue; } } } } // 外界传入参数不为空 if (ObjUtil.isNotNull(parameterObject)) { String entityClassName = null; // 之所以要分成几种类型,是因为查看通过返回值获取类型,增改可以传递的实体类获取类型,而删除传递为id, 返回值也不是我们所需要的 // 查询类型 if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) { // 获取实体类的类名 // com.whitebrocade.jasyptdemo.demos.domain.StudentInfo entityClassName = mappedStatement.getResultMaps().get(0).getType().getName(); } else if(mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT) || mappedStatement.getSqlCommandType().equals(SqlCommandType.UPDATE)) { // 增,改都是获取注解上的类型 Annotation[][] pa = method.getParameterAnnotations(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < pa.length; i++) { for (Annotation annotation : pa[i]) { // 只有@EncryptTransaction注解的参数,才会被加密 if (annotation instanceof EncryptTransaction) { entityClassName = parameters[i].getType().getTypeName(); } } } } else if (mappedStatement.getSqlCommandType().equals(SqlCommandType.DELETE)) { // 通常来说,都是根据id删除,并且id类型都是int, long为主 // 直接放行 return invocation.proceed(); } Class<?> entityClass = Class.forName(entityClassName); // 对类字段进行加密 // 校验该实例的类是否被@SensitiveData所注解 SensitiveData sensitiveData = AnnotationUtil.getAnnotation(entityClass, SensitiveData.class); if (ObjUtil.isNotNull(sensitiveData)) { //取出当前当前类所有字段,传入加密方法 Field[] declaredFields = entityClass.getDeclaredFields(); // 对外界参数进行加密 parameterObject = this.encrypt(declaredFields, parameterObject); } // 将加密后的参数代替原来的参数 if (CollUtil.isNotEmpty(paramNames)) { // 反射获取 BoundSql 对象,此对象包含生成的sql和sql的参数map映射 Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql"); boundSqlField.setAccessible(true); PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0]; // 改写的参数设置到原parameterHandler对象 parameterField.set(parameterHandler, parameterObject); parameterHandler.setParameters(ps); } } // 执行查询 return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 加密 * @param declaredFields 对象的字段 * @param paramsObject Mybatis传入参数 * @return 加密后的对象 */ private Object encrypt(Field[] declaredFields, Object paramsObject) { // 取出所有被EncryptTransaction注解的字段 for (Field field : declaredFields) { EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); // 字段名 String paramName = field.getName(); Object obj = null; Map<String, Object> map = null; if (paramsObject instanceof String) { // 表示只传有一个参数 obj = (String) paramsObject; } else if (paramsObject instanceof Map) { map = (Map<String, Object>) paramsObject; // 获取该字段对应的参数,非空就跳过 obj = map.get(paramName); } else { // 如果是具体的实体对象,就转换成map map = BeanUtil.beanToMap(paramsObject); // 获取该字段对应的参数 obj = map.get(paramName); } // 为空跳过 if (Objects.isNull(obj)) { continue; } // 字段类型 Class<?> paramClass = field.getType(); // 暂时只实现String类型的加密 // 如果字段类型是字符串,且传入参数是类型, 那么就转换成字符串 if (paramClass == String.class && obj instanceof String) { String value = (String) obj; //加密 try { // 加密 String encryptStr = encoder.encrypt(value); if (paramsObject instanceof String) { paramsObject = encryptStr; return encryptStr; } else if (paramsObject instanceof Map) { map.put(paramName, encryptStr); } else { // 实体类对象 map.put(paramName, encryptStr); paramsObject = BeanUtil.toBean(map, paramsObject.getClass()); } } catch (Exception e) { log.error("加密错误", e); throw new RuntimeException("加密错误", e); } } } } return paramsObject; } }
import cn.hutool.core.util.ObjUtil; import com.whitebrocade.jasyptdemo.demos.anno.EncryptTransaction; import com.whitebrocade.jasyptdemo.demos.anno.SensitiveData; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.lang.reflect.Field; import java.sql.Statement; import java.util.ArrayList; import java.util.Objects; /** * 解密拦截 */ @Slf4j @Component @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class DecryInterceptor implements Interceptor { @Autowired private Encoder encoder; @Override public Object intercept(Invocation invocation) throws Throwable { // 取出查询的结果 Object resultObject = invocation.proceed(); if (Objects.isNull(resultObject)) { return null; } // 基于selectList if (resultObject instanceof ArrayList) { @SuppressWarnings("unchecked") ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject; if (! CollectionUtils.isEmpty(resultList) && this.needToDecrypt(resultList.get(0))) { for (Object result : resultList) { //逐一解密 this.decrypt(result); } } // 基于selectOne } else { if (this.needToDecrypt(resultObject)) { this.decrypt(resultObject); } } return resultObject; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 是否需要加密,通过判断实体类是否添加@SensitiveData注解 * @param object 实体类 * @return 有添加@SensitiveData注解返回true, 没有返回false */ private boolean needToDecrypt(Object object) { Class<?> objectClass = object.getClass(); SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class); return ObjUtil.isNotNull(sensitiveData); } /** * 解密 * @param result 要解密的对象 * @return 解密后的对象 * @param <T> 对象的类型 * @throws IllegalAccessException */ private <T> T decrypt(T result) throws IllegalAccessException { //取出resultType的类 Class<?> resultClass = result.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { // 取出所有被EncryptTransaction注解的字段 EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(result); // String的解密 if (object instanceof String) { String value = (String) object; // 对注解的字段进行逐一解密 try { String decryptStr = encoder.decrypt(value); field.set(result, decryptStr); } catch (Exception e) { log.error("解密失败", e); throw new RuntimeException("解密失败"); } } } } return result; } }
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; } }
@RestController public class MyTestController { @Autowired private StudentMapper studentMapper; @ResponseBody @RequestMapping("/getInfo") public void getInfo(@Param("id") String id) { StudentInfo stu = studentMapper.getInfo(id); System.out.println("stu = " + stu); } // http://localhost:8080/test5?sname=tom @ResponseBody @RequestMapping("/getInfoByName") public StudentInfo getInfoByName(@Param("sname") String sname) { StudentInfo stu = studentMapper.getInfoByName(sname); System.out.println("stu = " + stu); return stu; } @ResponseBody @PostMapping("/insertInfo") public StudentInfo insertInfo(@RequestBody StudentInfo studentInfo) { studentMapper.insertInfo(studentInfo); return studentInfo; } @ResponseBody @PostMapping("/updateById") public StudentInfo updateById(@RequestBody StudentInfo studentInfo) { studentMapper.updateById(studentInfo); return studentInfo; } @ResponseBody @GetMapping("/selectAll") public List<StudentInfo> selectAll() { return studentMapper.selectAll(); } @ResponseBody @DeleteMapping("/deleteById") public void deleteById(int id) { studentMapper.deleteById(id); } }
需要注意的是,上述代码中不要引入Mybatis-plus,还未适配
再补充一下,既然我们直接将盐值,密钥等写入yaml中不安全,那么我们就可以借助之前的jasypt对这些信息进行加密,也就实现了密钥轮替,安全性提高了
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/Shanghai username: root # ENC中的值是可以不断替换的 password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD) mybatis: mapper-locations: classpath:mapper/*.xml # application.yml logging: level: com.whitebrocade.jasyptdemo.demos: debug # ----------------- # Mybatis的脱敏加密配置 whitebrocade: crypto: secret-key: ENC(pKsZAaYDoBw2UaTS4/1R06LFavC/qlQjgb2eM3d2dVs=) algorithm: ENC(f8muaLy4uX7/X3mG6rOwTg==) # 这里的password建议外部传入 jasypt: encryptor: password: whitebrocade
效果如下, 正常查询能显示
实际中数据库就是加密了
数据源配置:是指DataSource的配置。
加密器配置:是指使用什么加密策略进行加解密。目前ShardingSphere内置了两种加解密策略:AES/MD5。用户还可以通过实现ShardingSphere提供的接口,自行实现一套加解密算法
脱敏表配置:用于告诉ShardingSphere数据表里哪个列用于存储密文数据(cipherColumn)、哪个列用于存储明文数据(plainColumn)以及用户想使用哪个列进行SQL编写(logicColumn)
查询属性的配置:当底层数据库表里同时存储了明文数据、密文数据后,该属性开关用于决定是直接查询数据库表里的明文数据进行返回,还是查询密文数据通过Encrypt-JDBC解密后返回。
新增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
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;
}
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); }
spring: # 分库分表下的脱敏 shardingsphere: datasource: names: demo demo: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1.101:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai username: root # ENC中的值是可以不断替换的 password: ENC(G2avJvQM9TRcath/6SjtSl2J1gYeySQD) encrypt: encryptors: my-encryptor: # 加密算法类型 type: CustomEncryptor # type: CustomQueryAssistedEncryptor # 要加密的表 tables: t_user: columns: password: # 真实列 plain-column: password # 加密列 cipher-column: password_encrypt # 辅助查询列 # assisted-query-column: password_assisted # 加密算法 encryptor: my-encryptor # 查询是否使用密文列 ture显示cipher-column false显示plain-column props: query.with.cipher.column: true # 加密配置 whitebrocade: crypto: # 密钥,16/24/32字节 secret-key: ENC(pKsZAaYDoBw2UaTS4/1R06LFavC/qlQjgb2eM3d2dVs=) algorithm: ENC(f8muaLy4uX7/X3mG6rOwTg==) # Mybatis XML配置 mybatis: mapper-locations: classpath:mapper/*.xml # application.yml logging: level: com.whitebrocade.jasyptdemo.demos: debug # 加密 jasypt: encryptor: password: whitebrocade
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; } }
import cn.hutool.core.util.ObjUtil; import cn.hutool.extra.spring.SpringUtil; import com.whitebrocade.jasyptdemo.demos.service.Encoder; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.encrypt.strategy.spi.Encryptor; import java.util.Properties; /** * 该种加密方式特点: 相同数据存储内容一样 */ @Slf4j @Getter @Setter public class CustomEncryptor implements Encryptor { /** * 加密器, 这里无法通过@Autowired注入, 通过工具类获取Bean对象进行初始化 */ private Encoder encoder; /** * 算法策略类型 */ private static final String TYPE = "CustomEncryptor"; private Properties properties = new Properties(); @Override public void init() { Encoder tmepEncoder = SpringUtil.getBean(Encoder.class); if (ObjUtil.isNull(tmepEncoder)) { log.error("Spring容器中不存在Encoder类型的Bean"); throw new RuntimeException("Spring容器中不存在Encoder类型的Bean"); } encoder = tmepEncoder; } /** * 加密 * @param plaintext 需要加密的数据 * @return 加密后的数据 */ @Override public String encrypt(Object plaintext) { if (ObjUtil.isNull(plaintext)) { return null; } return encoder.encrypt(String.valueOf(plaintext)); } /** * 解密 * @param ciphertext 需要解密的数据 * @return 解密后的数据 */ @Override public Object decrypt(String ciphertext) { if (ObjUtil.isNull(ciphertext)) { return null; } return encoder.decrypt(ciphertext); } /** * 返回所使用的加密算法,后续配置文件中填写这个算法名 */ @Override public String getType() { return TYPE; } @Override public void setProperties(Properties properties) { } }
cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestAlgorithm; import cn.hutool.crypto.digest.Digester; import cn.hutool.extra.spring.SpringUtil; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.encrypt.strategy.spi.QueryAssistedEncryptor; import java.util.Properties; /** * 该种加密方式特点: 相同数据存储会变化 */ @Slf4j @Getter @Setter public class CustomQueryAssistedEncryptor implements QueryAssistedEncryptor { /** * 加密器, 这里无法通过@Autowired注入, 通过工具类获取Bean对象进行初始化 */ private Encoder encoder; /** * 摘要器 */ private static final Digester digester = new Digester(DigestAlgorithm.SHA256); /** * 算法策略类型 */ private static final String TYPE = "CustomQueryAssistedEncryptor"; /** * 随机种子长度 */ private static final int seedLength = String.valueOf(System.currentTimeMillis()).length(); private Properties properties = new Properties(); /** * 初始化加密要用的Encoder */ @Override public void init() { // 初始化Encoder Encoder tmepEncoder = SpringUtil.getBean(Encoder.class); if (ObjUtil.isNull(tmepEncoder)) { log.error("Spring容器中不存在Encoder类型的Bean"); throw new RuntimeException("Spring容器中不存在Encoder类型的Bean"); } encoder = tmepEncoder; } /** * 辅助查询列 * @param plaintext plaintext 辅助查询列对象 * @return 摘要时候的字符串 */ @Override public String queryAssistedEncrypt(String plaintext) { if (ObjUtil.isNull(plaintext)) { return null; } String digestHexStr = digester.digestHex(plaintext); return digestHexStr; } /** * 加密 * @param plaintext 需要加密的数据 * @return 加密后的数据 */ @Override public String encrypt(Object plaintext) { if (ObjUtil.isNull(plaintext)) { return null; } // 原始字符串 + 随机因子(这里采用时间戳) plaintext = plaintext + String.valueOf(System.currentTimeMillis()); String encryptStr = encoder.encrypt(String.valueOf(plaintext)); return encryptStr; } /** * 解密 * @param ciphertext 需要解密的数据 * @return 解密后的数据 */ @Override public Object decrypt(String ciphertext) { if (ObjUtil.isNull(ciphertext)) { return null; } String decryptStr = encoder.decrypt(ciphertext); String rawStr = StrUtil.sub(decryptStr, 0, decryptStr.length() - seedLength); return rawStr; } /** * 返回所使用的加密算法,后续配置文件中填写这个算法名 */ @Override public String getType() { return TYPE; } @Override public void setProperties(Properties properties) { } }
import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; import java.util.List; @Slf4j @SpringBootTest class JasyptDemoApplicationTests { @Resource private UserMapper userMapper; @Test void insertUser() { UserEntity userEntity = new UserEntity(); userEntity.setUserId(1); userEntity.setUserName("tom"); userEntity.setPassword("123456"); userMapper.insertUser(userEntity); } @Test void insertUser2() { UserEntity userEntity = new UserEntity(); userEntity.setUserId(1); userEntity.setUserName("tom"); userEntity.setPassword("123456"); userMapper.insertUser(userEntity); userEntity.setUserId(2); userMapper.insertUser(userEntity); } @Test void getUserInfo() { List<UserEntity> userEntityList = userMapper.getUserInfo("tom", "123456"); userEntityList.forEach(System.out::println); } }
清空t_user表
修改yaml配置
执行inserter()方法, 发现MySQL中新增数据
执行getUserInfo, 发现解密成功
清空t_user表
修改yaml配置
执行inserter()2方法, 发现MySQL中新增2条数据(注意这里执行的是inster2方法), 并且即是密码都是123456,但是加密后字符串是不一样的
执行getUserInfo, 发现解密成功
加班加点补充中,
原理是分词密文映射表
分词密文映射表
新建一张分词密文映射表,在敏感字段数据新增、修改的后,对敏感字段进行分词组合,再对每个分词进行加密,建立起敏感字段的分词密文与目标数据行主键的关联关系;在处理模糊查询的时候,对模糊查询关键字进行加密,用加密后的模糊查询关键字,对分词密文映射表进行like查询,得到目标数据行的主键,再以目标数据行的主键为条件返回目标表进行精确查询
数据脱敏 :: ShardingSphere (apache.org)
MyBatis 核心配置综述之 ResultSetHandler
MyBatis 核心配置综述之StatementHandler
Springboot 配置文件、隐私数据脱敏的最佳实践(原理+源码)
加密后的敏感字段还能进行模糊查询吗?该如何实现?_加密后的敏感字段还能进行模糊查询吗?该如何实现?
求求你别乱脱敏了!MyBatis 插件 + 注解轻松实现数据脱敏,So easy~! - Java技术栈
Apache ShardingSphere数据脱敏全解决方案详解(上)
ShardingSphere4.1.1:Sharding-JDBC数据加密及SPI加密策略实现
Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。