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


    前言

    最近由于项目需要做等保,其中有一项要求是系统中的个人信息和业务信息需要进行加密存储。经过一番搜索,最终总结出了五种数据加密存储的方法(结合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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个就不再做详细解释了~~~

    注解结合 AOP

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

    /**
     * 字段加密注解
     *
     * @author 庄周de蝴蝶
     * @date 2023-11-07
     */
    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EncryptField {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    /**
     * 方法加密处理注解
     *
     * @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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    然后结合 AOP 去处理方法注解:

    /**
     * 处理加密注解切面
     * 特别注意, 这里的排序需要 + 1, 否则会报错, 具体原因参考链接: 
     * ...
     *
     * @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 注解的字段进行解密, 只考虑了返回值是对象的情况
    	 *
    	 * @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;
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    这里只考虑了返回结果是实体对象的情况,如果返回类型的是分页或者是列表亦或者是类似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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    然后是在方法上添加注解,这里是在控制层使用,当然也可以在ServiceImpl里的方法上使用:

    @EncryptMethod
    @PostMapping("/method2")
    public User method2(@RequestBody User user) {
    	return userService.method2(user);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    自定义序列化注解

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

    /**
     * 解密序列化处理器
     *
     * @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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    /**
     * 加密序列化处理器
     *
     * @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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    然后定义注解,这里通过使用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 {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    /**
     * 用户类
     *
     * @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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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);
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

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

    /**
     * 用户类
     *
     * @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;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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));
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    /**
     * 加密查询拦截器处理
     *
     * @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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    其中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)));
                }
            }
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    总结

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

  • 相关阅读:
    Linux之Shell(二)
    J9数字论:以太坊合并?以太坊合并又会带来哪些影响?
    5. 文档的字段映射
    Linux组管理和权限管理
    这么有料的福利,你还不赶紧关注一波吗?(代码合集)
    负载均衡算法介绍及应用连接池负载不均问题分析
    电源出口到国外需要做哪些检测认证?
    springboot启动流程是什么?
    算法day26
    java 字符串初始化=“” 和=null的区别
  • 原文地址:https://blog.csdn.net/qq_41698074/article/details/134292110