如何用Mybatis攔截器實現(xiàn)自動對敏感字段進行加解密

為了數(shù)據(jù)安全問題埋涧,有時候需要將部分敏感字段加密后再入庫围肥,查詢時又需要將其解密后返回給前端使用戏羽。我們可以用Mybatis的攔截器來實現(xiàn)這一需求梧奢。

  1. 定義一個注解狱掂,用來標(biāo)識需要加解密的字段。

為了盡量減少不必要的反射操作亲轨,可以將該注解同時標(biāo)識在實體類上趋惨,對于沒有被標(biāo)識的實體類,無需利用反射來操作其屬性惦蚊。

import java.lang.annotation.*;

/**
 * 標(biāo)識需要加解密的類及其屬性
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface FieldEncrypt {
}
  1. 定義加密攔截器

攔截所有insert和update操作器虾,拿到實體對象,并通過反射獲取到所有標(biāo)記了@FieldExcrypt注解的屬性蹦锋,將其值進行加密并替換

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

/**
 * 加密攔截器
 * <p>攔截所有insert和update操作兆沙,</p>
 * <p>如果實體類中有屬性標(biāo)記有@FieldEncrypt注解,則對其進行加密替換</p>
 */
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class EncryptionInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        log.debug("EncryptionInterceptor 操作類型:{}", sqlCommandType);
        MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)args[1];
        Object entity = paramMap.get("et");
        try {
            if (SqlCommandType.INSERT == sqlCommandType) {
                FieldEncryptUtil.encryptField(entity);
            } else if (SqlCommandType.UPDATE == sqlCommandType) {
                FieldEncryptUtil.encryptField(entity);
                log.debug("[EncryptionInterceptor] update operation,encrypt field: {}", entity);
            }
        } catch (Exception e) {
            log.error("[EncryptionInterceptor] encryptField failed  entity:{}", entity.getClass().getName(), e);
        }
        return invocation.proceed();
    }

}
  1. 定義解密攔截器

攔截所有query操作莉掂,先執(zhí)行sql葛圃,拿到返回的結(jié)果,并通過反射獲取到所有標(biāo)記了@FieldExcrypt注解的屬性,將其值進行解密并替換装悲。

請注意:結(jié)果可能是單個Bean(如selectOne方法)昏鹃,也有可能是ArrayList。如果是集合诀诊,則以第一個元素的Class對象為準(zhǔn)洞渤。

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.ArrayList;

/**
 * 解密攔截器
 * <p>攔截所有query操作</p>
 * <p>如果結(jié)果集合元素實體中,存在屬性標(biāo)記了@FieldEncrypt注解属瓣,則解密并替換</p>
 */
@Slf4j
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DecryptionInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        try {
            if(result instanceof ArrayList) {
                ArrayList list = (ArrayList) result;
                if(list.isEmpty()) {
                    return result;
                }
                Class objClazz = list.get(0).getClass();
                if (FieldEncryptUtil.hasEncryptFields(objClazz)) {
                    for (Object item : list) {
                        FieldEncryptUtil.decryptField(item);
                    }
                }
                return result;
            } else {
                FieldEncryptUtil.decryptField(result);
            }
        } catch (Exception e) {
            log.error("[DecryptionInterceptor] decryptField failed  entity:{}", result.getClass().getName(), e);
        }

        return result;
    }
}
  1. 加解密工具類
import com.baomidou.mybatisplus.core.toolkit.AES;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Objects;

@Slf4j
public class FieldEncryptUtil {

    private static final String SECRET = "{替換成AES密鑰}";

    private FieldEncryptUtil() {
    }

    public static String encrypt(String src) {
        return AES.encrypt(src, SECRET);
    }

    public static String decrypt(String src) {
        return AES.decrypt(src, SECRET);
    }

    public static boolean hasEncryptFields(Class clz) {
        return AnnotationUtils.isCandidateClass(clz, FieldEncrypt.class);
    }

    private static boolean isFieldEncrypt(Field field) {
        FieldEncrypt encryptAnno = AnnotationUtils.findAnnotation(field, FieldEncrypt.class);
        return Objects.nonNull(encryptAnno);
    }

    /**
     * 對實體類字段加密替換
     */
    public static void encryptField(Object object) throws IllegalAccessException {
        if (Objects.isNull(object) || !hasEncryptFields(object.getClass())) {
            return;
        }
        Class clazz = object.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            Type fieldClazz = field.getGenericType();
            if (!(fieldClazz instanceof Class) || !String.class.isAssignableFrom((Class)fieldClazz)) {
                continue;
            }
            if (isFieldEncrypt(field)) {
                field.setAccessible(true);
                Object originVal = field.get(object);
                if (null == originVal || StringUtils.isBlank(originVal.toString())) {
                    continue;
                }
                String encryptedStr = encrypt(originVal.toString());
                field.set(object, encryptedStr);
                log.debug("[encryptField success] encrypt {}#{}", clazz.getName(), field.getName());
            }
        }
    }

    public static void decryptField(Object object) throws IllegalAccessException {
        if (Objects.isNull(object) || !hasEncryptFields(object.getClass())) {
            return;
        }
        Class clazz = object.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            Type fieldClazz = field.getGenericType();
            if (!(fieldClazz instanceof Class) || !String.class.isAssignableFrom((Class)fieldClazz)) {
                continue;
            }
            if (isFieldEncrypt(field)) {
                field.setAccessible(true);
                Object originVal = field.get(object);
                if (null == originVal || StringUtils.isBlank(originVal.toString())) {
                    continue;
                }
                String decryptedStr = decrypt(originVal.toString());
                field.set(object, decryptedStr);
                log.debug("[decryptField success] {}#{}", clazz.getName(), field.getName());
            }
        }
    }

}

實體類示例(對身份證號進行加密存儲)

@FieldEncrypt
@TableName("user")
public class User {
    
    @TableId
    private Long id;
    
    private String username;
    
    private String phone;
    
    @FieldEncrypt
    private String idCard;
    
}

注意事項

  • 因為我是在starter組件里實現(xiàn)的载迄,所以兩個攔截器沒有使用@Component注解標(biāo)識,而是使用AutoConfiguration聲明的抡蛙。如果你是在項目中直接實現(xiàn)的护昧,請將其交給Spring容器進行管理。

  • 因為加解密是針對字符串而言的粗截,所以惋耙,在進行加解密時需要判斷字段類型,如果注解錯誤地標(biāo)識在了非String類型字段上熊昌,要么忽略绽榛,要么根據(jù)實際需求實現(xiàn)邏輯。

  • 工具類中寫死了AES密鑰婿屹,這個密鑰必須全局唯一灭美,否則會出錯。

  • 為防止意外錯誤發(fā)生昂利,導(dǎo)致代碼報錯届腐,我這里在加解密過程中若發(fā)生異常,默認是不替換蜂奸,直接返回原值犁苏。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市扩所,隨后出現(xiàn)的幾起案子傀顾,更是在濱河造成了極大的恐慌,老刑警劉巖碌奉,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件短曾,死亡現(xiàn)場離奇詭異,居然都是意外死亡赐劣,警方通過查閱死者的電腦和手機嫉拐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魁兼,“玉大人婉徘,你說我怎么就攤上這事。” “怎么了盖呼?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵儒鹿,是天一觀的道長。 經(jīng)常有香客問我几晤,道長约炎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任蟹瘾,我火速辦了婚禮圾浅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘憾朴。我一直安慰自己狸捕,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布众雷。 她就那樣靜靜地躺著灸拍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砾省。 梳的紋絲不亂的頭發(fā)上鸡岗,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音纯蛾,去河邊找鬼纤房。 笑死纵隔,一個胖子當(dāng)著我的面吹牛翻诉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捌刮,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼碰煌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绅作?” 一聲冷哼從身側(cè)響起芦圾,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俄认,沒想到半個月后个少,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡眯杏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年夜焦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岂贩。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡茫经,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情卸伞,我是刑警寧澤抹镊,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站荤傲,受9級特大地震影響垮耳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弃酌,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一氨菇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧妓湘,春花似錦查蓉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至唬党,卻和暖如春鹃共,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驶拱。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工霜浴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蓝纲。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓阴孟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親税迷。 傳聞我的和親對象是個殘疾皇子永丝,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容