五种SpringBoot实现数据加密存储的方式总结
作者:庄周de蝴蝶
前言
最近由于项目需要做等保,其中有一项要求是系统中的个人信息和业务信息需要进行加密存储。经过一番搜索,最终总结出了五种数据加密存储的方法(结合SpringBoot
和MyBatisPlus
框架进行实现),不知道家人们在项目中使用的是哪种方式,如果有更好地方式也欢迎一起交流~~~,本文所贴出的完整代码已上传到GitHub。
思路总览
在具体讲解实现方式之前,先讲一下五种方式的思路:
1.手动处理字段加解密
最简单、朴素的方式。如果项目中只有个别字段,例如密码字段需要加密,则可以使用这种方法。不过,通常密码都是做单向 Hash 加密,不存在解密的情况,本文后续为了统一讲解加解密的方式,就对字段统一使用了 AES 对称加密算法。听说有的项目需要使用 SM2 之类非对称加密算法,本文就不再介绍了,只需要参考思路替换相应加解密调用的方法即可。
优点:使用简单、易懂,技术难度低
缺点:全手工处理,容易遗漏,费时。
2.注解结合 AOP 实现
相对简便的方式,在需要进行加解密处理的字段上添加字段注解,然后在有加解密处理需求的方法上添加方法注解,之后结合 AOP ,对入参和返回结果进行处理即可。
优点:使用相对简单,加解密处理统一在切面处理中完成。
缺点:只能处理入参和返回结果中的字段加解密,如果处理逻辑中涉及到加解密需求还是需要手动处理。同时需要在所有有加解密处理需求的类或方法(可以定义类级别和方法级别的注解,本文只讲解方法级别的注解)上添加注解,也容易遗漏,测试时需要特别注意。
3.自定义序列化 / 反序列化类结合注解实现
通过自定义序列化 / 反序列化处理类,然后结合序列化 / 反序列化注解中指定相应类进行实现。
优点:使用简单,加解密处理统一在自定义的序列化化类中完成,只需要在字段上添加注解。
缺点:只能处理序列化数据中的加解密,如果业务逻辑中需要手动设置某个加密字段的值,还是需要手工处理。
4.MybatisPlus
自定义TypeHandler
实现
和自定义序列化 / 反序列化类的思路类似,不过是和框架功能相耦合,通过使用MybatisPlus
自定义TypeHandler
实现。
优点:使用简单,加解密处理统一在自定义的TypeHandler
中完成,只需要在字段上添加注解。
缺点:只能处理 SQL 的查询和保存的结果,如果业务逻辑中需要手动设置某个加密字段的值,还是需要手工处理,如果存在自定义 SQL ,还需要额外添加注解处理,与框架绑定。
5.MybatisPlus自定义拦截器实现
相对底层的方式,结合框架自带的拦截器功能,通过对 SQL 拼接和处理 SQL 查询结果进行实现。
优点:使用简单,加解密处理统一在自定义的拦截器中完成,无需使用注解。
缺点:只能处理 SQL 的查询和保存的结果,如果业务逻辑中需要手动设置某个加密字段的值,还是需要手工处理,此外还需要整理所有需要加解密操作的字段名,与框架绑定。
小结:当然除了以上几种方法,根据使用技术和框架的不同,还有很多种方式,例如 JPA 可以自定义 Convert,类似方法 3 和 4,这里不再介绍。其实,在实现的过程中,有想过通过修改字节码,修改字段的 Getter / Setter 方法进行实现,但是在实现的时候才发现两者的操作是成对的,入库的时候也就还是明文,不过如果是数据脱敏,由于只需要修改 Setter 方法,则可以考虑使用修改字节码的方式。
具体实现
手工处理
话不多说,直接上代码:
public User method1(User user) { Long userId = 1L; user.setId(userId); user.setUsername(AESUtils.encrypt(user.getUsername())); user.setPassword(AESUtils.encrypt(user.getPassword())); saveOrUpdate(user); User resultUser = getById(userId); resultUser.setUsername(AESUtils.decrypt(resultUser.getUsername())); resultUser.setPassword(AESUtils.decrypt(resultUser.getPassword())); return resultUser; }
这个就不再做详细解释了~~~
注解结合 AOP
首先需要定义一个字段注解和方法注解:
/** * 字段加密注解 * * @author 庄周de蝴蝶 * @date 2023-11-07 */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptField { }
/** * 方法加密处理注解 * * @author 庄周de蝴蝶 * @date 2023-11-07 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Order(Ordered.HIGHEST_PRECEDENCE) public @interface EncryptMethod { /** * 需要处理对象在参数列表中的位置 */ int[] value() default { 0 }; /** * 是否启用解密处理 */ boolean enableDecrypt() default true; }
然后结合 AOP 去处理方法注解:
/** * 处理加密注解切面 * 特别注意, 这里的排序需要 + 1, 否则会报错, 具体原因参考链接: * <a href="https://blog.csdn.net/qq_18300037/article/details/128626005">...</a> * * @author 庄周de蝴蝶 * @date 2023-11-07 */ @Slf4j @Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE + 1) public class EncryptMethodAspect { /** * 处理加密方法注解 * * @param joinPoint 切点 * @param encryptMethod 加密方法注解 * @return 结果 */ @Around("@annotation(encryptMethod)") public Object around(ProceedingJoinPoint joinPoint, EncryptMethod encryptMethod) throws Throwable { try { int[] indexArr = encryptMethod.value(); Object[] args = joinPoint.getArgs(); for (int i = 0; i < indexArr.length; i++) { if (i >= args.length) { break; } // 处理入参中的加密 handleEncrypt(args[i]); } Object result = joinPoint.proceed(); if (encryptMethod.enableDecrypt()) { // 对返回结果中的字段进行解密处理 return handleDecrypt(result); } return result; } catch (Throwable throwable) { log.error("加密注解处理出现异常", throwable); throw throwable; } } /** * 对添加了 EncryptField 注解的字段进行加密 * * @param obj 要处理的对象 */ private void handleEncrypt(Object obj) throws IllegalAccessException { handleEnDecrypt(obj, AESUtils::encrypt); } /** * 对添加了 EncryptField 注解的字段进行解密, <b>只考虑了返回值是对象的情况</b> * * @param obj 要处理的对象 * @return 结果 */ private Object handleDecrypt(Object obj) throws IllegalAccessException { return handleEnDecrypt(obj, AESUtils::decrypt); } /** * 对添加了 EncryptField 注解的字段进行加解密处理 * * @param obj 要处理的对象 * @param handleFun 处理函数 * @return 结果 */ private Object handleEnDecrypt(Object obj, UnaryOperator<String> handleFun) throws IllegalAccessException { if (obj == null) { return null; } Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { boolean hasSecureField = field.isAnnotationPresent(EncryptField.class); if (hasSecureField) { field.setAccessible(true); String val = (String) field.get(obj); String result = handleFun.apply(val); field.set(obj, result); } } return obj; } }
这里只考虑了返回结果是实体对象的情况,如果返回类型的是分页或者是列表亦或者是类似Result的形式,需要自己进行额外的处理。
最后是使用的方式,首先是在实体的字段上添加注解:
/** * 用户类 * * @author 庄周de蝴蝶 * @date 2023-10-27 */ @Data @TableName("user") public class User { /** * id */ @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; /** * 用户名 */ @EncryptField private String username; /** * 密码 */ @EncryptField private String password; }
然后是在方法上添加注解,这里是在控制层使用,当然也可以在ServiceImpl
里的方法上使用:
@EncryptMethod @PostMapping("/method2") public User method2(@RequestBody User user) { return userService.method2(user); }
自定义序列化注解
首先需要实现序列化 / 反序列化处理类:
/** * 解密序列化处理器 * * @author 庄周de蝴蝶 * @date 2023-11-07 */ @NoArgsConstructor public class DecryptSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { if (StringUtils.isNotBlank(value)) { value = AESUtils.decrypt(value); } jsonGenerator.writeString(value); } }
/** * 加密序列化处理器 * * @author 庄周de蝴蝶 * @date 2023-2023-11-07 */ @NoArgsConstructor public class EncryptDeserializer extends JsonDeserializer<String> { @Override public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { if (jsonParser != null && StringUtils.isNotBlank(jsonParser.getText())) { String text = jsonParser.getText(); return AESUtils.encrypt(text); } return null; } }
然后定义注解,这里通过使用Jackson
的JacksonAnnotationsInside
注解将序列化和反序列化合并,这样在使用时就可以只使用一个注解:
/** * 字段加解密序列化注解 * * @author 庄周de蝴蝶 * @date 2023-10-23 */ @JsonSerialize(using = DecryptSerializer.class) @JsonDeserialize(using = EncryptDeserializer.class) @JacksonAnnotationsInside @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptSerializer { }
最后在相应的实体字段中添加注解即可:
/** * 用户类 * * @author 庄周de蝴蝶 * @date 2023-10-27 */ @Data @TableName("user") public class User { /** * id */ @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; /** * 用户名 */ @EncryptSerializer private String username; /** * 密码 */ @EncryptSerializer private String password; }
MybatisPlus自定义TypeHandler
首先是自定义的TypeHandler
:
/** * 加密类型字段处理类 * * @author 庄周de蝴蝶 * @date 2023-10-27 */ public class EncryptTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, handleResult(parameter, AESUtils::encrypt)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return handleResult(rs.getString(columnName), AESUtils::decrypt); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return handleResult(rs.getString(columnIndex), AESUtils::decrypt); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return handleResult(cs.getString(columnIndex), AESUtils::decrypt); } /** * 值加解密处理 * * @param val 值 * @param fun 处理函数 * @return 结果 */ private String handleResult(String val, UnaryOperator<String> fun) { HttpServletRequest request = ServletUtils.getRequest(); return StringUtils.isBlank(val) ? val : fun.apply(val); } }
然后在相应的实体字段中添加注解即可:
/** * 用户类 * * @author 庄周de蝴蝶 * @date 2023-10-27 */ @Data @TableName("user") public class User { /** * id */ @TableId(value = "id", type = IdType.ASSIGN_ID) private Long id; /** * 用户名 */ @TableField(typeHandler = EncryptTypeHandler.class) private String username; /** * 密码 */ @TableField(typeHandler = EncryptTypeHandler.class) private String password; }
MybatisPlus自定义拦截器
首先是定义一个保存操作的拦截器,关于拦截器的使用,由于和框架相关联,这里不再详细介绍使用方式:
/** * 加密更新拦截器处理 * * @author 庄周de蝴蝶 * @date 2023-10-27 */ @Configuration public class EncryptUpdateInterceptor implements InnerInterceptor { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new EncryptUpdateInterceptor()); return interceptor; } @Override public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) { SQLUtils.handleSql(ms.getConfiguration(), ms.getBoundSql(parameter)); } }
然后再定义一个查询操作的拦截器:
/** * 加密查询拦截器处理 * * @author 庄周de蝴蝶 * @date 2023-11-08 */ @Component @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), }) public class EncryptQueryInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; String sqlId = mappedStatement.getId(); if (sqlId.contains("selectCount")) { return invocation.proceed(); } Object proceed = invocation.proceed(); @SuppressWarnings("unchecked") List<Object> objectList = (List<Object>) proceed; if (objectList.isEmpty()) { return proceed; } Class<?> type = objectList.get(0).getClass(); List<Object> resultList = new ArrayList<>(); for (Object o : objectList) { Map<String, Object> map = JSONUtil.toBean(JSONUtil.toJsonStr(o), new TypeReference<Map<String, Object>>() {}, true); for (String keyword : SQLUtils.ENCRYPT_SET) { map.put(keyword, AESUtils.decrypt(String.valueOf(map.get(keyword)))); } resultList.add(JSONUtil.toBean(JSONUtil.toJsonStr(map), type)); } return resultList; } }
其中SQLUtils
的内容如下:
/** * sql 工具类 * * @author 庄周de蝴蝶 * @date 2023-11-08 */ public class SQLUtils { public static final Set<String> ENCRYPT_SET = new HashSet<>(Arrays.asList("username", "password")); private SQLUtils() {} /** * 处理 sql * * @param configuration 配置 * @param boundSql sql */ public static void handleSql(Configuration configuration, BoundSql boundSql) { Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings.isEmpty() || parameterObject == null) { return; } MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty().toLowerCase(); Object value = metaObject.getValue(propertyName); if (ENCRYPT_SET.contains(propertyName.substring(propertyName.indexOf(".") + 1))) { metaObject.setValue(propertyName, AESUtils.encrypt(String.valueOf(value))); } } } }
以上就是五种SpringBoot实现数据加密存储的方式总结的详细内容,更多关于SpringBoot数据加密存储的资料请关注脚本之家其它相关文章!