五种SpringBoot实现数据加密存储的方式总结

 更新时间:2023年11月08日 16:16:52   作者:庄周de蝴蝶  
这篇文章主要为大家详细介绍了五种常见数据加密存储的方法(结合SpringBoot和MyBatisPlus框架进行实现),文中的示例代码讲解详细,需要的可以参考下

前言

最近由于项目需要做等保,其中有一项要求是系统中的个人信息和业务信息需要进行加密存储。经过一番搜索,最终总结出了五种数据加密存储的方法(结合SpringBootMyBatisPlus框架进行实现),不知道家人们在项目中使用的是哪种方式,如果有更好地方式也欢迎一起交流~~~,本文所贴出的完整代码已上传到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;
    }
​
}

然后定义注解,这里通过使用JacksonJacksonAnnotationsInside注解将序列化和反序列化合并,这样在使用时就可以只使用一个注解:

/**
 * 字段加解密序列化注解
 *
 * @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数据加密存储的资料请关注脚本之家其它相关文章!

相关文章

  • java使用内存数据库ssdb的步骤

    java使用内存数据库ssdb的步骤

    这篇文章主要介绍了java使用内存数据库ssdb的步骤,帮助大家更好的理解和使用Java,感兴趣的朋友可以了解下
    2020-12-12
  • java实现俄罗斯方块游戏

    java实现俄罗斯方块游戏

    这篇文章主要为大家详细介绍了java实现俄罗斯方块游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-02-02
  • Springboot如何连接远程服务器上的数据库实践

    Springboot如何连接远程服务器上的数据库实践

    本文主要介绍了Springboot如何连接远程服务器上的数据库实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • mybatis 传入null值的解决方案

    mybatis 传入null值的解决方案

    这篇文章主要介绍了mybatis 传入null值的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • Java泛型的类型擦除示例详解

    Java泛型的类型擦除示例详解

    Java泛型(Generic)的引入加强了参数类型的安全性,减少了类型的转换,但有一点需要注意,Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类型在编译后都会被清除掉,这篇文章主要给大家介绍了关于Java泛型的类型擦除的相关资料,需要的朋友可以参考下
    2021-07-07
  • Spring中Bean创建完后打印语句的两种方法

    Spring中Bean创建完后打印语句的两种方法

    这篇文章主要介绍了Spring中Bean创建完后打印语句的两种方法,一个是实现InitializingBean接口,另一个使用@Bean注解和initMethod属性,通过代码示例介绍的非常详细,感兴趣的小伙伴可以参考阅读
    2023-07-07
  • Java中创建线程的四种方法解析

    Java中创建线程的四种方法解析

    这篇文章主要介绍了Java中创建线程的四种方法解析,线程是Java编程语言中的一个重要概念,它允许程序在同一时间执行多个任务,线程是程序中的执行路径,可以同时执行多个线程,每个线程都有自己的执行流程,需要的朋友可以参考下
    2023-10-10
  • Java的函数方法详解(含汉诺塔问题)

    Java的函数方法详解(含汉诺塔问题)

    汉诺塔问题是一个经典的递归问题,下面这篇文章主要给大家介绍了关于Java函数方法(含汉诺塔问题)的相关资料,文中通过图文以及代码示例介绍的非常详细,需要的朋友可以参考下
    2023-11-11
  • Stream distinct根据list某个字段去重的解决方案

    Stream distinct根据list某个字段去重的解决方案

    这篇文章主要介绍了Stream distinct根据list某个字段去重,stream的distinct去重方法,是根据 Object.equals,和 Object.hashCode这两个方法来判断是否重复的,本文给大家介绍的非常详细,需要的朋友可以参考下
    2023-05-05
  • SpringBoot发送html邮箱验证码功能

    SpringBoot发送html邮箱验证码功能

    这篇文章主要介绍了SpringBoot发送html邮箱验证码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12

最新评论