一、AppId和AppSecret
AppId的使用
AppId作為一種全局唯一的標識符,其作用主要在于方便用戶身份識別以及數(shù)據分析等方面。為了防止其他用戶通過惡意使用別人的AppId來發(fā)起請求,一般都會采用配對AppSecret的方式,類似于一種密碼。AppId和AppSecret通常會組合生成一套簽名,并按照一定規(guī)則進行加密處理。在請求方發(fā)起請求時,需要將這個簽名值一并提交給提供方進行驗證。如果簽名驗證通過,則可以進行數(shù)據交互,否則將被拒絕。這種機制能夠保證數(shù)據的安全性和準確性,提高系統(tǒng)的可靠性和可用性。
AppId的生成
正如前面所說,AppId就是有一個身份標識,生成時只要保證全局唯一即可。
AppSecret生成
AppSecret就是密碼,按照一般的的密碼安全性要求生成即可。
二、sign簽名
RSASignature
首先,在介紹簽名方式之前,我們必須先了解2個概念,分別是:非對稱加密算法(比如:RSA)、摘要算法(比如:MD5)。
簡單來說,非對稱加密的應用場景一般有兩種,一種是公鑰加密,私鑰解密,可以應用在加解密場景中(不過由于非對稱加密的效率實在不高,用的比較少);還有一種就是結合摘要算法,把信息經過摘要后,再用私鑰加密,公鑰用來解密,可以應用在簽名場景中,也是我們將要使用到的方式。
大致看看RSASignature簽名的方式,稍后用到SHA256withRSA底層就是使用的這個方法。
摘要算法與非對稱算法的最大區(qū)別就在于,它是一種不需要密鑰的且不可逆的算法,也就是一旦明文數(shù)據經過摘要算法計算后,得到的密文數(shù)據一定是不可反推回來的。
簽名的作用
好了,現(xiàn)在我們再來看看簽名,簽名主要可以用在兩個場景,一種是數(shù)據防篡改,一種是身份防冒充,實際上剛好可以對應上前面我們介紹的兩種算法。
數(shù)據防篡改
顧名思義,就是防止數(shù)據在網絡傳輸過程中被修改,摘要算法可以保證每次經過摘要算法的原始數(shù)據,計算出來的結果都一樣,所以一般接口提供方只要用同樣的原數(shù)據經過同樣的摘要算法,然后與接口請求方生成的數(shù)據進行比較,如果一致則表示數(shù)據沒有被篡改過。
身份防冒充
這里身份防冒充,我們就要使用另一種方式,比如SHA256withRSA,其實現(xiàn)原理就是先用數(shù)據進行SHA256計算,然后再使用RSA私鑰加密,對方解的時候也一樣,先用RSA公鑰解密,然后再進行SHA256計算,最后看結果是否匹配。
三、使用示例
前置準備
在沒有自動化開放平臺時,appId、appSecret可直接通過線下的方式給到接入方,appSecret需要接入方自行保存好,避免泄露。也可以自行
公私鑰可以由接口提供方來生成,同樣通過線下的方式,把私鑰交給對方,并要求對方需保密。
交互流程
客戶端準備
接口請求方,首先把業(yè)務參數(shù),進行摘要算法計算,生成一個簽名(sign)
//業(yè)務請求參數(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); Set keySet=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("【請求方】拼接后的參數(shù):"+sb.toString()); System.out.println(); 【請求方】拼接后的參數(shù):appId=123456&nonce=1234&sign=c630885277f9d31cf449697238bfc6b044a78545894c83aad2ff6d0b7d486bc5×tamp=1653057661381&appSecret=654321 【請求方】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("【請求方】接口請求參數(shù):"+requestParam); 【請求方】接口請求參數(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"}}
服務端準備
從請求參數(shù)中,先獲取body的內容,然后簽名,完成對參數(shù)校驗
Headerheader=apiRequestEntity.getHeader(); UserEntityuserEntity=JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()),UserEntity.class); //首先,拿到參數(shù)后同樣進行簽名 Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity)); if(!sign.equals(header.getSign())){ thrownewException("數(shù)據簽名錯誤!"); }
從header中獲取相關信息,并使用公鑰進行驗簽,完成身份認證
//從header中獲取相關信息,其中appSecret需要自己根據傳過來的appId來獲取 StringappId=header.getAppId(); StringappSecret=getAppSecret(appId); Stringnonce=header.getNonce(); Stringtimestamp=header.getTimestamp(); //按照同樣的方式生成appSign,然后使用公鑰進行驗簽 Mapdata=Maps.newHashMap(); data.put("appId",appId); data.put("nonce",nonce); data.put("sign",sign); data.put("timestamp",timestamp); Set keySet=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("公鑰驗簽錯誤!"); } System.out.println(); System.out.println("【提供方】驗證通過!");
完整代碼示例
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(); /** *分別保存生成的公私鑰對 *key:appId,value:公私鑰對 */ staticMap >appKeyPair=Maps.newConcurrentMap(); publicstaticvoidmain(String[]args)throwsException{ //模擬生成appId、appSecret StringappId=initAppInfo(); //根據appId生成公私鑰對 initKeyPair(appId); //模擬請求方 StringrequestParam=clientCall(); //模擬提供方驗證 serverVerify(requestParam); } privatestaticStringinitAppInfo(){ //appId、appSecret生成規(guī)則,依據之前介紹過的方式,保證全局唯一即可 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ù)后同樣進行簽名 Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity)); if(!sign.equals(header.getSign())){ thrownewException("數(shù)據簽名錯誤!"); } //從header中獲取相關信息,其中appSecret需要自己根據傳過來的appId來獲取 StringappId=header.getAppId(); StringappSecret=getAppSecret(appId); Stringnonce=header.getNonce(); Stringtimestamp=header.getTimestamp(); //按照同樣的方式生成appSign,然后使用公鑰進行驗簽 Map data=Maps.newHashMap(); data.put("appId",appId); data.put("nonce",nonce); data.put("sign",sign); data.put("timestamp",timestamp); Set keySet=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("公鑰驗簽錯誤!"); } System.out.println(); System.out.println("【提供方】驗證通過!"); } publicstaticStringclientCall(){ //假設接口請求方與接口提供方,已經通過其他渠道,確認了雙方交互的appId、appSecret StringappId="123456"; StringappSecret="654321"; Stringtimestamp=String.valueOf(System.currentTimeMillis()); //應該為隨機數(shù),演示隨便寫一個 Stringnonce="1234"; //業(yè)務請求參數(shù) UserEntityuserEntity=newUserEntity(); userEntity.setUserId("1"); userEntity.setPhone("13912345678"); //使用sha256的方式生成簽名 Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity)); Map data=Maps.newHashMap(); data.put("appId",appId); data.put("nonce",nonce); data.put("sign",sign); data.put("timestamp",timestamp); Set keySet=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("【請求方】拼接后的參數(shù):"+sb.toString()); System.out.println(); //使用sha256withRSA的方式對header中的內容加簽 StringappSign=sha256withRSASignature(appKeyPair.get(appId).get("privateKey"),sb.toString()); System.out.println("【請求方】appSign:"+appSign); System.out.println(); //請求參數(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("【請求方】接口請求參數(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("簽名計算出現(xiàn)異常",e); } } /** *公鑰驗簽 * *@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)); } /** *生成公私鑰對 * *@throwsException */ publicstaticvoidinitKeyPair(StringappId)throwsException{ KeyPairGeneratorkeyPairGenerator=KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); KeyPairkeyPair=keyPairGenerator.generateKeyPair(); RSAPublicKeypublicKey=(RSAPublicKey)keyPair.getPublic(); RSAPrivateKeyprivateKey=(RSAPrivateKey)keyPair.getPrivate(); Map keyMap=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); } }
四、常見防護手段
timestamp
前面在接口設計中,我們使用到了timestamp,這個參數(shù)主要可以用來防止同一個請求參數(shù)被無限期的使用。
稍微修改一下原服務端校驗邏輯,增加了5分鐘有效期的校驗邏輯。
privatestaticvoidserverVerify(StringrequestParam)throwsException{ APIRequestEntityapiRequestEntity=JSONObject.parseObject(requestParam,APIRequestEntity.class); Headerheader=apiRequestEntity.getHeader(); UserEntityuserEntity=JSONObject.parseObject(JSONObject.toJSONString(apiRequestEntity.getBody()),UserEntity.class); //首先,拿到參數(shù)后同樣進行簽名 Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity)); if(!sign.equals(header.getSign())){ thrownewException("數(shù)據簽名錯誤!"); } //從header中獲取相關信息,其中appSecret需要自己根據傳過來的appId來獲取 StringappId=header.getAppId(); StringappSecret=getAppSecret(appId); Stringnonce=header.getNonce(); Stringtimestamp=header.getTimestamp(); //請求時間有效期校驗 longnow=LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); if((now-Long.parseLong(timestamp))/1000/60>=5){ thrownewException("請求過期!"); } cache.put(appId+"_"+nonce,"1"); //按照同樣的方式生成appSign,然后使用公鑰進行驗簽 Mapdata=Maps.newHashMap(); data.put("appId",appId); data.put("nonce",nonce); data.put("sign",sign); data.put("timestamp",timestamp); Set keySet=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("驗簽錯誤!"); } System.out.println(); System.out.println("【提供方】驗證通過!"); }
nonce
nonce值是一個由接口請求方生成的隨機數(shù),在有需要的場景中,可以用它來實現(xiàn)請求一次性有效,也就是說同樣的請求參數(shù)只能使用一次,這樣可以避免接口重放攻擊。
具體實現(xiàn)方式:接口請求方每次請求都會隨機生成一個不重復的nonce值,接口提供方可以使用一個存儲容器(為了方便演示,我使用的是guava提供的本地緩存,生產環(huán)境中可以使用redis這樣的分布式存儲方式),每次先在容器中看看是否存在接口請求方發(fā)來的nonce值,如果不存在則表明是第一次請求,則放行,并且把當前nonce值保存到容器中,這樣,如果下次再使用同樣的nonce來請求則容器中一定存在,那么就可以判定是無效請求了。
這里可以設置緩存的失效時間為5分鐘,因為前面有效期已經做了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ù)后同樣進行簽名 Stringsign=getSHA256Str(JSONObject.toJSONString(userEntity)); if(!sign.equals(header.getSign())){ thrownewException("數(shù)據簽名錯誤!"); } //從header中獲取相關信息,其中appSecret需要自己根據傳過來的appId來獲取 StringappId=header.getAppId(); StringappSecret=getAppSecret(appId); Stringnonce=header.getNonce(); Stringtimestamp=header.getTimestamp(); //請求時間有效期校驗 longnow=LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); if((now-Long.parseLong(timestamp))/1000/60>=5){ thrownewException("請求過期!"); } //nonce有效性判斷 Stringstr=cache.getIfPresent(appId+"_"+nonce); if(Objects.nonNull(str)){ thrownewException("請求失效!"); } cache.put(appId+"_"+nonce,"1"); //按照同樣的方式生成appSign,然后使用公鑰進行驗簽 Map data=Maps.newHashMap(); data.put("appId",appId); data.put("nonce",nonce); data.put("sign",sign); data.put("timestamp",timestamp); Set keySet=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("驗簽錯誤!"); } System.out.println(); System.out.println("【提供方】驗證通過!"); }
訪問權限
數(shù)據訪問權限,一般可根據appId的身份來獲取開放給其的相應權限,要確保每個appId只能訪問其權限范圍內的數(shù)據。
參數(shù)合法性校驗
參數(shù)的合法性校驗應該是每個接口必備的,無論是前端發(fā)起的請求,還是后端的其他調用都必須對參數(shù)做校驗,比如:參數(shù)的長度、類型、格式,必傳參數(shù)是否有傳,是否符合約定的業(yè)務規(guī)則等等。
推薦使用SpringBoot Validation來快速實現(xiàn)一些基本的參數(shù)校驗。
參考如下示例:
@Data @ToString publicclassDemoEntity{ //不能為空,比較時會除去空格 @NotBlank(message="名稱不能為空") privateStringname; //amount必須是一個大于等于5,小于等于10的數(shù)字 @DecimalMax(value="10") @DecimalMin(value="5") privateBigDecimalamount; //必須符合email格式 @Email privateStringemail; //size長度必須在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; //限制必須是一個未來或現(xiàn)在的時間 @FutureOrPresent privateDatefutureOrPast; //支持正則表達式 @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"; } } }
限流保護
在設計接口時,我們應當對接口的負載能力做出評估,尤其是開放給外部使用時,這樣當實際請求流量超過預期流量時,我們便可采取相應的預防策略,以免服務器崩潰。
一般來說限流主要是為了防止惡意刷站請求,爬蟲等非正常的業(yè)務訪問,因此一般來說采取的方式都是直接丟棄超出閾值的部分。
限流的具體實現(xiàn)有多種,單機版可以使用Guava的RateLimiter,分布式可以使用Redis,想要更加完善的成套解決方案則可以使用阿里開源的Sentinel。
敏感數(shù)據訪問
敏感信息一般包含,身份證、手機號、銀行卡號、車牌號、姓名等等,應該按照脫敏規(guī)則進行處理。
白名單機制
使用白名單機制可以進一步加強接口的安全性,一旦服務與服務交互可以使用,接口提供方可以限制只有白名單內的IP才能訪問,這樣接口請求方只要把其出口IP提供出來即可。
黑名單機制
與之對應的黑名單機制,則是應用在服務端與客戶端的交互,由于客戶端IP都是不固定的,所以無法使用白名單機制,不過我們依然可以使用黑名單攔截一些已經被識別為非法請求的IP。
五、其他考慮
名稱和描述:API 的名稱和描述應該簡潔明了,并清晰地表明其功能和用途。
請求和響應:API 應該支持標準的 HTTP 請求方法,如 GET、POST、PUT 和 DELETE,并定義這些方法的參數(shù)和響應格式。
錯誤處理:API 應該定義各種錯誤碼,并提供有關錯誤的詳細信息。
文檔和示例:API 應該提供文檔和示例,以幫助開發(fā)人員了解如何使用該 API,并提供示例數(shù)據以進行測試。
可擴展:API應當考慮未來的升級擴展不但能夠向下兼容(一般可以在接口參數(shù)中添加接口的版本號),還能方便添加新的能力。
六、額外補充
1. 關于MD5應用的介紹
在提到對于開放接口的安全設計時,一定少不了對于摘要算法的應用(MD5算法是其實現(xiàn)方式之一),在接口設計方面它可以幫助我們完成數(shù)據簽名的功能,也就是說用來防止請求或者返回的數(shù)據被他人篡改。
本節(jié)我們單從安全的角度出發(fā),看看到底哪些場景下的需求可以借助MD5的方式來實現(xiàn)。
密碼存儲
在一開始的時候,大多數(shù)服務端對于用戶密碼的存儲肯定都是明文的,這就導致了一旦存儲密碼的地方被發(fā)現(xiàn),無論是黑客還是服務端維護人員自己,都可以輕松的得到用戶的賬號、密碼,并且其實很多用戶的賬號、密碼在各種網站上都是一樣的,也就是說一旦因為有一家網站數(shù)據保護的不好,導致信息被泄露,那可能對于用戶來說影響的則是他的所有賬號密碼的地方都被泄露了,想想看這是多少可怕的事情。
所以,那應該要如何存儲用戶的密碼呢?最安全的做法當然就是不存儲,這聽起來很奇怪,不存儲密碼那又如何能夠校驗密碼,實際上不存儲指的是不存儲用戶直接輸入的密碼。
如果用戶直接輸入的密碼不存儲,那應該存儲什么呢?到這里,MD5就派上用場了,經過MD5計算后的數(shù)據有這么幾個特點:
其長度是固定的。
其數(shù)據是不可逆的。
一份原始數(shù)據每次MD5后產生的數(shù)據都是一樣的。
下面我們來實驗一下
publicstaticvoidmain(String[]args){ Stringpwd="123456"; Strings=DigestUtils.md5Hex(pwd); System.out.println("第一次MD5計算:"+s); Strings1=DigestUtils.md5Hex(pwd); System.out.println("第二次MD5計算:"+s1); pwd="123456789"; Strings3=DigestUtils.md5Hex(pwd); System.out.println("原數(shù)據長度變長,經過MD5計算后長度固定:"+s3); } 第一次MD5計算:e10adc3949ba59abbe56e057f20f883e 第二次MD5計算:e10adc3949ba59abbe56e057f20f883e 原數(shù)據長度變長,經過MD5計算后長度固定:25f9e794323b453885f5181f1b624d0b
有了這樣的特性后,我們就可以用它來存儲用戶的密碼了。
publicstaticMappwdMap=Maps.newConcurrentMap(); publicstaticvoidmain(String[]args){ //一般情況下,用戶在前端輸入密碼后,向后臺傳輸時,就已經是經過MD5計算后的數(shù)據了,所以后臺只需要直接保存即可 register("1",DigestUtils.md5Hex("123456")); //true System.out.println(verifyPwd("1",DigestUtils.md5Hex("123456"))); //false System.out.println(verifyPwd("1",DigestUtils.md5Hex("1234567"))); } //用戶輸入的密碼,在前端已經經過MD5計算了,所以到時候校驗時直接比對即可 publicstaticbooleanverifyPwd(Stringaccount,Stringpwd){ Stringmd5Pwd=pwdMap.get(account); returnObjects.equals(md5Pwd,pwd); } publicstaticvoidregister(Stringaccount,Stringpwd){ pwdMap.put(account,pwd); }
MD5后就安全了嗎?
目前為止,雖然我們已經對原始數(shù)據進行了MD5計算,并且也得到了一串唯一且不可逆的密文,但實際上還遠遠不夠,;不信,我們找一個破解MD5的網站試一下!
我們把前面經過MD5計算后得到的密文查詢一下試試,結果居然被查詢出來了!
之所以會這樣,其實恰好就是利用了MD5的特性之一:一份原始數(shù)據每次MD5后產生的數(shù)據都是一樣的。
試想一想,雖然我們不能通過密文反解出明文來,但是我們可以直接用明文去和猜,假設有人已經把所有可能出現(xiàn)的明文組合,都經過MD5計算后,并且保存了起來,那當拿到密文后,只需要去記錄庫里匹配一下密文就能得到明文了,正如上圖這個網站的做法一樣。
對于保存這樣數(shù)據的表,還有個專門的名詞:彩虹表,也就是說只要時間足夠、空間足夠,也一定能夠破解出來。
加鹽
正因為上述情況的存在,所以出現(xiàn)了加鹽的玩法,說白了就是在原始數(shù)據中,再摻雜一些別的數(shù)據,這樣就不會那么容易破解了。
Stringpwd="123456"; Stringsalt="wylsalt"; Strings=DigestUtils.md5Hex(salt+pwd); System.out.println("第一次MD5計算:"+s); 第一次MD5計算:b9ff58406209d6c4f97e1a0d424a59ba
你看,簡單加一點內容,破解網站就查詢不到了吧!
攻防都是在不斷的博弈中進行升級,很遺憾,如果僅僅做成這樣,實際上還是不夠安全,比如攻擊者自己注冊一個賬號,密碼就設置成1。
Stringpwd="1"; Stringsalt="wylsalt"; Strings=DigestUtils.md5Hex(salt+pwd); System.out.println("第一次MD5計算:"+s); 第一次MD5計算:4e7b25db2a0e933b27257f65b117582a
雖然要付費,但是明顯已經是匹配到結果了。
所以說,無論是密碼還是鹽值,其實都要求其本身要保證有足夠的長度和復雜度,這樣才能防止像彩虹表這樣被存儲下來,如果再能定期更換一個,那就更安全了,雖說無論再復雜,理論上都可以被窮舉到,但越長的數(shù)據,想要被窮舉出來的時間則也就越長,所以相對來說也就是安全的。
數(shù)字簽名
摘要算法另一個常見的應用場景就是數(shù)字簽名了,前面章節(jié)也有介紹過了
大致流程,百度百科也有介紹
2. 對稱加密算法
對稱加密算法是指通過密鑰對原始數(shù)據(明文),進行特殊的處理后,使其變成密文發(fā)送出去,數(shù)據接收方收到數(shù)據后,再使用同樣的密鑰進行特殊處理后,再使其還原為原始數(shù)據(明文),對稱加密算法中密鑰只有一個,數(shù)據加密與解密方都必須事先約定好。
對稱加密算法特點
只有一個密鑰,加密和解密都使用它。
加密、解密速度快、效率高。
由于數(shù)據加密方和數(shù)據解密方使用的是同一個密鑰,因此密鑰更容易被泄露。
常用的加密算法介紹
DES
其入口參數(shù)有三個:key、data、mode。key為加密解密使用的密鑰,data為加密解密的數(shù)據,mode為其工作模式。當模式為加密模式時,明文按照64位進行分組,形成明文組,key用于對數(shù)據加密,當模式為解密模式時,key用于對數(shù)據解密。實際運用中,密鑰只用到了64位中的56位,這樣才具有高的安全性。
算法特點
DES算法具有極高安全性,除了用窮舉搜索法對DES算法進行攻擊外,還沒有發(fā)現(xiàn)更有效的辦法。而56位長的密鑰的窮舉空間為2^56,這意味著如果一臺計算機的速度是每一秒鐘檢測一百萬個密鑰,則它搜索完全部密鑰就需要將近2285年的時間,可見,這是難以實現(xiàn)的。然而,這并不等于說DES是不可破解的。而實際上,隨著硬件技術和Internet的發(fā)展,其破解的可能性越來越大,而且,所需要的時間越來越少。使用經過特殊設計的硬件并行處理要幾個小時。
為了克服DES密鑰空間小的缺陷,人們又提出了3DES的變形方式。
3DES
3DES相當于對每個數(shù)據塊進行三次DES加密算法,雖然解決了DES不夠安全的問題,但效率上也相對慢了許多。
AES
AES用來替代原先的DES算法,是當前對稱加密中最流行的算法之一。
ECB模式
AES加密算法中一個重要的機制就是分組加密,而ECB模式就是最簡單的一種分組加密模式,比如按照每128位數(shù)據塊大小將數(shù)據分成若干塊,之后再對每一塊數(shù)據使用相同的密鑰進行加密,最終生成若干塊加密后的數(shù)據,這種算法由于每個數(shù)據塊可以進行獨立的加密、解密,因此可以進行并行計算,效率很高,但也因如此,則會很容易被猜測到密文的規(guī)律。
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
可以看出,加密后的密文明顯也是重復的,因此針對這一特性可進行分組重放攻擊。
CBC模式
CBC模式引入了初始化向量的概念(IV),第一組分組會使用向量值與第一塊明文進行異或運算,之后得到的結果既是密文塊,也是與第二塊明文進行異或的對象,以此類推,最終解決了ECB模式的安全問題。
CBC模式的特點
CBC模式安全性比ECB模式要高,但由于每一塊數(shù)據之間有依賴性,所以無法進行并行計算,效率沒有ECB模式高。
審核編輯:劉清
-
存儲器
+關注
關注
38文章
7452瀏覽量
163598 -
DES
+關注
關注
0文章
64瀏覽量
48199 -
加密算法
+關注
關注
0文章
211瀏覽量
25530 -
RSA
+關注
關注
0文章
59瀏覽量
18863
原文標題:如何設計一個安全好用的OpenApi
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論