JAVA實(shí)現(xiàn)sm3加密簽名以及防止重復(fù)攻擊
背景:
后端開發(fā)基本都遇到過使用簽名校驗(yàn)的情況,簽名的作用是為了防止請求數(shù)據(jù)被別人截取篡改重新請求。
為什么簽名驗(yàn)證可以防止請求數(shù)據(jù)被篡改,因?yàn)橐话愫灻囊?guī)則就是,你的所有請求參數(shù),按照約定好的格式進(jìn)行拼接,后面得到一個(gè)拼接后的字符串,這個(gè)字符串后面再加上一個(gè)雙方私下確認(rèn)后的簽名秘鑰(固定),最后組合成一個(gè)待簽名字符串,用這個(gè)字符串進(jìn)行sm3加密。sm3加密是個(gè)不可逆的加密(理論上),請求方把這個(gè)加密后面簽名字段放到請求頭中,服務(wù)提供方本地根據(jù)相同的方法進(jìn)行組合簽名字符串和加密,然后對面本地加密的簽名和請求頭中的簽名是否相同,相同則認(rèn)為這個(gè)數(shù)據(jù)是可靠的,未被篡改過的(加密前數(shù)據(jù)不同,加密后的簽名肯定不同)。
PS:簽名只能驗(yàn)證防止請求數(shù)據(jù)被篡改,并不能說你把數(shù)據(jù)加密讓別人看不見,只要是互聯(lián)網(wǎng)上傳輸,數(shù)據(jù)就可能被別人獲取到
簽名優(yōu)化:
請求頭加上時(shí)間戳,代碼上驗(yàn)證請求的時(shí)間戳和當(dāng)前時(shí)間戳?xí)r間差距,差距過大就拒絕請求(比如只接受一分鐘內(nèi)請求),同時(shí)把這個(gè)時(shí)間戳加到簽名字符串去,這樣簽名的數(shù)據(jù)內(nèi)容就包含時(shí)間戳(可以防止別人用同一份簽名發(fā)起重復(fù)請求攻擊,因?yàn)闀r(shí)間戳是一直在變的,簽名中的時(shí)間戳和請求報(bào)文頭的時(shí)間戳不同,驗(yàn)證簽名就不會通過),這樣就可以做到即使有人獲取到了某個(gè)請求的參數(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;//請求簽名有效時(shí)間 默認(rèn)1分鐘
/**
* 獲取實(shí)體類拼成的加密字段
* @param checkSign 前端傳入的簽名(從請求報(bào)文頭獲?。?
* @param signModel 查詢的DTO模型類
* @param privateKey 簽名加密私鑰
* @param timestamp 時(shí)間戳(從請求報(bào)文頭獲取)
* @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)){
//時(shí)間為0或者未配置簽名超時(shí)時(shí)間,默認(rèn)不驗(yàn)證時(shí)間戳
if(thisTime >= 60*1000*checkSignTimeOut || thisTime<=0){
//checkSignTimeOut分鐘內(nèi)的時(shí)間戳才處理
log.error("時(shí)間戳異常,非"+checkSignTimeOut+"分鐘內(nèi)請求,當(dāng)前時(shí)間戳:"+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);
}
/**
* 獲取實(shí)體類拼成的加密字段
* @param classA 傳入?yún)?shù)實(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ù)實(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");
//模擬請求報(bào)文頭時(shí)間戳
Long nowTime = System.currentTimeMillis();
//簽名加密私鑰(不要在互聯(lián)網(wǎng)上傳輸,調(diào)用方和提供方私下物理確認(rèn)和設(shè)置)
String privateKey = "48d95af20fa1bc438db42e280085707b60841c";
Sm3UtilHua sm3UtilHua = new Sm3UtilHua();
System.out.println("1:請求簽名錯(cuò)誤的案例");
//模擬請求錯(cuò)誤的簽名
String errorSign = "2222222222222222222";
System.out.println("驗(yàn)簽校驗(yàn)結(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("驗(yàn)簽校驗(yàn)結(jié)果:" + sm3UtilHua.checkSign(trueSign,dto, privateKey,nowTime));
System.out.println();
System.out.println("3:請求簽名正確,但是時(shí)間非一分鐘內(nèi)請求的案例");
Long oldTime = 1688036145560L;//這個(gè)時(shí)間戳大概是2023年06月29號的時(shí)間戳,所以必不是當(dāng)前一分鐘內(nèi)時(shí)間
System.out.println("驗(yàn)簽校驗(yàn)結(jié)果:" + sm3UtilHua.checkSign(trueSign,dto, privateKey,oldTime));
}測試結(jié)果:
下面的簽名信息打印是不完整的,沒有拼接時(shí)間戳和簽名私鑰,也是防止打印到日志然后泄露
1:請求簽名錯(cuò)誤的案例
10:42:11.506 [main] ERROR com.dw.task.utils.Sm3UtilHua - 時(shí)間戳異常,非1分鐘內(nèi)請求,當(dāng)前時(shí)間戳:1688092931498
驗(yàn)簽校驗(yàn)結(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
驗(yàn)簽校驗(yàn)結(jié)果:true3:請求簽名正確,但是時(shí)間非一分鐘內(nèi)請求的案例
10:42:12.359 [main] ERROR com.dw.task.utils.Sm3UtilHua - 時(shí)間戳異常,非1分鐘內(nèi)請求,當(dāng)前時(shí)間戳:1688092932359
驗(yàn)簽校驗(yàn)結(jié)果:false
引申優(yōu)化:
上面的代碼已經(jīng)可以防止一定程度的重復(fù)請求攻擊,但是一分鐘內(nèi)的重復(fù)請求攻擊還是無法防止的,如果別人截取請求信息后一分鐘內(nèi)發(fā)起大量重復(fù)請求的話,還是會通過簽名并且穿透到業(yè)務(wù)層,對此呢如果要再優(yōu)化,就可以把每次驗(yàn)證成功的簽名放到redis緩存中,數(shù)據(jù)有效期是1分鐘。這樣每次在驗(yàn)簽之前先查詢r(jià)edis緩存中是否有相同的簽名,有即代表這個(gè)請求是重復(fù)請求,直接攔截
思路就是這樣,代碼就不寫了
總結(jié)
到此這篇關(guān)于JAVA實(shí)現(xiàn)sm3加密簽名以及防止重復(fù)攻擊的文章就介紹到這了,更多相關(guān)JAVA sm3加密簽名內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea如何為java程序添加啟動(dòng)參數(shù)
文章介紹了如何在Java程序中添加啟動(dòng)參數(shù),包括program arguments、VM arguments和Environment variables,并解釋了如何在代碼中使用System類獲取這些參數(shù)2025-01-01
Java利用openoffice將doc、docx轉(zhuǎn)為pdf實(shí)例代碼
這篇文章主要介紹了Java利用openoffice將doc、docx轉(zhuǎn)為pdf實(shí)例代碼,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
Java Apollo環(huán)境搭建以及集成SpringBoot案例詳解
這篇文章主要介紹了Java Apollo環(huán)境搭建以及集成SpringBoot案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
MybatisPlus中QueryWrapper常用方法總結(jié)
MyBatis-Plus是一個(gè)Mybatis增強(qiáng)版工具,在MyBatis上擴(kuò)充了其他功能沒有改變其基本功能,為了簡化開發(fā)提交效率而存在,queryWrapper是mybatis plus中實(shí)現(xiàn)查詢的對象封裝操作類,本文就給大家總結(jié)了MybatisPlus中QueryWrapper的常用方法,需要的朋友可以參考下2023-07-07
一分鐘入門Java Spring Boot徹底解決SSM配置問題
Spring Boot是由Pivotal團(tuán)隊(duì)提供的全新框架,其設(shè)計(jì)目的是用來簡化新Spring應(yīng)用的初始搭建以及開發(fā)過程。該框架使用了特定的方式來進(jìn)行配置,從而使開發(fā)人員不再需要定義樣板化的配置。通過這種方式,Spring Boot致力于在蓬勃發(fā)展的快速應(yīng)用開發(fā)領(lǐng)域成為領(lǐng)導(dǎo)者2021-10-10

