五種SpringBoot實(shí)現(xiàn)數(shù)據(jù)加密存儲(chǔ)的方式總結(jié)
前言
最近由于項(xiàng)目需要做等保,其中有一項(xiàng)要求是系統(tǒng)中的個(gè)人信息和業(yè)務(wù)信息需要進(jìn)行加密存儲(chǔ)。經(jīng)過(guò)一番搜索,最終總結(jié)出了五種數(shù)據(jù)加密存儲(chǔ)的方法(結(jié)合SpringBoot
和MyBatisPlus
框架進(jìn)行實(shí)現(xiàn)),不知道家人們?cè)陧?xiàng)目中使用的是哪種方式,如果有更好地方式也歡迎一起交流~~~,本文所貼出的完整代碼已上傳到GitHub。
思路總覽
在具體講解實(shí)現(xiàn)方式之前,先講一下五種方式的思路:
1.手動(dòng)處理字段加解密
最簡(jiǎn)單、樸素的方式。如果項(xiàng)目中只有個(gè)別字段,例如密碼字段需要加密,則可以使用這種方法。不過(guò),通常密碼都是做單向 Hash 加密,不存在解密的情況,本文后續(xù)為了統(tǒng)一講解加解密的方式,就對(duì)字段統(tǒng)一使用了 AES 對(duì)稱加密算法。聽(tīng)說(shuō)有的項(xiàng)目需要使用 SM2 之類非對(duì)稱加密算法,本文就不再介紹了,只需要參考思路替換相應(yīng)加解密調(diào)用的方法即可。
優(yōu)點(diǎn):使用簡(jiǎn)單、易懂,技術(shù)難度低
缺點(diǎn):全手工處理,容易遺漏,費(fèi)時(shí)。
2.注解結(jié)合 AOP 實(shí)現(xiàn)
相對(duì)簡(jiǎn)便的方式,在需要進(jìn)行加解密處理的字段上添加字段注解,然后在有加解密處理需求的方法上添加方法注解,之后結(jié)合 AOP ,對(duì)入?yún)⒑头祷亟Y(jié)果進(jìn)行處理即可。
優(yōu)點(diǎn):使用相對(duì)簡(jiǎn)單,加解密處理統(tǒng)一在切面處理中完成。
缺點(diǎn):只能處理入?yún)⒑头祷亟Y(jié)果中的字段加解密,如果處理邏輯中涉及到加解密需求還是需要手動(dòng)處理。同時(shí)需要在所有有加解密處理需求的類或方法(可以定義類級(jí)別和方法級(jí)別的注解,本文只講解方法級(jí)別的注解)上添加注解,也容易遺漏,測(cè)試時(shí)需要特別注意。
3.自定義序列化 / 反序列化類結(jié)合注解實(shí)現(xiàn)
通過(guò)自定義序列化 / 反序列化處理類,然后結(jié)合序列化 / 反序列化注解中指定相應(yīng)類進(jìn)行實(shí)現(xiàn)。
優(yōu)點(diǎn):使用簡(jiǎn)單,加解密處理統(tǒng)一在自定義的序列化化類中完成,只需要在字段上添加注解。
缺點(diǎn):只能處理序列化數(shù)據(jù)中的加解密,如果業(yè)務(wù)邏輯中需要手動(dòng)設(shè)置某個(gè)加密字段的值,還是需要手工處理。
4.MybatisPlus
自定義TypeHandler
實(shí)現(xiàn)
和自定義序列化 / 反序列化類的思路類似,不過(guò)是和框架功能相耦合,通過(guò)使用MybatisPlus
自定義TypeHandler
實(shí)現(xiàn)。
優(yōu)點(diǎn):使用簡(jiǎn)單,加解密處理統(tǒng)一在自定義的TypeHandler
中完成,只需要在字段上添加注解。
缺點(diǎn):只能處理 SQL 的查詢和保存的結(jié)果,如果業(yè)務(wù)邏輯中需要手動(dòng)設(shè)置某個(gè)加密字段的值,還是需要手工處理,如果存在自定義 SQL ,還需要額外添加注解處理,與框架綁定。
5.MybatisPlus自定義攔截器實(shí)現(xiàn)
相對(duì)底層的方式,結(jié)合框架自帶的攔截器功能,通過(guò)對(duì) SQL 拼接和處理 SQL 查詢結(jié)果進(jìn)行實(shí)現(xiàn)。
優(yōu)點(diǎn):使用簡(jiǎn)單,加解密處理統(tǒng)一在自定義的攔截器中完成,無(wú)需使用注解。
缺點(diǎn):只能處理 SQL 的查詢和保存的結(jié)果,如果業(yè)務(wù)邏輯中需要手動(dòng)設(shè)置某個(gè)加密字段的值,還是需要手工處理,此外還需要整理所有需要加解密操作的字段名,與框架綁定。
小結(jié):當(dāng)然除了以上幾種方法,根據(jù)使用技術(shù)和框架的不同,還有很多種方式,例如 JPA 可以自定義 Convert,類似方法 3 和 4,這里不再介紹。其實(shí),在實(shí)現(xiàn)的過(guò)程中,有想過(guò)通過(guò)修改字節(jié)碼,修改字段的 Getter / Setter 方法進(jìn)行實(shí)現(xiàn),但是在實(shí)現(xiàn)的時(shí)候才發(fā)現(xiàn)兩者的操作是成對(duì)的,入庫(kù)的時(shí)候也就還是明文,不過(guò)如果是數(shù)據(jù)脫敏,由于只需要修改 Setter 方法,則可以考慮使用修改字節(jié)碼的方式。
具體實(shí)現(xiàn)
手工處理
話不多說(shuō),直接上代碼:
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; }
這個(gè)就不再做詳細(xì)解釋了~~~
注解結(jié)合 AOP
首先需要定義一個(gè)字段注解和方法注解:
/** * 字段加密注解 * * @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 { ? /** * 需要處理對(duì)象在參數(shù)列表中的位置 */ int[] value() default { 0 }; ? /** * 是否啟用解密處理 */ boolean enableDecrypt() default true; ? }
然后結(jié)合 AOP 去處理方法注解:
/** * 處理加密注解切面 * 特別注意, 這里的排序需要 + 1, 否則會(huì)報(bào)錯(cuò), 具體原因參考鏈接: * <a >...</a> * * @author 莊周de蝴蝶 * @date 2023-11-07 */ @Slf4j @Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE + 1) public class EncryptMethodAspect { ? /** * 處理加密方法注解 * * @param joinPoint 切點(diǎn) * @param encryptMethod 加密方法注解 * @return 結(jié)果 */ @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; } // 處理入?yún)⒅械募用? handleEncrypt(args[i]); } Object result = joinPoint.proceed(); if (encryptMethod.enableDecrypt()) { // 對(duì)返回結(jié)果中的字段進(jìn)行解密處理 return handleDecrypt(result); } return result; } catch (Throwable throwable) { log.error("加密注解處理出現(xiàn)異常", throwable); throw throwable; } } ? /** * 對(duì)添加了 EncryptField 注解的字段進(jìn)行加密 * * @param obj 要處理的對(duì)象 */ private void handleEncrypt(Object obj) throws IllegalAccessException { handleEnDecrypt(obj, AESUtils::encrypt); } ? /** * 對(duì)添加了 EncryptField 注解的字段進(jìn)行解密, <b>只考慮了返回值是對(duì)象的情況</b> * * @param obj 要處理的對(duì)象 * @return 結(jié)果 */ private Object handleDecrypt(Object obj) throws IllegalAccessException { return handleEnDecrypt(obj, AESUtils::decrypt); } /** * 對(duì)添加了 EncryptField 注解的字段進(jìn)行加解密處理 * * @param obj 要處理的對(duì)象 * @param handleFun 處理函數(shù) * @return 結(jié)果 */ 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; } ? }
這里只考慮了返回結(jié)果是實(shí)體對(duì)象的情況,如果返回類型的是分頁(yè)或者是列表亦或者是類似Result的形式,需要自己進(jìn)行額外的處理。
最后是使用的方式,首先是在實(shí)體的字段上添加注解:
/** * 用戶類 * * @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; ? }
然后是在方法上添加注解,這里是在控制層使用,當(dāng)然也可以在ServiceImpl
里的方法上使用:
@EncryptMethod @PostMapping("/method2") public User method2(@RequestBody User user) { return userService.method2(user); }
自定義序列化注解
首先需要實(shí)現(xiàn)序列化 / 反序列化處理類:
/** * 解密序列化處理器 * * @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; } ? }
然后定義注解,這里通過(guò)使用Jackson
的JacksonAnnotationsInside
注解將序列化和反序列化合并,這樣在使用時(shí)就可以只使用一個(gè)注解:
/** * 字段加解密序列化注解 * * @author 莊周de蝴蝶 * @date 2023-10-23 */ @JsonSerialize(using = DecryptSerializer.class) @JsonDeserialize(using = EncryptDeserializer.class) @JacksonAnnotationsInside @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptSerializer { }
最后在相應(yīng)的實(shí)體字段中添加注解即可:
/** * 用戶類 * * @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 處理函數(shù) * @return 結(jié)果 */ private String handleResult(String val, UnaryOperator<String> fun) { HttpServletRequest request = ServletUtils.getRequest(); return StringUtils.isBlank(val) ? val : fun.apply(val); } }
然后在相應(yīng)的實(shí)體字段中添加注解即可:
/** * 用戶類 * * @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自定義攔截器
首先是定義一個(gè)保存操作的攔截器,關(guān)于攔截器的使用,由于和框架相關(guān)聯(lián),這里不再詳細(xì)介紹使用方式:
/** * 加密更新攔截器處理 * * @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)); } ? }
然后再定義一個(gè)查詢操作的攔截器:
/** * 加密查詢攔截器處理 * * @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
的內(nèi)容如下:
/** * 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實(shí)現(xiàn)數(shù)據(jù)加密存儲(chǔ)的方式總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot數(shù)據(jù)加密存儲(chǔ)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java System類兩個(gè)常用方法代碼實(shí)例
這篇文章主要介紹了Java System類兩個(gè)常用方法代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02Hibernate實(shí)現(xiàn)many-to-many的映射關(guān)系
今天小編就為大家分享一篇關(guān)于Hibernate實(shí)現(xiàn)many-to-many的映射關(guān)系,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03Java利用happen-before規(guī)則如何實(shí)現(xiàn)共享變量的同步操作詳解
這篇文章主要給大家介紹了關(guān)于Java利用happen-before規(guī)則實(shí)現(xiàn)共享變量的同步操作的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-06-06Hibernate中使用HQLQuery查詢?nèi)繑?shù)據(jù)和部分?jǐn)?shù)據(jù)的方法實(shí)例
今天小編就為大家分享一篇關(guān)于Hibernate中使用HQLQuery查詢?nèi)繑?shù)據(jù)和部分?jǐn)?shù)據(jù)的方法實(shí)例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03Spring Cloud Config對(duì)特殊字符加密處理的方法詳解
這篇文章主要給大家介紹了關(guān)于Spring Cloud Config對(duì)特殊字符加密處理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2018-05-05