RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

怎樣去設(shè)計(jì)一個(gè)安全好用的OpenApi呢?

jf_ro2CN3Fa ? 來源:架構(gòu)師 ? 2023-11-06 09:49 ? 次閱讀

一、AppId和AppSecret

AppId的使用

AppId作為一種全局唯一的標(biāo)識(shí)符,其作用主要在于方便用戶身份識(shí)別以及數(shù)據(jù)分析等方面。為了防止其他用戶通過惡意使用別人的AppId來發(fā)起請(qǐng)求,一般都會(huì)采用配對(duì)AppSecret的方式,類似于一種密碼。AppId和AppSecret通常會(huì)組合生成一套簽名,并按照一定規(guī)則進(jìn)行加密處理。在請(qǐng)求方發(fā)起請(qǐng)求時(shí),需要將這個(gè)簽名值一并提交給提供方進(jìn)行驗(yàn)證。如果簽名驗(yàn)證通過,則可以進(jìn)行數(shù)據(jù)交互,否則將被拒絕。這種機(jī)制能夠保證數(shù)據(jù)的安全性和準(zhǔn)確性,提高系統(tǒng)的可靠性和可用性。

AppId的生成

正如前面所說,AppId就是有一個(gè)身份標(biāo)識(shí),生成時(shí)只要保證全局唯一即可。

AppSecret生成

AppSecret就是密碼,按照一般的的密碼安全性要求生成即可。

二、sign簽名

RSASignature

首先,在介紹簽名方式之前,我們必須先了解2個(gè)概念,分別是:非對(duì)稱加密算法(比如:RSA)、摘要算法(比如:MD5)。

簡(jiǎn)單來說,非對(duì)稱加密的應(yīng)用場(chǎng)景一般有兩種,一種是公鑰加密,私鑰解密,可以應(yīng)用在加解密場(chǎng)景中(不過由于非對(duì)稱加密的效率實(shí)在不高,用的比較少);還有一種就是結(jié)合摘要算法,把信息經(jīng)過摘要后,再用私鑰加密,公鑰用來解密,可以應(yīng)用在簽名場(chǎng)景中,也是我們將要使用到的方式。

大致看看RSASignature簽名的方式,稍后用到SHA256withRSA底層就是使用的這個(gè)方法。

045c37ae-7bbc-11ee-939d-92fbcf53809c.jpg

摘要算法與非對(duì)稱算法的最大區(qū)別就在于,它是一種不需要密鑰的且不可逆的算法,也就是一旦明文數(shù)據(jù)經(jīng)過摘要算法計(jì)算后,得到的密文數(shù)據(jù)一定是不可反推回來的。

簽名的作用

好了,現(xiàn)在我們?cè)賮砜纯春灻?,簽名主要可以用在兩個(gè)場(chǎng)景,一種是數(shù)據(jù)防篡改,一種是身份防冒充,實(shí)際上剛好可以對(duì)應(yīng)上前面我們介紹的兩種算法。

數(shù)據(jù)防篡改

顧名思義,就是防止數(shù)據(jù)在網(wǎng)絡(luò)傳輸過程中被修改,摘要算法可以保證每次經(jīng)過摘要算法的原始數(shù)據(jù),計(jì)算出來的結(jié)果都一樣,所以一般接口提供方只要用同樣的原數(shù)據(jù)經(jīng)過同樣的摘要算法,然后與接口請(qǐng)求方生成的數(shù)據(jù)進(jìn)行比較,如果一致則表示數(shù)據(jù)沒有被篡改過。

身份防冒充

這里身份防冒充,我們就要使用另一種方式,比如SHA256withRSA,其實(shí)現(xiàn)原理就是先用數(shù)據(jù)進(jìn)行SHA256計(jì)算,然后再使用RSA私鑰加密,對(duì)方解的時(shí)候也一樣,先用RSA公鑰解密,然后再進(jìn)行SHA256計(jì)算,最后看結(jié)果是否匹配。

三、使用示例

前置準(zhǔn)備

在沒有自動(dòng)化開放平臺(tái)時(shí),appId、appSecret可直接通過線下的方式給到接入方,appSecret需要接入方自行保存好,避免泄露。也可以自行

公私鑰可以由接口提供方來生成,同樣通過線下的方式,把私鑰交給對(duì)方,并要求對(duì)方需保密。

交互流程

0473a4ac-7bbc-11ee-939d-92fbcf53809c.jpg

客戶端準(zhǔn)備

接口請(qǐng)求方,首先把業(yè)務(wù)參數(shù),進(jìn)行摘要算法計(jì)算,生成一個(gè)簽名(sign)

//業(yè)務(wù)請(qǐng)求參數(shù)
UserEntityuserEntity=newUserEntity();
userEntity.setUserId("1");
userEntity.setPhone("13912345678");

//使用sha256的方式生成簽名
Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity));
sign=c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5

然后繼續(xù)拼接header部的參數(shù),可以使用&符合連接,使用Set集合完成自然排序,并且過濾參數(shù)為空的key,最后使用私鑰加簽的方式,得到appSign。

Mapdata=Maps.newHashMap();
data.put("appId",appId);
data.put("nonce",nonce);
data.put("sign",sign);
data.put("timestamp",timestamp);
SetkeySet=data.keySet();
String[]keyArray=keySet.toArray(newString[keySet.size()]);
Arrays.sort(keyArray);
StringBuildersb=newStringBuilder();
for(Stringk:keyArray){
if(data.get(k).trim().length()>0)//參數(shù)值為空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
System.out.println("【請(qǐng)求方】拼接后的參數(shù):"+sb.toString());
System.out.println();
【請(qǐng)求方】拼接后的參數(shù):appId=123456&nonce=1234&sign=c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5×tamp=1653057661381&appSecret=654321

【請(qǐng)求方】appSign:m/xk0fkDZlHEkbYSpCPdpbriG/EWG9gNZtInoYOu2RtrLMzHNM0iZe1iL4p/+IedAJN2jgG9pS5o5NZH1i55TVoTbZePdCbR9CEJoHq2TZLIiKPeoRgDimAl14V5jHZiMQCXS8RxWT63W8MKFyZQtB7xCtxVD7+IvLGQOAWn7QX+EmfAUvhgjkaVf2YLk9J9LqtyjfTYeloiP901ZsBZo5y9Gs5P73b+JoEcxmGZRv+Fkv3HnHWTQEpl7W6Lrmd0j44/XupwzHxaanRo5k0ALOVSFohdyMtHk3eOYx/bj+GeMKf8PN4J4tsPndnjyu4XUOnh74aaW9oC2DLiIzr4+Q==

最后把參數(shù)組裝,發(fā)送給接口提供方。

Headerheader=Header.builder()
.appId(appId)
.nonce(nonce)
.sign(sign)
.timestamp(timestamp)
.appSign(appSign)
.build();
APIRequestEntityapiRequestEntity=newAPIRequestEntity();
apiRequestEntity.setHeader(header);
apiRequestEntity.setBody(userEntity);
StringrequestParam=JSONObject.toJSONString(apiRequestEntity);
System.out.println("【請(qǐng)求方】接口請(qǐng)求參數(shù):"+requestParam);
【請(qǐng)求方】接口請(qǐng)求參數(shù):{"body":{"phone":"13912345678","userId":"1"},"header":{"appId":"123456","appSign":"m/xk0fkDZlHEkbYSpCPdpbriG/EWG9gNZtInoYOu2RtrLMzHNM0iZe1iL4p/+IedAJN2jgG9pS5o5NZH1i55TVoTbZePdCbR9CEJoHq2TZLIiKPeoRgDimAl14V5jHZiMQCXS8RxWT63W8MKFyZQtB7xCtxVD7+IvLGQOAWn7QX+EmfAUvhgjkaVf2YLk9J9LqtyjfTYeloiP901ZsBZo5y9Gs5P73b+JoEcxmGZRv+Fkv3HnHWTQEpl7W6Lrmd0j44/XupwzHxaanRo5k0ALOVSFohdyMtHk3eOYx/bj+GeMKf8PN4J4tsPndnjyu4XUOnh74aaW9oC2DLiIzr4+Q==","nonce":"1234","sign":"c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5","timestamp":"1653057661381"}}
0491d198-7bbc-11ee-939d-92fbcf53809c.jpg

服務(wù)端準(zhǔn)備

從請(qǐng)求參數(shù)中,先獲取body的內(nèi)容,然后簽名,完成對(duì)參數(shù)校驗(yàn)

Headerheader=apiRequestEntity.getHeader();
UserEntityuserEntity=JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()),UserEntity.class);
//首先,拿到參數(shù)后同樣進(jìn)行簽名
Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity));
if(!sign.equals(header.getSign())){
thrownewException("數(shù)據(jù)簽名錯(cuò)誤!");
}

從header中獲取相關(guān)信息,并使用公鑰進(jìn)行驗(yàn)簽,完成身份認(rèn)證

//從header中獲取相關(guān)信息,其中appSecret需要自己根據(jù)傳過來的appId來獲取
StringappId=header.getAppId();
StringappSecret=getAppSecret(appId);
Stringnonce=header.getNonce();
Stringtimestamp=header.getTimestamp();
//按照同樣的方式生成appSign,然后使用公鑰進(jìn)行驗(yàn)簽
Mapdata=Maps.newHashMap();
data.put("appId",appId);
data.put("nonce",nonce);
data.put("sign",sign);
data.put("timestamp",timestamp);
SetkeySet=data.keySet();
String[]keyArray=keySet.toArray(newString[keySet.size()]);
Arrays.sort(keyArray);
StringBuildersb=newStringBuilder();
for(Stringk:keyArray){
if(data.get(k).trim().length()>0)//參數(shù)值為空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if(!rsaVerifySignature(sb.toString(),appKeyPair.get(appId).get("publicKey"),header.getAppSign())){
thrownewException("公鑰驗(yàn)簽錯(cuò)誤!");
}
System.out.println();
System.out.println("【提供方】驗(yàn)證通過!");

完整代碼示例

packageopenApi;

importcom.alibaba.fastjson.JSONObject;
importcom.google.common.collect.Maps;
importlombok.SneakyThrows;
importorg.apache.commons.codec.binary.Hex;

importjava.nio.charset.StandardCharsets;
importjava.security.*;
importjava.security.interfaces.RSAPrivateKey;
importjava.security.interfaces.RSAPublicKey;
importjava.security.spec.PKCS8EncodedKeySpec;
importjava.security.spec.X509EncodedKeySpec;
importjava.util.*;


publicclassAppUtils{

/**
*key:appId、value:appSecret
*/
staticMapappMap=Maps.newConcurrentMap();

/**
*分別保存生成的公私鑰對(duì)
*key:appId,value:公私鑰對(duì)
*/
staticMap>appKeyPair=Maps.newConcurrentMap();

publicstaticvoidmain(String[]args)throwsException{
//模擬生成appId、appSecret
StringappId=initAppInfo();

//根據(jù)appId生成公私鑰對(duì)
initKeyPair(appId);

//模擬請(qǐng)求方
StringrequestParam=clientCall();

//模擬提供方驗(yàn)證
serverVerify(requestParam);

}

privatestaticStringinitAppInfo(){
//appId、appSecret生成規(guī)則,依據(jù)之前介紹過的方式,保證全局唯一即可
StringappId="123456";
StringappSecret="654321";
appMap.put(appId,appSecret);
returnappId;
}

privatestaticvoidserverVerify(StringrequestParam)throwsException{
APIRequestEntityapiRequestEntity=JSONObject.parseObject(requestParam,APIRequestEntity.class);
Headerheader=apiRequestEntity.getHeader();
UserEntityuserEntity=JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()),UserEntity.class);

//首先,拿到參數(shù)后同樣進(jìn)行簽名
Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity));
if(!sign.equals(header.getSign())){
thrownewException("數(shù)據(jù)簽名錯(cuò)誤!");
}

//從header中獲取相關(guān)信息,其中appSecret需要自己根據(jù)傳過來的appId來獲取
StringappId=header.getAppId();
StringappSecret=getAppSecret(appId);
Stringnonce=header.getNonce();
Stringtimestamp=header.getTimestamp();

//按照同樣的方式生成appSign,然后使用公鑰進(jìn)行驗(yàn)簽
Mapdata=Maps.newHashMap();
data.put("appId",appId);
data.put("nonce",nonce);
data.put("sign",sign);
data.put("timestamp",timestamp);
SetkeySet=data.keySet();
String[]keyArray=keySet.toArray(newString[keySet.size()]);
Arrays.sort(keyArray);
StringBuildersb=newStringBuilder();
for(Stringk:keyArray){
if(data.get(k).trim().length()>0)//參數(shù)值為空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);


if(!rsaVerifySignature(sb.toString(),appKeyPair.get(appId).get("publicKey"),header.getAppSign())){
thrownewException("公鑰驗(yàn)簽錯(cuò)誤!");
}

System.out.println();
System.out.println("【提供方】驗(yàn)證通過!");

}

publicstaticStringclientCall(){
//假設(shè)接口請(qǐng)求方與接口提供方,已經(jīng)通過其他渠道,確認(rèn)了雙方交互的appId、appSecret
StringappId="123456";
StringappSecret="654321";
Stringtimestamp=String.valueOf(System.currentTimeMillis());
//應(yīng)該為隨機(jī)數(shù),演示隨便寫一個(gè)
Stringnonce="1234";

//業(yè)務(wù)請(qǐng)求參數(shù)
UserEntityuserEntity=newUserEntity();
userEntity.setUserId("1");
userEntity.setPhone("13912345678");

//使用sha256的方式生成簽名
Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity));

Mapdata=Maps.newHashMap();
data.put("appId",appId);
data.put("nonce",nonce);
data.put("sign",sign);
data.put("timestamp",timestamp);
SetkeySet=data.keySet();
String[]keyArray=keySet.toArray(newString[keySet.size()]);
Arrays.sort(keyArray);
StringBuildersb=newStringBuilder();
for(Stringk:keyArray){
if(data.get(k).trim().length()>0)//參數(shù)值為空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);

System.out.println("【請(qǐng)求方】拼接后的參數(shù):"+sb.toString());
System.out.println();

//使用sha256withRSA的方式對(duì)header中的內(nèi)容加簽
StringappSign=sha256withRSASignature(appKeyPair.get(appId).get("privateKey"),sb.toString());
System.out.println("【請(qǐng)求方】appSign:"+appSign);
System.out.println();

//請(qǐng)求參數(shù)組裝
Headerheader=Header.builder()
.appId(appId)
.nonce(nonce)
.sign(sign)
.timestamp(timestamp)
.appSign(appSign)
.build();
APIRequestEntityapiRequestEntity=newAPIRequestEntity();
apiRequestEntity.setHeader(header);
apiRequestEntity.setBody(userEntity);

StringrequestParam=JSONObject.toJSONString(apiRequestEntity);
System.out.println("【請(qǐng)求方】接口請(qǐng)求參數(shù):"+requestParam);

returnrequestParam;
}


/**
*私鑰簽名
*
*@paramprivateKeyStr
*@paramdataStr
*@return
*/
publicstaticStringsha256withRSASignature(StringprivateKeyStr,StringdataStr){
try{
byte[]key=Base64.getDecoder().decode(privateKeyStr);
byte[]data=dataStr.getBytes();
PKCS8EncodedKeySpeckeySpec=newPKCS8EncodedKeySpec(key);
KeyFactorykeyFactory=KeyFactory.getInstance("RSA");
PrivateKeyprivateKey=keyFactory.generatePrivate(keySpec);
Signaturesignature=Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data);
returnnewString(Base64.getEncoder().encode(signature.sign()));
}catch(Exceptione){
thrownewRuntimeException("簽名計(jì)算出現(xiàn)異常",e);
}
}

/**
*公鑰驗(yàn)簽
*
*@paramdataStr
*@parampublicKeyStr
*@paramsignStr
*@return
*@throwsException
*/
publicstaticbooleanrsaVerifySignature(StringdataStr,StringpublicKeyStr,StringsignStr)throwsException{
KeyFactorykeyFactory=KeyFactory.getInstance("RSA");
X509EncodedKeySpecx509EncodedKeySpec=newX509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));
PublicKeypublicKey=keyFactory.generatePublic(x509EncodedKeySpec);
Signaturesignature=Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(dataStr.getBytes());
returnsignature.verify(Base64.getDecoder().decode(signStr));
}

/**
*生成公私鑰對(duì)
*
*@throwsException
*/
publicstaticvoidinitKeyPair(StringappId)throwsException{
KeyPairGeneratorkeyPairGenerator=KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPairkeyPair=keyPairGenerator.generateKeyPair();
RSAPublicKeypublicKey=(RSAPublicKey)keyPair.getPublic();
RSAPrivateKeyprivateKey=(RSAPrivateKey)keyPair.getPrivate();
MapkeyMap=Maps.newHashMap();
keyMap.put("publicKey",newString(Base64.getEncoder().encode(publicKey.getEncoded())));
keyMap.put("privateKey",newString(Base64.getEncoder().encode(privateKey.getEncoded())));
appKeyPair.put(appId,keyMap);
}

privatestaticStringgetAppSecret(StringappId){
returnString.valueOf(appMap.get(appId));
}


@SneakyThrows
publicstaticStringgetSHA256Str(Stringstr){
MessageDigestmessageDigest;
messageDigest=MessageDigest.getInstance("SHA-256");
byte[]hash=messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
returnHex.encodeHexString(hash);
}

}

四、常見防護(hù)手段

timestamp

前面在接口設(shè)計(jì)中,我們使用到了timestamp,這個(gè)參數(shù)主要可以用來防止同一個(gè)請(qǐng)求參數(shù)被無(wú)限期的使用。

稍微修改一下原服務(wù)端校驗(yàn)邏輯,增加了5分鐘有效期的校驗(yàn)邏輯。

privatestaticvoidserverVerify(StringrequestParam)throwsException{
APIRequestEntityapiRequestEntity=JSONObject.parseObject(requestParam,APIRequestEntity.class);
Headerheader=apiRequestEntity.getHeader();
UserEntityuserEntity=JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()),UserEntity.class);
//首先,拿到參數(shù)后同樣進(jìn)行簽名
Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity));
if(!sign.equals(header.getSign())){
thrownewException("數(shù)據(jù)簽名錯(cuò)誤!");
}
//從header中獲取相關(guān)信息,其中appSecret需要自己根據(jù)傳過來的appId來獲取
StringappId=header.getAppId();
StringappSecret=getAppSecret(appId);
Stringnonce=header.getNonce();
Stringtimestamp=header.getTimestamp();

//請(qǐng)求時(shí)間有效期校驗(yàn)
longnow=LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if((now-Long.parseLong(timestamp))/1000/60>=5){
thrownewException("請(qǐng)求過期!");
}

cache.put(appId+"_"+nonce,"1");
//按照同樣的方式生成appSign,然后使用公鑰進(jìn)行驗(yàn)簽
Mapdata=Maps.newHashMap();
data.put("appId",appId);
data.put("nonce",nonce);
data.put("sign",sign);
data.put("timestamp",timestamp);
SetkeySet=data.keySet();
String[]keyArray=keySet.toArray(newString[0]);
Arrays.sort(keyArray);
StringBuildersb=newStringBuilder();
for(Stringk:keyArray){
if(data.get(k).trim().length()>0)//參數(shù)值為空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if(!rsaVerifySignature(sb.toString(),appKeyPair.get(appId).get("publicKey"),header.getAppSign())){
thrownewException("驗(yàn)簽錯(cuò)誤!");
}
System.out.println();
System.out.println("【提供方】驗(yàn)證通過!");
}

nonce

nonce值是一個(gè)由接口請(qǐng)求方生成的隨機(jī)數(shù),在有需要的場(chǎng)景中,可以用它來實(shí)現(xiàn)請(qǐng)求一次性有效,也就是說同樣的請(qǐng)求參數(shù)只能使用一次,這樣可以避免接口重放攻擊。

具體實(shí)現(xiàn)方式:接口請(qǐng)求方每次請(qǐng)求都會(huì)隨機(jī)生成一個(gè)不重復(fù)的nonce值,接口提供方可以使用一個(gè)存儲(chǔ)容器(為了方便演示,我使用的是guava提供的本地緩存,生產(chǎn)環(huán)境中可以使用redis這樣的分布式存儲(chǔ)方式),每次先在容器中看看是否存在接口請(qǐng)求方發(fā)來的nonce值,如果不存在則表明是第一次請(qǐng)求,則放行,并且把當(dāng)前nonce值保存到容器中,這樣,如果下次再使用同樣的nonce來請(qǐng)求則容器中一定存在,那么就可以判定是無(wú)效請(qǐng)求了。

這里可以設(shè)置緩存的失效時(shí)間為5分鐘,因?yàn)榍懊嬗行谝呀?jīng)做了5分鐘的控制。

staticCachecache=CacheBuilder.newBuilder()
.expireAfterWrite(5,TimeUnit.MINUTES)
.build();

privatestaticvoidserverVerify(StringrequestParam)throwsException{
APIRequestEntityapiRequestEntity=JSONObject.parseObject(requestParam,APIRequestEntity.class);
Headerheader=apiRequestEntity.getHeader();
UserEntityuserEntity=JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()),UserEntity.class);
//首先,拿到參數(shù)后同樣進(jìn)行簽名
Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity));
if(!sign.equals(header.getSign())){
thrownewException("數(shù)據(jù)簽名錯(cuò)誤!");
}
//從header中獲取相關(guān)信息,其中appSecret需要自己根據(jù)傳過來的appId來獲取
StringappId=header.getAppId();
StringappSecret=getAppSecret(appId);
Stringnonce=header.getNonce();
Stringtimestamp=header.getTimestamp();
//請(qǐng)求時(shí)間有效期校驗(yàn)
longnow=LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if((now-Long.parseLong(timestamp))/1000/60>=5){
thrownewException("請(qǐng)求過期!");
}
//nonce有效性判斷
Stringstr=cache.getIfPresent(appId+"_"+nonce);
if(Objects.nonNull(str)){
thrownewException("請(qǐng)求失效!");
}
cache.put(appId+"_"+nonce,"1");
//按照同樣的方式生成appSign,然后使用公鑰進(jìn)行驗(yàn)簽
Mapdata=Maps.newHashMap();
data.put("appId",appId);
data.put("nonce",nonce);
data.put("sign",sign);
data.put("timestamp",timestamp);
SetkeySet=data.keySet();
String[]keyArray=keySet.toArray(newString[0]);
Arrays.sort(keyArray);
StringBuildersb=newStringBuilder();
for(Stringk:keyArray){
if(data.get(k).trim().length()>0)//參數(shù)值為空,則不參與簽名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("appSecret=").append(appSecret);
if(!rsaVerifySignature(sb.toString(),appKeyPair.get(appId).get("publicKey"),header.getAppSign())){
thrownewException("驗(yàn)簽錯(cuò)誤!");
}
System.out.println();
System.out.println("【提供方】驗(yàn)證通過!");
}

訪問權(quán)限

數(shù)據(jù)訪問權(quán)限,一般可根據(jù)appId的身份來獲取開放給其的相應(yīng)權(quán)限,要確保每個(gè)appId只能訪問其權(quán)限范圍內(nèi)的數(shù)據(jù)。

參數(shù)合法性校驗(yàn)

參數(shù)的合法性校驗(yàn)應(yīng)該是每個(gè)接口必備的,無(wú)論是前端發(fā)起的請(qǐng)求,還是后端的其他調(diào)用都必須對(duì)參數(shù)做校驗(yàn),比如:參數(shù)的長(zhǎng)度、類型、格式,必傳參數(shù)是否有傳,是否符合約定的業(yè)務(wù)規(guī)則等等。

推薦使用SpringBoot Validation來快速實(shí)現(xiàn)一些基本的參數(shù)校驗(yàn)。

參考如下示例:

@Data
@ToString
publicclassDemoEntity{

//不能為空,比較時(shí)會(huì)除去空格
@NotBlank(message="名稱不能為空")
privateStringname;

//amount必須是一個(gè)大于等于5,小于等于10的數(shù)字
@DecimalMax(value="10")
@DecimalMin(value="5")
privateBigDecimalamount;

//必須符合email格式
@Email
privateStringemail;

//size長(zhǎng)度必須在5到10之間
@Size(max=10,min=5)
privateStringsize;

//age大小必須在18到35之間
@Min(value=18)
@Max(value=35)
privateintage;

//user不能為null
@NotNull
privateUseruser;

//限制必須為小數(shù),且整數(shù)位integer最多2位,小數(shù)位fraction最多為4位
@Digits(integer=2,fraction=4)
privateBigDecimaldigits;

//限制必須為未來的日期
@Future
privateDatefuture;

//限制必須為過期的日期
@Past
privateDatepast;

//限制必須是一個(gè)未來或現(xiàn)在的時(shí)間
@FutureOrPresent
privateDatefutureOrPast;

//支持正則表達(dá)式
@Pattern(regexp="^\d+$")
privateStringdigit;
}
@RestController
@Slf4j
@RequestMapping("/valid")
publicclassTestValidController{

@RequestMapping("/demo1")
publicStringdemo12(@Validated@RequestBodyDemoEntitydemoEntity){
try{
return"SUCCESS";
}catch(Exceptione){
log.error(e.getMessage(),e);
return"FAIL";
}
}
}

限流保護(hù)

在設(shè)計(jì)接口時(shí),我們應(yīng)當(dāng)對(duì)接口的負(fù)載能力做出評(píng)估,尤其是開放給外部使用時(shí),這樣當(dāng)實(shí)際請(qǐng)求流量超過預(yù)期流量時(shí),我們便可采取相應(yīng)的預(yù)防策略,以免服務(wù)器崩潰。

一般來說限流主要是為了防止惡意刷站請(qǐng)求,爬蟲等非正常的業(yè)務(wù)訪問,因此一般來說采取的方式都是直接丟棄超出閾值的部分。

限流的具體實(shí)現(xiàn)有多種,單機(jī)版可以使用Guava的RateLimiter,分布式可以使用Redis,想要更加完善的成套解決方案則可以使用阿里開源的Sentinel。

敏感數(shù)據(jù)訪問

敏感信息一般包含,身份證、手機(jī)號(hào)、銀行卡號(hào)、車牌號(hào)、姓名等等,應(yīng)該按照脫敏規(guī)則進(jìn)行處理。

白名單機(jī)制

使用白名單機(jī)制可以進(jìn)一步加強(qiáng)接口的安全性,一旦服務(wù)與服務(wù)交互可以使用,接口提供方可以限制只有白名單內(nèi)的IP才能訪問,這樣接口請(qǐng)求方只要把其出口IP提供出來即可。

黑名單機(jī)制

與之對(duì)應(yīng)的黑名單機(jī)制,則是應(yīng)用在服務(wù)端與客戶端的交互,由于客戶端IP都是不固定的,所以無(wú)法使用白名單機(jī)制,不過我們依然可以使用黑名單攔截一些已經(jīng)被識(shí)別為非法請(qǐng)求的IP。

五、其他考慮

名稱和描述:API 的名稱和描述應(yīng)該簡(jiǎn)潔明了,并清晰地表明其功能和用途。

請(qǐng)求和響應(yīng):API 應(yīng)該支持標(biāo)準(zhǔn)的 HTTP 請(qǐng)求方法,如 GET、POST、PUT 和 DELETE,并定義這些方法的參數(shù)和響應(yīng)格式。

錯(cuò)誤處理:API 應(yīng)該定義各種錯(cuò)誤碼,并提供有關(guān)錯(cuò)誤的詳細(xì)信息。

文檔和示例:API 應(yīng)該提供文檔和示例,以幫助開發(fā)人員了解如何使用該 API,并提供示例數(shù)據(jù)以進(jìn)行測(cè)試。

可擴(kuò)展:API應(yīng)當(dāng)考慮未來的升級(jí)擴(kuò)展不但能夠向下兼容(一般可以在接口參數(shù)中添加接口的版本號(hào)),還能方便添加新的能力。

六、額外補(bǔ)充

1. 關(guān)于MD5應(yīng)用的介紹

在提到對(duì)于開放接口的安全設(shè)計(jì)時(shí),一定少不了對(duì)于摘要算法的應(yīng)用(MD5算法是其實(shí)現(xiàn)方式之一),在接口設(shè)計(jì)方面它可以幫助我們完成數(shù)據(jù)簽名的功能,也就是說用來防止請(qǐng)求或者返回的數(shù)據(jù)被他人篡改。

本節(jié)我們單從安全的角度出發(fā),看看到底哪些場(chǎng)景下的需求可以借助MD5的方式來實(shí)現(xiàn)。

密碼存儲(chǔ)

在一開始的時(shí)候,大多數(shù)服務(wù)端對(duì)于用戶密碼的存儲(chǔ)肯定都是明文的,這就導(dǎo)致了一旦存儲(chǔ)密碼的地方被發(fā)現(xiàn),無(wú)論是黑客還是服務(wù)端維護(hù)人員自己,都可以輕松的得到用戶的賬號(hào)、密碼,并且其實(shí)很多用戶的賬號(hào)、密碼在各種網(wǎng)站上都是一樣的,也就是說一旦因?yàn)橛幸患揖W(wǎng)站數(shù)據(jù)保護(hù)的不好,導(dǎo)致信息被泄露,那可能對(duì)于用戶來說影響的則是他的所有賬號(hào)密碼的地方都被泄露了,想想看這是多少可怕的事情。

所以,那應(yīng)該要如何存儲(chǔ)用戶的密碼呢?最安全的做法當(dāng)然就是不存儲(chǔ),這聽起來很奇怪,不存儲(chǔ)密碼那又如何能夠校驗(yàn)密碼,實(shí)際上不存儲(chǔ)指的是不存儲(chǔ)用戶直接輸入的密碼。

如果用戶直接輸入的密碼不存儲(chǔ),那應(yīng)該存儲(chǔ)什么呢?到這里,MD5就派上用場(chǎng)了,經(jīng)過MD5計(jì)算后的數(shù)據(jù)有這么幾個(gè)特點(diǎn):

其長(zhǎng)度是固定的。

其數(shù)據(jù)是不可逆的。

一份原始數(shù)據(jù)每次MD5后產(chǎn)生的數(shù)據(jù)都是一樣的。

下面我們來實(shí)驗(yàn)一下

publicstaticvoidmain(String[]args){
Stringpwd="123456";
Strings=DigestUtils.md5Hex(pwd);
System.out.println("第一次MD5計(jì)算:"+s);
Strings1=DigestUtils.md5Hex(pwd);
System.out.println("第二次MD5計(jì)算:"+s1);
pwd="123456789";
Strings3=DigestUtils.md5Hex(pwd);
System.out.println("原數(shù)據(jù)長(zhǎng)度變長(zhǎng),經(jīng)過MD5計(jì)算后長(zhǎng)度固定:"+s3);
}
第一次MD5計(jì)算:e10adc3949ba59abbe56e057f20f883e
第二次MD5計(jì)算:e10adc3949ba59abbe56e057f20f883e
原數(shù)據(jù)長(zhǎng)度變長(zhǎng),經(jīng)過MD5計(jì)算后長(zhǎng)度固定:25f9e794323b453885f5181f1b624d0b

有了這樣的特性后,我們就可以用它來存儲(chǔ)用戶的密碼了。

publicstaticMappwdMap=Maps.newConcurrentMap();

publicstaticvoidmain(String[]args){
//一般情況下,用戶在前端輸入密碼后,向后臺(tái)傳輸時(shí),就已經(jīng)是經(jīng)過MD5計(jì)算后的數(shù)據(jù)了,所以后臺(tái)只需要直接保存即可
register("1",DigestUtils.md5Hex("123456"));

//true
System.out.println(verifyPwd("1",DigestUtils.md5Hex("123456")));
//false
System.out.println(verifyPwd("1",DigestUtils.md5Hex("1234567")));
}

//用戶輸入的密碼,在前端已經(jīng)經(jīng)過MD5計(jì)算了,所以到時(shí)候校驗(yàn)時(shí)直接比對(duì)即可
publicstaticbooleanverifyPwd(Stringaccount,Stringpwd){
Stringmd5Pwd=pwdMap.get(account);
returnObjects.equals(md5Pwd,pwd);
}

publicstaticvoidregister(Stringaccount,Stringpwd){
pwdMap.put(account,pwd);
}

MD5后就安全了嗎?

目前為止,雖然我們已經(jīng)對(duì)原始數(shù)據(jù)進(jìn)行了MD5計(jì)算,并且也得到了一串唯一且不可逆的密文,但實(shí)際上還遠(yuǎn)遠(yuǎn)不夠,;不信,我們找一個(gè)破解MD5的網(wǎng)站試一下!

我們把前面經(jīng)過MD5計(jì)算后得到的密文查詢一下試試,結(jié)果居然被查詢出來了!

04b07d46-7bbc-11ee-939d-92fbcf53809c.jpg

之所以會(huì)這樣,其實(shí)恰好就是利用了MD5的特性之一:一份原始數(shù)據(jù)每次MD5后產(chǎn)生的數(shù)據(jù)都是一樣的。

試想一想,雖然我們不能通過密文反解出明文來,但是我們可以直接用明文去和猜,假設(shè)有人已經(jīng)把所有可能出現(xiàn)的明文組合,都經(jīng)過MD5計(jì)算后,并且保存了起來,那當(dāng)拿到密文后,只需要去記錄庫(kù)里匹配一下密文就能得到明文了,正如上圖這個(gè)網(wǎng)站的做法一樣。

對(duì)于保存這樣數(shù)據(jù)的表,還有個(gè)專門的名詞:彩虹表,也就是說只要時(shí)間足夠、空間足夠,也一定能夠破解出來。

加鹽

正因?yàn)樯鲜銮闆r的存在,所以出現(xiàn)了加鹽的玩法,說白了就是在原始數(shù)據(jù)中,再摻雜一些別的數(shù)據(jù),這樣就不會(huì)那么容易破解了。

Stringpwd="123456";
Stringsalt="wylsalt";
Strings=DigestUtils.md5Hex(salt+pwd);
System.out.println("第一次MD5計(jì)算:"+s);
第一次MD5計(jì)算:b9ff58406209d6c4f97e1a0d424a59ba

你看,簡(jiǎn)單加一點(diǎn)內(nèi)容,破解網(wǎng)站就查詢不到了吧!

攻防都是在不斷的博弈中進(jìn)行升級(jí),很遺憾,如果僅僅做成這樣,實(shí)際上還是不夠安全,比如攻擊者自己注冊(cè)一個(gè)賬號(hào),密碼就設(shè)置成1。

04cae686-7bbc-11ee-939d-92fbcf53809c.jpg

Stringpwd="1";
Stringsalt="wylsalt";
Strings=DigestUtils.md5Hex(salt+pwd);
System.out.println("第一次MD5計(jì)算:"+s);
第一次MD5計(jì)算:4e7b25db2a0e933b27257f65b117582a

雖然要付費(fèi),但是明顯已經(jīng)是匹配到結(jié)果了。

04e671da-7bbc-11ee-939d-92fbcf53809c.jpg

所以說,無(wú)論是密碼還是鹽值,其實(shí)都要求其本身要保證有足夠的長(zhǎng)度和復(fù)雜度,這樣才能防止像彩虹表這樣被存儲(chǔ)下來,如果再能定期更換一個(gè),那就更安全了,雖說無(wú)論再?gòu)?fù)雜,理論上都可以被窮舉到,但越長(zhǎng)的數(shù)據(jù),想要被窮舉出來的時(shí)間則也就越長(zhǎng),所以相對(duì)來說也就是安全的。

數(shù)字簽名

摘要算法另一個(gè)常見的應(yīng)用場(chǎng)景就是數(shù)字簽名了,前面章節(jié)也有介紹過了

大致流程,百度百科也有介紹

04f0e1d8-7bbc-11ee-939d-92fbcf53809c.jpg

2. 對(duì)稱加密算法

對(duì)稱加密算法是指通過密鑰對(duì)原始數(shù)據(jù)(明文),進(jìn)行特殊的處理后,使其變成密文發(fā)送出去,數(shù)據(jù)接收方收到數(shù)據(jù)后,再使用同樣的密鑰進(jìn)行特殊處理后,再使其還原為原始數(shù)據(jù)(明文),對(duì)稱加密算法中密鑰只有一個(gè),數(shù)據(jù)加密與解密方都必須事先約定好。

對(duì)稱加密算法特點(diǎn)

只有一個(gè)密鑰,加密和解密都使用它。

加密、解密速度快、效率高。

由于數(shù)據(jù)加密方和數(shù)據(jù)解密方使用的是同一個(gè)密鑰,因此密鑰更容易被泄露。

常用的加密算法介紹

DES

其入口參數(shù)有三個(gè):key、data、mode。key為加密解密使用的密鑰,data為加密解密的數(shù)據(jù),mode為其工作模式。當(dāng)模式為加密模式時(shí),明文按照64位進(jìn)行分組,形成明文組,key用于對(duì)數(shù)據(jù)加密,當(dāng)模式為解密模式時(shí),key用于對(duì)數(shù)據(jù)解密。實(shí)際運(yùn)用中,密鑰只用到了64位中的56位,這樣才具有高的安全性。

050f66bc-7bbc-11ee-939d-92fbcf53809c.jpg

算法特點(diǎn)

DES算法具有極高安全性,除了用窮舉搜索法對(duì)DES算法進(jìn)行攻擊外,還沒有發(fā)現(xiàn)更有效的辦法。而56位長(zhǎng)的密鑰的窮舉空間為2^56,這意味著如果一臺(tái)計(jì)算機(jī)的速度是每一秒鐘檢測(cè)一百萬(wàn)個(gè)密鑰,則它搜索完全部密鑰就需要將近2285年的時(shí)間,可見,這是難以實(shí)現(xiàn)的。然而,這并不等于說DES是不可破解的。而實(shí)際上,隨著硬件技術(shù)和Internet的發(fā)展,其破解的可能性越來越大,而且,所需要的時(shí)間越來越少。使用經(jīng)過特殊設(shè)計(jì)的硬件并行處理要幾個(gè)小時(shí)。

為了克服DES密鑰空間小的缺陷,人們又提出了3DES的變形方式。

3DES

3DES相當(dāng)于對(duì)每個(gè)數(shù)據(jù)塊進(jìn)行三次DES加密算法,雖然解決了DES不夠安全的問題,但效率上也相對(duì)慢了許多。

AES

AES用來替代原先的DES算法,是當(dāng)前對(duì)稱加密中最流行的算法之一。

ECB模式

AES加密算法中一個(gè)重要的機(jī)制就是分組加密,而ECB模式就是最簡(jiǎn)單的一種分組加密模式,比如按照每128位數(shù)據(jù)塊大小將數(shù)據(jù)分成若干塊,之后再對(duì)每一塊數(shù)據(jù)使用相同的密鑰進(jìn)行加密,最終生成若干塊加密后的數(shù)據(jù),這種算法由于每個(gè)數(shù)據(jù)塊可以進(jìn)行獨(dú)立的加密、解密,因此可以進(jìn)行并行計(jì)算,效率很高,但也因如此,則會(huì)很容易被猜測(cè)到密文的規(guī)律。

052f8654-7bbc-11ee-939d-92fbcf53809c.jpg

privatestaticfinalStringAES_ALG="AES";
privatestaticfinalStringAES_ECB_PCK_ALG="AES/ECB/NoPadding";

publicstaticvoidmain(String[]args)throwsException{
System.out.println("第一次加密:"+encryptWithECB("1234567812345678","50AHsYx7H3OHVMdF123456","UTF-8"));
System.out.println("第二次加密:"+encryptWithECB("12345678123456781234567812345678","50AHsYx7H3OHVMdF123456","UTF-8"));
}

publicstaticStringencryptWithECB(Stringcontent,StringaesKey,Stringcharset)throwsException{
Ciphercipher=Cipher.getInstance(AES_ECB_PCK_ALG);
cipher.init(Cipher.ENCRYPT_MODE,
newSecretKeySpec(Base64.decodeBase64(aesKey.getBytes()),AES_ALG));
byte[]encryptBytes=cipher.doFinal(content.getBytes(charset));
returnHex.encodeHexString(encryptBytes);
}
第一次加密:87d2d15dbcb5747ed16cfe4c029e137c
第二次加密:87d2d15dbcb5747ed16cfe4c029e137c87d2d15dbcb5747ed16cfe4c029e137c

可以看出,加密后的密文明顯也是重復(fù)的,因此針對(duì)這一特性可進(jìn)行分組重放攻擊。

CBC模式

CBC模式引入了初始化向量的概念(IV),第一組分組會(huì)使用向量值與第一塊明文進(jìn)行異或運(yùn)算,之后得到的結(jié)果既是密文塊,也是與第二塊明文進(jìn)行異或的對(duì)象,以此類推,最終解決了ECB模式的安全問題。

0550a686-7bbc-11ee-939d-92fbcf53809c.jpg

CBC模式的特點(diǎn)

CBC模式安全性比ECB模式要高,但由于每一塊數(shù)據(jù)之間有依賴性,所以無(wú)法進(jìn)行并行計(jì)算,效率沒有ECB模式高。






審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 存儲(chǔ)器
    +關(guān)注

    關(guān)注

    38

    文章

    7484

    瀏覽量

    163761
  • DES
    DES
    +關(guān)注

    關(guān)注

    0

    文章

    64

    瀏覽量

    48215
  • 加密算法
    +關(guān)注

    關(guān)注

    0

    文章

    215

    瀏覽量

    25541
  • RSA
    RSA
    +關(guān)注

    關(guān)注

    0

    文章

    59

    瀏覽量

    18885

原文標(biāo)題:如何設(shè)計(jì)一個(gè)安全好用的OpenApi

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    怎樣定義個(gè)結(jié)構(gòu)體數(shù)組

    數(shù)據(jù)結(jié)構(gòu)的特點(diǎn)有哪些?怎樣定義個(gè)結(jié)構(gòu)體數(shù)組?
    發(fā)表于 10-14 07:25

    怎樣制作個(gè)電流電壓轉(zhuǎn)換器

    怎樣制作個(gè)電流電壓轉(zhuǎn)換器?怎樣制作
    發(fā)表于 10-15 06:47

    怎樣使用keil軟件創(chuàng)建個(gè)STM32工程

    怎樣使用keil軟件創(chuàng)建個(gè)STM32工程?有哪些操作步驟?
    發(fā)表于 10-21 07:22

    怎樣使用STM32CubeMX點(diǎn)亮個(gè)LED

    如何使用STM32CubeMX軟件?怎樣使用STM32CubeMX點(diǎn)亮個(gè)LED
    發(fā)表于 10-25 08:31

    怎樣新建個(gè)STM32標(biāo)準(zhǔn)外設(shè)庫(kù)

    STM32標(biāo)準(zhǔn)外設(shè)庫(kù)是什么?怎樣新建個(gè)STM32標(biāo)準(zhǔn)外設(shè)庫(kù)?為什么需要選擇啟動(dòng)文件?
    發(fā)表于 10-29 07:53

    怎樣使用Android studio新建個(gè)HelloWorld項(xiàng)目

    怎樣使用Android studio新建個(gè)HelloWorld項(xiàng)目?有哪些操作步驟?
    發(fā)表于 11-08 08:10

    怎樣設(shè)計(jì)個(gè)嵌入式產(chǎn)品

    怎樣設(shè)計(jì)個(gè)嵌入式產(chǎn)品?設(shè)計(jì)個(gè)嵌入式產(chǎn)品要注意
    發(fā)表于 11-12 06:53

    怎樣設(shè)計(jì)個(gè)基于FLASH的APP程序

    STM32正常的程序運(yùn)行流程是怎樣的?怎樣設(shè)計(jì)個(gè)基于FLASH的APP程序?
    發(fā)表于 11-26 06:08

    怎樣解決STM32串口第一個(gè)數(shù)據(jù)丟失的問題

    怎樣解決STM32串口第一個(gè)數(shù)據(jù)丟失的問題?故障出在哪里?怎樣
    發(fā)表于 12-09 07:04

    怎樣編寫個(gè)簡(jiǎn)單的stm32程序

    MDK該怎樣安裝?怎樣編寫個(gè)簡(jiǎn)單的stm32
    發(fā)表于 12-15 06:05

    怎樣編寫個(gè)USART的接口程序

    怎樣編寫個(gè)USART的接口程序?
    發(fā)表于 01-24 07:44

    怎樣使用Qt編寫個(gè)簡(jiǎn)單的上位機(jī)

    怎樣使用Qt編寫個(gè)簡(jiǎn)單的上位機(jī)?有哪些步驟?
    發(fā)表于 03-02 06:07

    怎樣創(chuàng)建個(gè)新的安卓程序應(yīng)用

    怎樣創(chuàng)建個(gè)新的安卓程序應(yīng)用?有哪些創(chuàng)建步驟?
    發(fā)表于 03-04 12:41

    怎樣編寫個(gè)gpio觸發(fā)的中斷驅(qū)動(dòng)代碼程序

    linux中斷分為哪幾部分?怎樣實(shí)現(xiàn)怎樣編寫
    發(fā)表于 03-09 06:40

    請(qǐng)教大神怎樣個(gè)變量?

    請(qǐng)教大神怎樣個(gè)變量?
    發(fā)表于 12-07 08:17
    RM新时代网站-首页