Mybatis插件+注解實現(xiàn)數(shù)據(jù)脫敏方式
引入
問題
在項目中需要對用戶敏感數(shù)據(jù)進行脫敏處理,例如身份證號、手機號等信息進行加密再入庫。
解決思路
方法一:最簡單直接的方式就是對代碼中所有涉及到敏感數(shù)據(jù)的接口在查詢和插入時進行加解密
方法二:方法一會對代碼入侵很大,需要考慮到所有涉及到的接口,工作量極大,并且可能出現(xiàn)組員協(xié)作時沒有考慮到對數(shù)據(jù)加解密的問題。最后決定采用mybatis的插件在mybatis SQL執(zhí)行和查詢結(jié)果填充操作上進行切入。上層業(yè)務(wù)調(diào)用不再需要考慮數(shù)據(jù)的加解密問題同時也保證了數(shù)據(jù)的脫敏
Mybatis插件原理
Mybatis的插件是通過攔截器實現(xiàn)的,Mabatis支持對四種對象進行攔截
實現(xiàn)
- 設(shè)置參數(shù)時對參數(shù)中含有敏感字段的數(shù)據(jù)進行加密
- 對查詢返回的結(jié)果進行解密處理
基于上面兩種要求,我們只需要對ParameterHandler和ResultSetHandler進行切入。
定義特定注解,在切入時只需要檢查字段中是否包含該注解來決定是否加解密
加解密注解
定義SensitiveData注解
import java.lang.annotation.*; /** * 該注解定義在類上 * 插件通過掃描類對象是否包含這個注解來決定是否繼續(xù)掃描其中的字段注解 * 這個注解要配合EncryptTransaction注解 * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:38 **/ @Inherited @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveData { }
定義EncryptTransaction注解
import java.lang.annotation.*; /** * 該注解有兩種使用方式 * ①:配合@SensitiveData加在類中的字段上 * ②:直接在Mapper中的方法參數(shù)上使用 * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:40 **/ @Documented @Inherited @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface EncryptTransaction { }
加解密工具類
解密接口
package sicnu.cs.ich.common.interceptor.transaction.service; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:43 **/ public interface IDecryptUtil { /** * 解密 * * @param result resultType的實例 * @return T * @throws IllegalAccessException 字段不可訪問異常 */ <T> T decrypt(T result) throws IllegalAccessException; }
加密接口
package sicnu.cs.ich.common.interceptor.transaction.service; import java.lang.reflect.Field; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:41 **/ public interface IEncryptUtil { /** * 加密 * * @param declaredFields 加密字段 * @param paramsObject 對象 * @param <T> 入?yún)㈩愋? * @return 返回加密 * @throws IllegalAccessException 不可訪問 */ <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException; }
package sicnu.cs.ich.common.interceptor.transaction.service.impl; import org.springframework.stereotype.Component; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.lang.reflect.Field; import java.util.Objects; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:43 **/ @Component public class DecryptImpl implements IDecryptUtil { /** * 解密 * * @param result resultType的實例 */ @Override public <T> T decrypt(T result) throws IllegalAccessException { //取出resultType的類 Class<?> resultClass = result.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被DecryptTransaction注解的字段 EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(result); //String的解密 if (object instanceof String) { String value = (String) object; //對注解的字段進行逐一解密 try { field.set(result, DBAESUtil.decrypt(value)); } catch (Exception e) { e.printStackTrace(); } } } } return result; } }
加密實現(xiàn)類
package sicnu.cs.ich.common.interceptor.transaction.service.impl; import com.fasterxml.jackson.databind.ObjectReader; import org.springframework.stereotype.Component; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.io.ObjectInputStream; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Objects; import java.util.Random; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:42 **/ @Component public class EncryptUtilImpl implements IEncryptUtil { @Override public <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException { //取出所有被EncryptTransaction注解的字段 for (Field field : declaredFields) { EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(paramsObject); //暫時只實現(xiàn)String類型的加密 if (object instanceof String) { String value = (String) object; //加密 try { field.set(paramsObject, DBAESUtil.encrypt(value)); } catch (Exception e) { e.printStackTrace(); } } } } return paramsObject; } }
解密實現(xiàn)類
package sicnu.cs.ich.common.interceptor.transaction.service.impl; import org.springframework.stereotype.Component; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.lang.reflect.Field; import java.util.Objects; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:43 **/ @Component public class DecryptImpl implements IDecryptUtil { /** * 解密 * * @param result resultType的實例 */ @Override public <T> T decrypt(T result) throws IllegalAccessException { //取出resultType的類 Class<?> resultClass = result.getClass(); Field[] declaredFields = resultClass.getDeclaredFields(); for (Field field : declaredFields) { //取出所有被DecryptTransaction注解的字段 EncryptTransaction encryptTransaction = field.getAnnotation(EncryptTransaction.class); if (!Objects.isNull(encryptTransaction)) { field.setAccessible(true); Object object = field.get(result); //String的解密 if (object instanceof String) { String value = (String) object; //對注解的字段進行逐一解密 try { field.set(result, DBAESUtil.decrypt(value)); } catch (Exception e) { e.printStackTrace(); } } } } return result; } }
加解密工具類
package sicnu.cs.ich.common.util.keyCryptor; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:45 **/ public class DBAESUtil { private static final String DEFAULT_V = "6859505890402435"; // 自己填寫 private static final String KEY = "***"; private static final String ALGORITHM = "AES"; private static SecretKeySpec getKey() { byte[] arrBTmp = DBAESUtil.KEY.getBytes(); // 創(chuàng)建一個空的16位字節(jié)數(shù)組(默認值為0) byte[] arrB = new byte[16]; for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) { arrB[i] = arrBTmp[i]; } return new SecretKeySpec(arrB, ALGORITHM); } /** * 加密 */ public static String encrypt(String content) throws Exception { final Base64.Encoder encoder = Base64.getEncoder(); SecretKeySpec keySpec = getKey(); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes()); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); byte[] encrypted = cipher.doFinal(content.getBytes()); return encoder.encodeToString(encrypted); } /** * 解密 */ public static String decrypt(String content) throws Exception { final Base64.Decoder decoder = Base64.getDecoder(); SecretKeySpec keySpec = getKey(); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec iv = new IvParameterSpec(DEFAULT_V.getBytes()); cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); byte[] base64 = decoder.decode(content); byte[] original = cipher.doFinal(base64); return new String(original); } }
插件實現(xiàn)
參數(shù)插件ParameterInterceptor
切入mybatis設(shè)置參數(shù)時對敏感數(shù)據(jù)進行加密
Mybatis插件的使用就是通過實現(xiàn)Mybatis中的Interceptor接口
再配合@Intercepts注解
// 使用mybatis插件時需要定義簽名 // type標識需要切入的Handler // method表示要要切入的方法 @Intercepts({ @Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class), })
package sicnu.cs.ich.common.interceptor.transaction; import com.baomidou.mybatisplus.core.MybatisParameterHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData; import sicnu.cs.ich.common.interceptor.transaction.service.IEncryptUtil; import sicnu.cs.ich.common.util.keyCryptor.DBAESUtil; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.sql.PreparedStatement; import java.util.*; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-22:48 **/ @Slf4j // 注入Spring @Component @Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class), }) public class ParameterInterceptor implements Interceptor { @Autowired private IEncryptUtil IEncryptUtil; @Override public Object intercept(Invocation invocation) throws Throwable { //@Signature 指定了 type= parameterHandler 后,這里的 invocation.getTarget() 便是parameterHandler //若指定ResultSetHandler ,這里則能強轉(zhuǎn)為ResultSetHandler MybatisParameterHandler parameterHandler = (MybatisParameterHandler) invocation.getTarget(); // 獲取參數(shù)對像,即 mapper 中 paramsType 的實例 Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject"); parameterField.setAccessible(true); //取出實例 Object parameterObject = parameterField.get(parameterHandler); // 搜索該方法中是否有需要加密的普通字段 List<String> paramNames = searchParamAnnotation(parameterHandler); if (parameterObject != null) { Class<?> parameterObjectClass = parameterObject.getClass(); //對類字段進行加密 //校驗該實例的類是否被@SensitiveData所注解 SensitiveData sensitiveData = AnnotationUtils.findAnnotation(parameterObjectClass, SensitiveData.class); if (Objects.nonNull(sensitiveData)) { //取出當前當前類所有字段,傳入加密方法 Field[] declaredFields = parameterObjectClass.getDeclaredFields(); IEncryptUtil.encrypt(declaredFields, parameterObject); } // 對普通字段進行加密 if (!CollectionUtils.isEmpty(paramNames)) { // 反射獲取 BoundSql 對象,此對象包含生成的sql和sql的參數(shù)map映射 Field boundSqlField = parameterHandler.getClass().getDeclaredField("boundSql"); boundSqlField.setAccessible(true); BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler); PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0]; // 改寫參數(shù) processParam(parameterObject, paramNames); // 改寫的參數(shù)設(shè)置到原parameterHandler對象 parameterField.set(parameterHandler, parameterObject); parameterHandler.setParameters(ps); } } return invocation.proceed(); } private void processParam(Object parameterObject, List<String> params) throws Exception { // 處理參數(shù)對象 如果是 map 且map的key 中沒有 tenantId,添加到參數(shù)map中 // 如果參數(shù)是bean,反射設(shè)置值 if (parameterObject instanceof Map) { @SuppressWarnings("unchecked") Map<String, String> map = ((Map<String, String>) parameterObject); for (String param : params) { String value = map.get(param); map.put(param, value==null?null:DBAESUtil.encrypt(value)); } // parameterObject = map; } } private List<String> searchParamAnnotation(ParameterHandler parameterHandler) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException { Class<MybatisParameterHandler> handlerClass = MybatisParameterHandler.class; Field mappedStatementFiled = handlerClass.getDeclaredField("mappedStatement"); mappedStatementFiled.setAccessible(true); MappedStatement mappedStatement = (MappedStatement) mappedStatementFiled.get(parameterHandler); String methodName = mappedStatement.getId(); Class<?> mapperClass = Class.forName(methodName.substring(0, methodName.lastIndexOf('.'))); methodName = methodName.substring(methodName.lastIndexOf('.') + 1); Method[] methods = mapperClass.getDeclaredMethods(); Method method = null; for (Method m : methods) { if (m.getName().equals(methodName)) { method = m; break; } } List<String> paramNames = null; if (method != null) { Annotation[][] pa = method.getParameterAnnotations(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < pa.length; i++) { for (Annotation annotation : pa[i]) { if (annotation instanceof EncryptTransaction) { if (paramNames == null) { paramNames = new ArrayList<>(); } paramNames.add(parameters[i].getName()); } } } } return paramNames; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
返回值插件ResultSetInterceptor
package sicnu.cs.ich.common.interceptor.transaction; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData; import java.sql.Statement; import java.util.ArrayList; import java.util.Objects; import java.util.Properties; /** * @author 沈洋 郵箱:1845973183@qq.com * @create 2021/10/26-23:02 **/ @Slf4j @Component @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class ResultSetInterceptor implements Interceptor { @Autowired private sicnu.cs.ich.common.interceptor.transaction.service.IDecryptUtil IDecryptUtil; @Override public Object intercept(Invocation invocation) throws Throwable { //取出查詢的結(jié)果 Object resultObject = invocation.proceed(); if (Objects.isNull(resultObject)) { return null; } //基于selectList if (resultObject instanceof ArrayList) { @SuppressWarnings("unchecked") ArrayList<Objects> resultList = (ArrayList<Objects>) resultObject; if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) { for (Object result : resultList) { //逐一解密 IDecryptUtil.decrypt(result); } } //基于selectOne } else { if (needToDecrypt(resultObject)) { IDecryptUtil.decrypt(resultObject); } } return resultObject; } private boolean needToDecrypt(Object object) { Class<?> objectClass = object.getClass(); SensitiveData sensitiveData = AnnotationUtils.findAnnotation(objectClass, SensitiveData.class); return Objects.nonNull(sensitiveData); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
使用
注解在實體類上
import lombok.*; import org.springframework.security.core.userdetails.UserDetails; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; import sicnu.cs.ich.api.common.annotations.transaction.SensitiveData; @With @Builder @Data @NoArgsConstructor @AllArgsConstructor @SensitiveData // 插件只對加了該注解的類進行掃描,只有加了這個注解的類才會生效 public class User implements Serializable { private Integer id; private String username; private String openId; private String password; // 表明對該字段進行加密 @EncryptTransaction private String email; // 表明對該字段進行加密 @EncryptTransaction private String mobile; private Date createTime; private Date expireTime; private Boolean status = true; }
注解在參數(shù)上
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import sicnu.cs.ich.api.common.annotations.transaction.EncryptTransaction; @Mapper public interface UserMapper extends BaseMapper<User> { // 只需要在參數(shù)前加上@EncryptTransaction 即可 long countByEmail(@EncryptTransaction @Param("email") String email); long countByMobile(@EncryptTransaction @Param("mobile") String mobile); }
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringCloud之監(jiān)控數(shù)據(jù)聚合Turbine的實現(xiàn)
這篇文章主要介紹了SpringCloud之監(jiān)控數(shù)據(jù)聚合Turbine的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08SpringBoot整合Javamail實現(xiàn)郵件發(fā)送功能
郵件發(fā)送是一個很普遍的功能,springboot整合了相關(guān)的starter,本文給大家介紹了可以實現(xiàn)一個簡單的郵件發(fā)送功能的實例,文中通過代碼給大家介紹的非常詳細,感興趣的朋友可以參考下2023-12-12Java數(shù)據(jù)結(jié)構(gòu)之并查集的實現(xiàn)
并查集是一種用來管理元素分組情況的數(shù)據(jù)結(jié)構(gòu)。并查集可以高效地進行如下操作。本文將通過Java實現(xiàn)并查集,感興趣的小伙伴可以了解一下2022-01-01Java中的HttpServletRequestWrapper用法解析
這篇文章主要介紹了Java中的HttpServletRequestWrapper用法解析,HttpServletRequest 對參數(shù)值的獲取實際調(diào)的是org.apache.catalina.connector.Request,沒有提供對應(yīng)的set方法修改屬性,所以不能對前端傳來的參數(shù)進行修改,需要的朋友可以參考下2024-01-01