五种实现数据加密存储的方式,你选择哪一种

前言

最近由于项目需要做等保,其中有一项要求是系统中的个人信息和业务信息需要进行加密存储。经过一番搜索,最终总结出了五种数据加密存储的方法(结合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 方法,则可以考虑使用修改字节码的方式。

具体实现

手工处理

话不多说,直接上代码:

java 复制代码
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

首先需要定义一个字段注解和方法注解:

java 复制代码
/**
 * 字段加密注解
 *
 * @author 庄周de蝴蝶
 * @date 2023-11-07
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
}
java 复制代码
/**
 * 方法加密处理注解
 *
 * @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 去处理方法注解:

java 复制代码
/**
 * 处理加密注解切面
 * 特别注意, 这里的排序需要 + 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的形式,需要自己进行额外的处理。

最后是使用的方式,首先是在实体的字段上添加注解:

java 复制代码
/**
 * 用户类
 *
 * @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里的方法上使用:

java 复制代码
@EncryptMethod
@PostMapping("/method2")
public User method2(@RequestBody User user) {
    return userService.method2(user);
}

自定义序列化注解

首先需要实现序列化 / 反序列化处理类:

java 复制代码
/**
 * 解密序列化处理器
 *
 * @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);
    }
​
}
java 复制代码
/**
 * 加密序列化处理器
 *
 * @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注解将序列化和反序列化合并,这样在使用时就可以只使用一个注解:

java 复制代码
/**
 * 字段加解密序列化注解
 *
 * @author 庄周de蝴蝶
 * @date 2023-10-23
 */
@JsonSerialize(using = DecryptSerializer.class)
@JsonDeserialize(using = EncryptDeserializer.class)
@JacksonAnnotationsInside
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptSerializer {
}

最后在相应的实体字段中添加注解即可:

java 复制代码
/**
 * 用户类
 *
 * @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

java 复制代码
/**
 * 加密类型字段处理类
 *
 * @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);
    }
    
}

然后在相应的实体字段中添加注解即可:

java 复制代码
/**
 * 用户类
 *
 * @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自定义拦截器

首先是定义一个保存操作的拦截器,关于拦截器的使用,由于和框架相关联,这里不再详细介绍使用方式:

java 复制代码
/**
 * 加密更新拦截器处理
 *
 * @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));
    }
​
}

然后再定义一个查询操作的拦截器:

java 复制代码
/**
 * 加密查询拦截器处理
 *
 * @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的内容如下:

java 复制代码
/**
 * 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)));
            }
        }
    }
    
}

总结

以上简单介绍了五种数据加密存储的方式和实际编码实现,虽然都经过简单测试,不过如果在项目中使用,还是建议多对相关接口和方法进行测试。如果你有其它的思路和方法,也欢迎一起交流~~~

相关推荐
程序员岳焱8 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
爱编程的喵8 分钟前
深入理解JavaScript原型机制:从Java到JS的面向对象编程之路
java·前端·javascript
龚思凯14 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响17 分钟前
枚举在实际开发中的使用小Tips
后端
on the way 12319 分钟前
行为型设计模式之Mediator(中介者)
java·设计模式·中介者模式
保持学习ing22 分钟前
Spring注解开发
java·深度学习·spring·框架
wuhunyu22 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi23 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
酷爱码26 分钟前
Spring Boot 整合 Apache Flink 的详细过程
spring boot·flink·apache
异常君1 小时前
Spring 中的 FactoryBean 与 BeanFactory:核心概念深度解析
java·spring·面试