基于Mybatis-Plus攔截器實(shí)現(xiàn)MySQL數(shù)據(jù)加解密的示例代碼
一、背景
用戶(hù)的一些敏感數(shù)據(jù),例如手機(jī)號(hào)、郵箱、身份證等信息,在數(shù)據(jù)庫(kù)以明文存儲(chǔ)時(shí)會(huì)存在數(shù)據(jù)泄露的風(fēng)險(xiǎn),因此需要進(jìn)行加密, 但存儲(chǔ)數(shù)據(jù)再被取出時(shí),需要進(jìn)行解密,因此加密算法需要使用對(duì)稱(chēng)加密算法。
常用的對(duì)稱(chēng)加密算法有AES、DES、RC、BASE64等等,各算法的區(qū)別與優(yōu)劣請(qǐng)自行百度。
本案例采用AES算法對(duì)數(shù)據(jù)進(jìn)行加密。
二、MybatisPlus攔截器介紹
本文基于SpringBoot+MybatisPlus(3.5.X)+MySQL8架構(gòu),Dao層與DB中間使用MP的攔截器機(jī)制,對(duì)數(shù)據(jù)存取過(guò)程進(jìn)行攔截,實(shí)現(xiàn)數(shù)據(jù)的加解密操作。
三、使用方法
該加解密攔截器功能在wutong-base-dao包(公司內(nèi)部包)已經(jīng)實(shí)現(xiàn),如果您的項(xiàng)目已經(jīng)依賴(lài)了base-dao,就可以直接使用。
另外,在碼云上有Demo案例,見(jiàn): java-test: java練習(xí)Demo項(xiàng)目 - Gitee.com
基于wutong-base-dao包的使用步驟如下。
1、添加wutong-base-dao依賴(lài)
<dependency> <groupId>com.talkweb</groupId> <artifactId>wutong-base-dao</artifactId> <version>請(qǐng)使用最新版本</version> </dependency>
2、在yaml配置開(kāi)關(guān),啟用加解密
mybatis-plus: wutong: encrypt: # 是否開(kāi)啟敏感數(shù)據(jù)加解密,默認(rèn)false enable: true # AES加密秘鑰,可以使用hutool的SecureUtil工具類(lèi)生成 secretKey: yourSecretKey
3、定義PO類(lèi)
實(shí)體類(lèi)上使用自定義注解,來(lái)標(biāo)記需要進(jìn)行加解密
// 必須使用@EncryptedTable注解 @EncryptedTable @TableName(value = "wsp_user") public class UserEntity implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; // 使用@EncryptedColumn注解 @EncryptedColumn private String mobile; // 使用@EncryptedColumn注解 @EncryptedColumn private String email; }
4、定義API接口
通過(guò)MP自帶API、Lambda、自定義mapper接口三種方式進(jìn)行測(cè)試
/** * 用戶(hù)表控制器 * * @author wangshaopeng@talkweb.com.cn * @Date 2023-01-11 */ @RestController @RequestMapping("/user") public class UserController { @Resource(name = "userServiceImpl") private IUserService userService; @Resource(name = "userXmlServiceImpl") private IUserService userXmlService; /** * 測(cè)試解密 */ @GetMapping(name = "測(cè)試解密", value = "/detail") public UserEntity detail(Long id) { // 測(cè)試MP API // UserEntity entity = userService.getById(id); // 測(cè)試自定義Mapper接口 UserEntity entity = userXmlService.getById(id); if (null == entity) { return new UserEntity(); } return entity; } /** * 新增用戶(hù)表,測(cè)試加密 */ @GetMapping(name = "新增用戶(hù)表,測(cè)試加密", value = "/add") public UserEntity add(UserEntity entity) { // 測(cè)試MP API // userService.save(entity); // 測(cè)試自定義Mapper接口 userXmlService.save(entity); return entity; } /** * 修改用戶(hù)表 */ @GetMapping(name = "修改用戶(hù)表", value = "/update") public UserEntity update(UserEntity entity) { // 測(cè)試MP API // userService.updateById(entity); // 測(cè)試Lambda // LambdaUpdateWrapper<UserEntity> wrapper = new LambdaUpdateWrapper<>(); // wrapper.eq(UserEntity::getId, entity.getId()); // wrapper.set(UserEntity::getMobile, entity.getMobile()); // wrapper.set(UserEntity::getName, entity.getName()); // wrapper.set(UserEntity::getEmail, entity.getEmail()); // userService.update(wrapper); // 測(cè)試自定義Mapper接口 userXmlService.updateById(entity); return entity; } }
四、實(shí)現(xiàn)原理
1、自定義注解
根據(jù)注解進(jìn)行數(shù)據(jù)攔截
/** * 需要加解密的實(shí)體類(lèi)用這個(gè)注解 * @author wangshaopeng@talkweb.com.cn * @Date 2023-05-31 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface EncryptedTable { } /** * 需要加解密的字段用這個(gè)注解 * @author wangshaopeng@talkweb.com.cn * @Date 2023-05-31 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface EncryptedColumn { }
2、定義攔截器
加密攔截器EncryptInterceptor
/** * 加密攔截器 * * @author wangshaopeng@talkweb.com.cn * @Date 2023-05-31 */ @SuppressWarnings({"rawtypes"}) public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor { /** * 變量占位符正則 */ private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}"); @Override public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException { if (Objects.isNull(parameterObject)) { return; } // 通過(guò)MybatisPlus自帶API(save、insert等)新增數(shù)據(jù)庫(kù)時(shí) if (!(parameterObject instanceof Map)) { if (needToDecrypt(parameterObject.getClass())) { encryptEntity(parameterObject); } return; } Map paramMap = (Map) parameterObject; Object param; // 通過(guò)MybatisPlus自帶API(update、updateById等)修改數(shù)據(jù)庫(kù)時(shí) if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) { if (needToDecrypt(param.getClass())) { encryptEntity(param); } return; } // 通過(guò)在mapper.xml中自定義API修改數(shù)據(jù)庫(kù)時(shí) if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) { if (needToDecrypt(param.getClass())) { encryptEntity(param); } return; } // 通過(guò)UpdateWrapper、LambdaUpdateWrapper修改數(shù)據(jù)庫(kù)時(shí) if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) { if (param instanceof Update && param instanceof AbstractWrapper) { Class<?> entityClass = mappedStatement.getParameterMap().getType(); if (needToDecrypt(entityClass)) { encryptWrapper(entityClass, param); } } return; } } /** * 校驗(yàn)該實(shí)例的類(lèi)是否被@EncryptedTable所注解 */ private boolean needToDecrypt(Class<?> objectClass) { EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class); return Objects.nonNull(sensitiveData); } /** * 通過(guò)API(save、updateById等)修改數(shù)據(jù)庫(kù)時(shí) * * @param parameter */ private void encryptEntity(Object parameter) { //取出parameterType的類(lèi) Class<?> resultClass = parameter.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被EncryptedColumn注解的字段 EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class); if (!Objects.isNull(sensitiveField)) { field.setAccessible(true); Object object = null; try { object = field.get(parameter); } catch (IllegalAccessException e) { continue; } //只支持String的解密 if (object instanceof String) { String value = (String) object; //對(duì)注解的字段進(jìn)行逐一加密 try { field.set(parameter, AESUtils.encrypt(value)); } catch (IllegalAccessException e) { continue; } } } } } /** * 通過(guò)UpdateWrapper、LambdaUpdateWrapper修改數(shù)據(jù)庫(kù)時(shí) * * @param entityClass * @param ewParam */ private void encryptWrapper(Class<?> entityClass, Object ewParam) { AbstractWrapper updateWrapper = (AbstractWrapper) ewParam; String sqlSet = updateWrapper.getSqlSet(); String[] elArr = sqlSet.split(","); Map<String, String> propMap = new HashMap<>(elArr.length); Arrays.stream(elArr).forEach(el -> { String[] elPart = el.split("="); propMap.put(elPart[0], elPart[1]); }); //取出parameterType的類(lèi) Field[] declaredFields = entityClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被EncryptedColumn注解的字段 EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class); if (Objects.isNull(sensitiveField)) { continue; } String el = propMap.get(field.getName()); Matcher matcher = PARAM_PAIRS_RE.matcher(el); if (matcher.matches()) { String valueKey = matcher.group(1); Object value = updateWrapper.getParamNameValuePairs().get(valueKey); updateWrapper.getParamNameValuePairs().put(valueKey, AESUtils.encrypt(value.toString())); } } } }
解密攔截器
/** * 解密攔截器 * * @author wangshaopeng@talkweb.com.cn * @Date 2023-05-31 */ @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) @Component public class DecryptInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object resultObject = invocation.proceed(); if (Objects.isNull(resultObject)) { return null; } if (resultObject instanceof ArrayList) { //基于selectList ArrayList resultList = (ArrayList) resultObject; if (!resultList.isEmpty() && needToDecrypt(resultList.get(0))) { for (Object result : resultList) { //逐一解密 decrypt(result); } } } else if (needToDecrypt(resultObject)) { //基于selectOne decrypt(resultObject); } return resultObject; } /** * 校驗(yàn)該實(shí)例的類(lèi)是否被@EncryptedTable所注解 */ private boolean needToDecrypt(Object object) { Class<?> objectClass = object.getClass(); EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class); return Objects.nonNull(sensitiveData); } @Override public Object plugin(Object o) { return Plugin.wrap(o, this); } private <T> T decrypt(T result) throws Exception { //取出resultType的類(lèi) Class<?> resultClass = result.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被EncryptedColumn注解的字段 EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class); if (!Objects.isNull(sensitiveField)) { field.setAccessible(true); Object object = field.get(result); //只支持String的解密 if (object instanceof String) { String value = (String) object; //對(duì)注解的字段進(jìn)行逐一解密 field.set(result, AESUtils.decrypt(value)); } } } return result; } }
五、其他實(shí)現(xiàn)方案
在技術(shù)調(diào)研過(guò)程中,還測(cè)試了另外兩種便宜實(shí)現(xiàn)方案,由于無(wú)法覆蓋MP自帶API、Lambda、自定義API等多種場(chǎng)景,因此未采用。
1、使用字段類(lèi)型處理器
字段類(lèi)型處理器的[官方文檔點(diǎn)這里],不能處理LambdaUpdateWrapper更新數(shù)據(jù)時(shí)加密的場(chǎng)景。
自定義類(lèi)型處理器,實(shí)現(xiàn)加解密:
/** * @author wangshaopeng@talkweb.com.cn * @desccription 加密類(lèi)型字段處理器 * @date 2023/5/31 */ public class EncryptTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, AESUtils.encrypt(parameter)); } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { final String value = rs.getString(columnName); return AESUtils.decrypt(value); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { final String value = rs.getString(columnIndex); return AESUtils.decrypt(value); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { final String value = cs.getString(columnIndex); return AESUtils.decrypt(value); } }
在實(shí)體屬性上進(jìn)行指定
// @TableName注解必須指定autoResultMap = true @EncryptedTable @TableName(value = "wsp_user", autoResultMap = true) public class UserEntity implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; @TableField(typeHandler = EncryptTypeHandler.class) private String mobile; @TableField(typeHandler = EncryptTypeHandler.class) private String email; }
2、自動(dòng)填充功能
自動(dòng)填充功能的[官方文檔點(diǎn)這里],不能處理LambdaUpdateWrapper、自定義mapper接口更新數(shù)據(jù)時(shí)加密的場(chǎng)景,不支持解密的需求。
自定義類(lèi)型處理器,實(shí)現(xiàn)加解密:
/** * Mybatis元數(shù)據(jù)填充處理類(lèi),僅能處理MP的函數(shù),不能處理mapper.xml中自定義的insert、update * * @author wangshaopeng@talkweb.com.cn * @Date 2023-01-11 */ public class DBMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { String mobile = (String) metaObject.getValue("mobile"); this.strictInsertFill(metaObject, "mobile", String.class, AESUtils.encrypt(mobile)); String email = (String) metaObject.getValue("email"); this.strictInsertFill(metaObject, "email", String.class, AESUtils.encrypt(email)); } @Override public void updateFill(MetaObject metaObject) { String mobile = (String) metaObject.getValue("mobile"); this.strictUpdateFill(metaObject, "mobile", String.class, AESUtils.encrypt(mobile)); String email = (String) metaObject.getValue("email"); this.strictUpdateFill(metaObject, "email", String.class, AESUtils.encrypt(email)); } }
在實(shí)體類(lèi)上指定自動(dòng)填充策略
@EncryptedTable @TableName(value = "wsp_user") public class UserEntity implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; private String name; @TableField(fill = FieldFill.INSERT_UPDATE) private String mobile; @TableField(fill = FieldFill.INSERT_UPDATE) private String email; }
以上就是基于Mybatis-Plus攔截器實(shí)現(xiàn)MySQL數(shù)據(jù)加解密的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Mybatis-Plus實(shí)現(xiàn)MySQL數(shù)據(jù)加解密的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決java web應(yīng)用線上系統(tǒng)偶發(fā)宕機(jī)的情況
這篇文章主要介紹了解決java web應(yīng)用線上系統(tǒng)偶發(fā)宕機(jī)的情況,具有好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09關(guān)于Java數(shù)組聲明、創(chuàng)建、初始化的相關(guān)介紹
這篇文章主要是關(guān)于Java數(shù)組聲明、創(chuàng)建、初始化的相關(guān)介紹,并給出其對(duì)應(yīng)的代碼,需要的朋友可以參考下2015-08-08Java concurrency集合之ConcurrentHashMap_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java concurrency集合之ConcurrentHashMap的相關(guān)資料,需要的朋友可以參考下2017-06-06java語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲
這篇文章主要為大家詳細(xì)介紹了java語(yǔ)言實(shí)現(xiàn)猜數(shù)字游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05Mybatis?plus邏輯刪除注解@TableLogic的使用
本文主要介紹了Mybatis?plus邏輯刪除注解@TableLogic,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01詳解Spring Cloud Zuul中路由配置細(xì)節(jié)
本篇文章主要介紹了詳解Spring Cloud Zuul中路由配置細(xì)節(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10java暴力匹配及KMP算法解決字符串匹配問(wèn)題示例詳解
這篇文章主要為大家介紹了java算法中暴力匹配算法及KMP算法解決字符串匹配的問(wèn)題示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11