JAVA實現(xiàn)sm3加密簽名以及防止重復(fù)攻擊
背景:
后端開發(fā)基本都遇到過使用簽名校驗的情況,簽名的作用是為了防止請求數(shù)據(jù)被別人截取篡改重新請求。
為什么簽名驗證可以防止請求數(shù)據(jù)被篡改,因為一般簽名的規(guī)則就是,你的所有請求參數(shù),按照約定好的格式進(jìn)行拼接,后面得到一個拼接后的字符串,這個字符串后面再加上一個雙方私下確認(rèn)后的簽名秘鑰(固定),最后組合成一個待簽名字符串,用這個字符串進(jìn)行sm3加密。sm3加密是個不可逆的加密(理論上),請求方把這個加密后面簽名字段放到請求頭中,服務(wù)提供方本地根據(jù)相同的方法進(jìn)行組合簽名字符串和加密,然后對面本地加密的簽名和請求頭中的簽名是否相同,相同則認(rèn)為這個數(shù)據(jù)是可靠的,未被篡改過的(加密前數(shù)據(jù)不同,加密后的簽名肯定不同)。
PS:簽名只能驗證防止請求數(shù)據(jù)被篡改,并不能說你把數(shù)據(jù)加密讓別人看不見,只要是互聯(lián)網(wǎng)上傳輸,數(shù)據(jù)就可能被別人獲取到
簽名優(yōu)化:
請求頭加上時間戳,代碼上驗證請求的時間戳和當(dāng)前時間戳?xí)r間差距,差距過大就拒絕請求(比如只接受一分鐘內(nèi)請求),同時把這個時間戳加到簽名字符串去,這樣簽名的數(shù)據(jù)內(nèi)容就包含時間戳(可以防止別人用同一份簽名發(fā)起重復(fù)請求攻擊,因為時間戳是一直在變的,簽名中的時間戳和請求報文頭的時間戳不同,驗證簽名就不會通過),這樣就可以做到即使有人獲取到了某個請求的參數(shù)和簽名,以此發(fā)起多次重復(fù)請求攻擊,這種攻擊最多只有一分鐘的有效期。
代碼:
package com.dw.task.utils; import cn.hutool.crypto.SmUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.util.Objects; @Slf4j @Component public class Sm3UtilHua { private static Integer httpCheckSignTimeOut = 1;//請求簽名有效時間 默認(rèn)1分鐘 /** * 獲取實體類拼成的加密字段 * @param checkSign 前端傳入的簽名(從請求報文頭獲?。? * @param signModel 查詢的DTO模型類 * @param privateKey 簽名加密私鑰 * @param timestamp 時間戳(從請求報文頭獲?。? * @return 比對結(jié)果 */ public boolean checkSign(String checkSign , Object signModel , String privateKey,Long timestamp) throws Exception { Long thisTime = System.currentTimeMillis() - timestamp; Integer checkSignTimeOut = httpCheckSignTimeOut; if (!(Objects.isNull(checkSignTimeOut) || checkSignTimeOut.intValue() == 0)){ //時間為0或者未配置簽名超時時間,默認(rèn)不驗證時間戳 if(thisTime >= 60*1000*checkSignTimeOut || thisTime<=0){ //checkSignTimeOut分鐘內(nèi)的時間戳才處理 log.error("時間戳異常,非"+checkSignTimeOut+"分鐘內(nèi)請求,當(dāng)前時間戳:"+System.currentTimeMillis()); return false; } } String signValue = getSignValue(signModel) + "×tamp=" + timestamp +"&privateKey=" +privateKey; String sign = getSign(signValue); log.info("【本地加密后 sm3 簽名】" + sign);//生產(chǎn)上建議注釋此行,防止泄露 return sign.toUpperCase().equals(checkSign.toUpperCase())? true : false; } /** * 加密簽名 * @param signValue 待加密簽名字符串 * @return 加密后簽名字符串 */ public String getSign(String signValue){ return SmUtil.sm3(signValue); } /** * 獲取實體類拼成的加密字段 * @param classA 傳入?yún)?shù)實體類 * @return 待加密字符串 */ public String getSignValue(Object classA) { Field[] fs = classA.getClass().getDeclaredFields();//獲取所有屬性 String[][] temp = new String[fs.length][2]; //用二維數(shù)組保存 參數(shù)名和參數(shù)值 for (int i=0; i<fs.length; i++) { fs[i].setAccessible(true); temp[i][0] = fs[i].getName().toLowerCase(); //獲取屬性名 try { temp[i][1] = String.valueOf(fs[i].get(classA)) ;//把屬性值放進(jìn)數(shù)組 } catch (Exception e) { log.error("【簽名字段:"+fs[i].getName()+"添加失敗】"); } } temp = doChooseSort(temp); //對參數(shù)實體類按照字母順序排續(xù) String result = ""; for (int i = 0; i < temp.length; i++) {//按照簽名規(guī)則生成待加密字符串 result = result + temp[i][0]+"="+temp[i][1]+"&"; } result = result.substring(0, result.length()-1);//消除掉最后的“&” log.info("【簽名信息】{}" ,result); return result; } /** * 對二維數(shù)組里面的數(shù)據(jù)進(jìn)行選擇排序,按字段名按abcd順序排列 * @param data 未按照字母順序排序的二維數(shù)組 * @return */ private String[][] doChooseSort(String[][] data) {//排序方式為選擇排序 String[][] temp = new String[data.length][2]; temp = data; int n = temp.length; for (int i = 0; i < n-1; i++) { int k = i;// 初始化最小值的小標(biāo) for (int j = i+1; j<n; j++) { if (temp[k][0].compareTo(temp[j][0]) > 0) { //下標(biāo)k字段名大于當(dāng)前字段名 k = j;// 修改最大值的小標(biāo) } } // 將最小值放到排序序列末尾 if (k > i) { //用相加相減法交換data[i] 和 data[k] String tempValue ; tempValue = temp[k][0]; temp[k][0] = temp[i][0]; temp[i][0] = tempValue; tempValue = temp[k][1]; temp[k][1] = temp[i][1]; temp[i][1] = tempValue; } } return temp; } }
測試代碼:
public static void main(String[] args) throws Exception { //模擬請求參數(shù) QueryDTO dto = new QueryDTO(); dto.setName("張三"); dto.setAge(18); dto.setIdCard("666666666666666"); //模擬請求報文頭時間戳 Long nowTime = System.currentTimeMillis(); //簽名加密私鑰(不要在互聯(lián)網(wǎng)上傳輸,調(diào)用方和提供方私下物理確認(rèn)和設(shè)置) String privateKey = "48d95af20fa1bc438db42e280085707b60841c"; Sm3UtilHua sm3UtilHua = new Sm3UtilHua(); System.out.println("1:請求簽名錯誤的案例"); //模擬請求錯誤的簽名 String errorSign = "2222222222222222222"; System.out.println("驗簽校驗結(jié)果:" + sm3UtilHua.checkSign(errorSign,dto, privateKey,nowTime)); System.out.println(); System.out.println("2:請求簽名正確的案例"); String signValue = sm3UtilHua.getSignValue(dto) + "×tamp=" + nowTime +"&privateKey=" +privateKey;//復(fù)制上面1請求的加密后簽名 String trueSign = sm3UtilHua.getSign(signValue); System.out.println("驗簽校驗結(jié)果:" + sm3UtilHua.checkSign(trueSign,dto, privateKey,nowTime)); System.out.println(); System.out.println("3:請求簽名正確,但是時間非一分鐘內(nèi)請求的案例"); Long oldTime = 1688036145560L;//這個時間戳大概是2023年06月29號的時間戳,所以必不是當(dāng)前一分鐘內(nèi)時間 System.out.println("驗簽校驗結(jié)果:" + sm3UtilHua.checkSign(trueSign,dto, privateKey,oldTime)); }
測試結(jié)果:
下面的簽名信息打印是不完整的,沒有拼接時間戳和簽名私鑰,也是防止打印到日志然后泄露
1:請求簽名錯誤的案例
10:42:11.506 [main] ERROR com.dw.task.utils.Sm3UtilHua - 時間戳異常,非1分鐘內(nèi)請求,當(dāng)前時間戳:1688092931498
驗簽校驗結(jié)果:false2:請求簽名正確的案例
10:42:11.522 [main] INFO com.dw.task.utils.Sm3UtilHua - 【簽名信息】age=18&idcard=666666666666666&name=張三
10:42:12.358 [main] INFO com.dw.task.utils.Sm3UtilHua - 【簽名信息】age=18&idcard=666666666666666&name=張三
10:42:12.359 [main] INFO com.dw.task.utils.Sm3UtilHua - 【本地加密后 sm3 簽名】e60bf8ea2453f44a4a6d3b43f55399c2ce09a6f5b4be68378506d95d2d6f4491
驗簽校驗結(jié)果:true3:請求簽名正確,但是時間非一分鐘內(nèi)請求的案例
10:42:12.359 [main] ERROR com.dw.task.utils.Sm3UtilHua - 時間戳異常,非1分鐘內(nèi)請求,當(dāng)前時間戳:1688092932359
驗簽校驗結(jié)果:false
引申優(yōu)化:
上面的代碼已經(jīng)可以防止一定程度的重復(fù)請求攻擊,但是一分鐘內(nèi)的重復(fù)請求攻擊還是無法防止的,如果別人截取請求信息后一分鐘內(nèi)發(fā)起大量重復(fù)請求的話,還是會通過簽名并且穿透到業(yè)務(wù)層,對此呢如果要再優(yōu)化,就可以把每次驗證成功的簽名放到redis緩存中,數(shù)據(jù)有效期是1分鐘。這樣每次在驗簽之前先查詢redis緩存中是否有相同的簽名,有即代表這個請求是重復(fù)請求,直接攔截
思路就是這樣,代碼就不寫了
總結(jié)
到此這篇關(guān)于JAVA實現(xiàn)sm3加密簽名以及防止重復(fù)攻擊的文章就介紹到這了,更多相關(guān)JAVA sm3加密簽名內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java利用openoffice將doc、docx轉(zhuǎn)為pdf實例代碼
這篇文章主要介紹了Java利用openoffice將doc、docx轉(zhuǎn)為pdf實例代碼,分享了相關(guān)代碼示例,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-01-01Java Apollo環(huán)境搭建以及集成SpringBoot案例詳解
這篇文章主要介紹了Java Apollo環(huán)境搭建以及集成SpringBoot案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08MybatisPlus中QueryWrapper常用方法總結(jié)
MyBatis-Plus是一個Mybatis增強版工具,在MyBatis上擴(kuò)充了其他功能沒有改變其基本功能,為了簡化開發(fā)提交效率而存在,queryWrapper是mybatis plus中實現(xiàn)查詢的對象封裝操作類,本文就給大家總結(jié)了MybatisPlus中QueryWrapper的常用方法,需要的朋友可以參考下2023-07-07一分鐘入門Java Spring Boot徹底解決SSM配置問題
Spring Boot是由Pivotal團(tuán)隊提供的全新框架,其設(shè)計目的是用來簡化新Spring應(yīng)用的初始搭建以及開發(fā)過程。該框架使用了特定的方式來進(jìn)行配置,從而使開發(fā)人員不再需要定義樣板化的配置。通過這種方式,Spring Boot致力于在蓬勃發(fā)展的快速應(yīng)用開發(fā)領(lǐng)域成為領(lǐng)導(dǎo)者2021-10-10