Spring-boot-手把手教你使用AOP進行加密解密簽名驗證

在上篇文章中,博主介紹了借助Spring攔截器進行token校驗妥凳。在本文中贝攒,將介紹如何通過AOP來進行加密解密,簽名驗證等操作圣猎,來保證接口的數(shù)據(jù)傳輸?shù)陌踩浴?/p>

加密算法

為什么需要加密呢士葫?就好比戰(zhàn)爭時期特工在進行傳輸情報的時候,如果將情報明文直接通過某種媒介傳輸給同盟人員送悔,那么一旦情報被地方截取慢显,就會釀成大禍。如果將明文通過某種加密算法加密成雜亂無章的密文欠啤,即使被敵方截獲荚藻,沒有對應的解密算法,也很難識別出其中的明文跪妥。安全傳輸領域鞋喇,加密算法是一種很常用的手段,它可以保證數(shù)據(jù)不被竊取和泄漏眉撵,還可以保證數(shù)據(jù)的完整性侦香,不被篡改。

常見的加密算法有對稱加密纽疟,非對稱加密罐韩,單向加密(簽名)等分類。其中對稱加密算法污朽,加密密鑰和解密密鑰是同一個散吵,因此發(fā)送發(fā)和接收方都需要維護一個相同的密鑰,如果密鑰要修改蟆肆,雙方都需要同時修改矾睦。非對稱加密算法中,發(fā)送發(fā)用公鑰進行加密炎功,接收方用私鑰進行解密枚冗。單向加密算法是對傳輸?shù)臄?shù)據(jù)生成一個簽名,通過這個簽名來驗證數(shù)據(jù)在傳輸過程中是否被篡改過蛇损,一般是不可逆的赁温。

常用的對稱加密算法有DES, AES, 3DES等坛怪, 非對稱加密算法有RSA, DSA, ECB等,簽名算法有SHA1, MD5, HMAC等股囊。在本文中將使用AES和HMAC-MD5來進行數(shù)據(jù)加密解密袜匿,以及簽名驗證。

算法分類

AES

AES 加密算法是一種對稱加密算,加密密鑰和解密密鑰是同一個。它采用對稱分組密碼體制濒翻,最少支持長度為128位的加密。涉及到分組加密穆壕,padding填充,初始向量IV其屏,密鑰,四種加密模式缨该。

  • 分組加密就是將原文分割成一段段的分別進行加密偎行,每段分組長度為128位16個字節(jié),如果最后一組長度不足128位贰拿,則采用padding填充模式將其補齊到128位蛤袒。然后對每組進行加密,最后組成最終密文膨更。

  • padding填充是為了解決分組后的長度不足128位的場景妙真。填充模式也有多種不同模式,比如PKCS5, PKCS7和NOPADDING荚守。其中PKSC5是指分組后缺少幾個字節(jié)珍德,就在后面填充幾個字節(jié)的幾,比如缺少2個字節(jié)矗漾,就在后面填充2個字節(jié)的2锈候。PKCS7是指缺少幾個字節(jié),就在后面填充幾個字節(jié)的0敞贡,比如缺少5個字節(jié)泵琳,就填充5個字節(jié)的0。NOPADDING模式就是不需要填充誊役。如果最后面剛好是16個字節(jié)的16获列,那么解密方不知道是填充數(shù)據(jù)還是真實數(shù)據(jù),因此會在后面再補16個字節(jié)的16來區(qū)分蛔垢。

  • 初始向量IV是為了保證數(shù)據(jù)的安全性击孩,如果我們對同一段內容進行加密后,所生成的密文應該是相同的啦桌,那么這樣就很容易通過密文分析出哪些段是相同的溯壶。比如原文分組后成為ABCADE,加密后的密文是GHIGJK,那么很容易看出那兩段內容是相同的及皂。第一個分組在初始加密向量的基礎上進行加密,以后的每一個分組都在前一個分組加密的結果為基礎進行加密且改,從而保證了即使相同的原文段验烧,也不會生成相同的密文段。

  • 密鑰是加密和解密公用的一個又跛,它一般是128位16個字節(jié)長度的隨機字符串碍拆,分組后的原文都用同一個密鑰進行加密。

  • 加密模式包含ECB慨蓝,CBC, CFB, OFB等四種模式感混。ECB分別對每個分組進行加密,相同的明文會被加密成相同的密文礼烈。CBC模式會使用上一段的加密結果作為加密向量弧满,相同的原文不會被加密成相同的密文。

MD5

MD5算法是一種不可逆的簽名算法此熬,對相同的輸入通過MD5散列函數(shù)處理后庭呜,會輸出相同的信息。因此MD5可以驗證傳輸?shù)臄?shù)據(jù)是否有被篡改犀忱,但是如果竊密者對明文進行了修改后募谎,再使用MD5算法進行散列,接收方將無法判斷明文已經(jīng)被修改了阴汇。一般數(shù)據(jù)庫存儲用戶密碼會將密碼使用MD5進行處理数冬。

HMAC-MD5

HMAC-MD5由一個H函數(shù)和一個密鑰組成,一般我們采用的散列函數(shù)為Md5或者SHA-1搀庶。HMAC-MD5算法就是采用密鑰加密+Md5信息摘要的方式形成新的密文拐纱。

AOP

眾所周知,AOP(面向切面編程)是Spring一個重要特性地来,它將核心關注點和業(yè)務邏輯進行解耦戳玫,將業(yè)務無關的邏輯提取出來作為公共模塊進行處理。它有切點未斑,切面咕宿,連接點,通知的概念蜡秽。切點就是我們可以織入切面的點府阀,切面就是我們要織入的橫切邏輯,通知包含前置通知芽突,后置通知试浙,返回通知,異常通知寞蚌,環(huán)繞通知等田巴。這些aop的概念钠糊,可在其它文章中了解。

加密解密接口

定一個加密解密接口壹哺,并定義一些操作方法抄伍,這樣如果要更改加密或者解密算法的話就可有不同實現(xiàn)。

public interface CryptSignHandler<T, R> {

    /**
     * 結果加密
     * @param data
     * @return
     */
    String encrypt(Object data);

    /**
     * 請求解密
     * @param data
     * @return
     */
    String decrypt(String data);

    /**
     * 校驗請求簽名
     * @return
     */
    void checkSign(T req);

    /**
     * 結果生成簽名
     * @param res
     * @return
     */
    String sign(R res);
}

加密解密實現(xiàn)

在博主的項目中管宵,采用的是128位截珍,CBC加密鏈模式,PKCS5填充模式, BASE64編碼的AES對稱加密算法箩朴。使用HMAC-MD5進行簽名岗喉。算法工具包引入的是Hu-tool,CryptSignHandle接口實現(xiàn)

public class CryptSignHandler implements CryptSignHandler<RequestDTO, ResultDataDTO>{
    
    @Override
    public String encrypt(Object data) {
        return encryptData(JSONUtil.toJsonStr(data));
    }

    @Override
    public String decrypt(String data) {
        return decryptData(data);
    }

    @Override
    public void checkSign(RequestDTO req) {
        String requestStr = req.getOperatorID() + req.getData() + req.getTimeStamp() + req.getSeq();
        String sign = sign(requestStr);
        if(!StrUtil.equals(sign, req.getSig())){
            throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.SIG_ERROR.getCode(),RetCodeEnum.SIG_ERROR.getName()));
        }
    }

    @Override
    public String sign(ResultDataDTO result) {
        String sign = sign(result);
        return sign;
    }

    /**
     * 獲取AES對象
     * @return
     */
    public static AES getAes(){
        return new AES(Mode.CBC, Padding.PKCS5Padding, getAesSecretKey().getBytes(), getAesIv().getBytes());
    }

    /**
     * 加密
     * @param data
     * @return
     */
    public String encryptData(Object data){
        if(ObjectUtil.isNull(data)){
            return "";
        }
        return getAes().encryptBase64(JSONUtil.toJsonStr(data));
    }

    /**
     * 解密
     * @param encryptData
     * @return
     */
    public static String decryptData(String encryptData){
        if(StrUtil.isEmpty(encryptData)){
            return "";
        }
        return getAes().decryptStr(encryptData);
    }

    /**
     * 獲取hmac對象
     * @return
     */
    public static HMac getHMac(){
        return new HMac(HmacAlgorithm.HmacMD5, getHmacMd5SignKey().getBytes());
    }
    
    /**
     * 生成簽名
     * @param str
     * @return
     */
    public static String sign(String str){
        return getHMac().digestHex(str).toUpperCase();
    }

}
自定義注解

如果要對加密解密進行統(tǒng)一處理炸庞,需要指定參數(shù)的基類钱床,進行加密解密的字段名,響應參數(shù)基類埠居,進行簽名設置的字段名诞丽,實現(xiàn)接口等。在需要進行加密解密操作的方法上加上該注解拐格,表示需要對請求參數(shù)和響應結果進行加密,解密刑赶,簽名驗證等捏浊。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CryptAndSign {

    // 請求參數(shù)基類
    Class requestVO() default RequestDTO.class;
    // 響應參數(shù)基類
    Class responseVO() default ResultDataDTO.class;
    // 進行加密解密的字段名
    String cryptFieldName() default "Data";
    // 進行簽名設置的字段名
    String signFieldName() default "Sig";
    // 加密,解密撞叨,簽名
    Class<? extends CryptSignHandler> cryptSignHandler() default CryptSignHandler.class;
}

RequestDTO 請求參數(shù)基類如下

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestDTO<T> implements Serializable {

    @JsonProperty("OperatorID")
    private String OperatorID;
    @JsonProperty("Data")
    private T Data;
    @JsonProperty("TimeStamp")
    private String TimeStamp;
    @JsonProperty("Sig")
    private String Sig;
    @JsonProperty("Seq")
    private String Seq;
}

ResultDataDTO 響應結果基類如下

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResultDTO implements Serializable {

    private String Ret;
    private String Msg;
    private String Data;
    private String Sig;
}

AOP環(huán)繞通知操作

新增CryptAndSignAOP定義切面邏輯金踪,在方法執(zhí)行前攔截請求參數(shù)對參數(shù)中的data字段進行解密,并校驗簽名的準確性牵敷。在方法執(zhí)行后對data字段進行加密胡岔,并生成簽名賦予sig字段。

@Aspect
@Component
@Slf4j
public class CryptAndSignAOP {

    /**
     * 定義切點
     */
    @Pointcut("@within(com.annotation.CryptAndSign) || @annotation(com.annotation.CryptAndSign)")
    public void pointcut(){

    }

    /**
     * 定義環(huán)繞切面
     * @param point
     * @return
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point){
        Object result = null;
        // 獲取被代理的對象
        Object target = point.getTarget();
        // 獲取被代理方法參數(shù)
        Object[] args = point.getArgs();
        // 獲取通知簽名
        MethodSignature signature = (MethodSignature) point.getSignature();

        try {
            // 獲取被代理方法
            Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 獲取被代理方法上的@CryptAndSign注解
            CryptAndSign cryptAndSign = pointMethod.getAnnotation(CryptAndSign.class);
            // 獲取被代理類上的@CryptAndSign注解
            if(ObjectUtil.isNull(cryptAndSign)){
                cryptAndSign = target.getClass().getAnnotation(CryptAndSign.class);
            }
            // 獲取加密解密實現(xiàn)
            CryptSignHandler cryptSignObj = null;

            if(ObjectUtil.isNotNull(cryptAndSign)){
                // 獲取參數(shù)加密基類
                Class clazz = cryptAndSign.requestVO();
                cryptSignObj = (CryptSignHandler) cryptAndSign.cryptSignHandler().newInstance();
                for(Object arg : args){
                    if(clazz.isInstance(arg)){
                        Object cast = clazz.cast(arg);
                        // 驗證請求參數(shù)簽名
                        cryptSignObj.checkSign(cast);
                        // 獲取加密解密字段名
                        String cryptFieldName = cryptAndSign.cryptFieldName();
                        // 執(zhí)行方法獲取加密數(shù)據(jù)
                        String encryptData = (String) getFieldValue(clazz, cast, cryptFieldName);
                        if(StringUtil.isNotEmpty(encryptData)){
                            String decryptData = cryptSignObj.decrypt(encryptData);
                            setFieldValue(clazz, cast, cryptFieldName, decryptData);
                        }
                    }
                }
            }

            // 執(zhí)行請求
            log.info("----[" + pointMethod.getName() + "]---> requestDTO = [{}]", JSONUtil.toJsonStr(args));
            result = point.proceed(args);
            log.info("----[" + pointMethod.getName() + "]---> responseDTO = [{}]", JSONUtil.toJsonStr(result));

            if(ObjectUtil.isNotNull(cryptAndSign)){
                Class clazz = cryptAndSign.responseVO();
                String cryptFieldName = cryptAndSign.cryptFieldName();
                String signName = cryptAndSign.signFieldName();
                Object resultObj = clazz.cast(result);
                // 加密
                Object resultData = getFieldValue(clazz, resultObj, cryptFieldName);
                String encryptData = cryptSignObj.encrypt(resultData);
                setFieldValue(clazz, resultObj, cryptFieldName, encryptData);
                // 生成簽名
                String sign = cryptSignObj.sign(resultObj);
                setFieldValue(clazz, resultObj, signName, sign);
            }

        } catch (OptimusExceptionBase e){
            throw e;
        } catch (Exception e) {
            log.error("occur an exception, errMsg = [{}]", e.getMessage(), e);
            throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.INTERNAL_ERROR.getCode(), RetCodeEnum.INTERNAL_ERROR.getName()));
        } catch (Throwable throwable) {
            log.error("occur an exception, errMsg = [{}]", throwable.getMessage(), throwable);
            throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.INTERNAL_ERROR.getCode(), RetCodeEnum.INTERNAL_ERROR.getName()));
        }

        return result;
    }


    /**
     * 獲取字段值
     * @param clazz
     * @param obj
     * @param fieldName
     * @return
     */
    public static Object getFieldValue(Class clazz, Object obj, String fieldName){
        try {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            log.error("get field value occur an exception, errMsg = [{}]", e.getMessage(), e);
        }
        return null;
    }

    /**
     * 設置字段值
     * @param clazz
     * @param obj
     * @param fieldName
     * @param value
     */
    public static void setFieldValue(Class clazz, Object obj, String fieldName, Object value){
        try {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, value);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            log.error("set field value occur an exception, errMsg = [{}]", e.getMessage(), e);
        }
    }

}
定義方法

在controller中新增方法枷餐,加上@CryptAndSign注解靶瘸,標示需要加密解密,簽名驗證等操作毛肋。

    @CryptAndSign
    @PostMapping("/api/callback/notification_start_charge_result")
    public ResultDataDTO notifyStartChargeResult(@RequestBody RequestDTO<String> requestDTO){
        RequestDTO<StartChargeNotifyRequestDTO> request = CallbackUtil.convertRequestDTO(requestDTO, new TypeReference<StartChargeNotifyRequestDTO>() {});
        StartChargeResultParamValidator.validate(request);
        return CallbackService.notifyStartChargeResult(request.getData());
    }

總結

在本文中介紹了加密怨咪,解密,簽名等幾本概念润匙,以及介紹了如何使用apo進行統(tǒng)一的參數(shù)解密诗眨,結果加密等操作。希望對大家有所幫助孕讳。

參考

http://www.reibang.com/p/3840b344b27c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末匠楚,一起剝皮案震驚了整個濱河市巍膘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芋簿,老刑警劉巖峡懈,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異益咬,居然都是意外死亡逮诲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門幽告,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梅鹦,“玉大人,你說我怎么就攤上這事冗锁∑胨簦” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵冻河,是天一觀的道長箍邮。 經(jīng)常有香客問我,道長叨叙,這世上最難降的妖魔是什么锭弊? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮擂错,結果婚禮上味滞,老公的妹妹穿的比我還像新娘。我一直安慰自己钮呀,他們只是感情好剑鞍,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爽醋,像睡著了一般蚁署。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚂四,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天光戈,我揣著相機與錄音,去河邊找鬼遂赠。 笑死田度,一個胖子當著我的面吹牛,可吹牛的內容都是我干的解愤。 我是一名探鬼主播镇饺,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼送讲!你這毒婦竟也來了奸笤?” 一聲冷哼從身側響起惋啃,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎监右,沒想到半個月后边灭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡健盒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年绒瘦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扣癣。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡惰帽,死狀恐怖,靈堂內的尸體忽然破棺而出父虑,到底是詐尸還是另有隱情该酗,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布士嚎,位于F島的核電站呜魄,受9級特大地震影響,放射性物質發(fā)生泄漏莱衩。R本人自食惡果不足惜爵嗅,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望笨蚁。 院中可真熱鬧操骡,春花似錦、人聲如沸赚窃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勒极。三九已至,卻和暖如春虑鼎,著一層夾襖步出監(jiān)牢的瞬間辱匿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工炫彩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匾七,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓江兢,卻偏偏與公主長得像昨忆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子杉允,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內容