萬(wàn)字長(zhǎng)文,談一談三方接口調(diào)用方案設(shè)計(jì)!
在為第三方系統(tǒng)提供接口的時(shí)候,肯定要考慮接口數(shù)據(jù)的安全問(wèn)題,比如數(shù)據(jù)是否被篡改,數(shù)據(jù)是否已經(jīng)過(guò)時(shí),數(shù)據(jù)是否可以重復(fù)提交等問(wèn)題。
在設(shè)計(jì)三方接口調(diào)用的方案時(shí),需要考慮到安全性和可用性。以下是一種設(shè)計(jì)方案的概述,其中包括使用API密鑰(Access Key/Secret Key)進(jìn)行身份驗(yàn)證和設(shè)置回調(diào)地址。
設(shè)計(jì)方案概述
1.API密鑰生成: 為每個(gè)三方應(yīng)用生成唯一的API密鑰對(duì)(AK/SK),其中AK用于標(biāo)識(shí)應(yīng)用,SK用于進(jìn)行簽名和加密。
AK:Access Key Id,用于標(biāo)示用戶。
SK:Secret Access Key,是用戶用于加密認(rèn)證字符串和用來(lái)驗(yàn)證認(rèn)證字符串的密鑰,其中SK必須保密。
通過(guò)使用Access Key Id / Secret Access Key加密的方法來(lái)驗(yàn)證某個(gè)請(qǐng)求的發(fā)送者身份。
2.接口鑒權(quán): 在進(jìn)行接口調(diào)用時(shí),客戶端需要使用AK和請(qǐng)求參數(shù)生成簽名,并將其放入請(qǐng)求頭或參數(shù)中以進(jìn)行身份驗(yàn)證。
3.回調(diào)地址設(shè)置: 三方應(yīng)用提供回調(diào)地址,用于接收異步通知和回調(diào)結(jié)果。
4.接口API設(shè)計(jì): 設(shè)計(jì)接口的URL、HTTP方法、請(qǐng)求參數(shù)、響應(yīng)格式等細(xì)節(jié)。
權(quán)限劃分
appID:應(yīng)用的唯一標(biāo)識(shí)
用來(lái)標(biāo)識(shí)你的開(kāi)發(fā)者賬號(hào)的,即:用戶id,可以在數(shù)據(jù)庫(kù)添加索引,方便快速查找,同一個(gè) appId 可以對(duì)應(yīng)多個(gè) appKey+appSecret,達(dá)到權(quán)限的
appKey:公匙(相當(dāng)于賬號(hào))
公開(kāi)的,調(diào)用服務(wù)所需要的密鑰。是用戶的身份認(rèn)證標(biāo)識(shí),用于調(diào)用平臺(tái)可用服務(wù).,可以簡(jiǎn)單理解成是賬號(hào)。
appSecret:私匙(相當(dāng)于密碼)
簽名的密鑰,是跟appKey配套使用的,可以簡(jiǎn)單理解成是密碼。
token:令牌(過(guò)期失效)
使用方法
- 向第三方服務(wù)器請(qǐng)求授權(quán)時(shí),帶上AppKey和AppSecret(需存在服務(wù)器端)
- 第三方服務(wù)器驗(yàn)證appKey和appSecret在數(shù)據(jù)庫(kù)、緩存中有沒(méi)有記錄
- 如果有,生成一串唯一的字符串(token令牌),返回給服務(wù)器,服務(wù)器再返回給客戶端
- 后續(xù)客戶端每次請(qǐng)求都需要帶上token令牌
為什么 要有appKey + appSecret 這種成對(duì)出現(xiàn)的機(jī)制呢?
因?yàn)橐用? 通常用在首次驗(yàn)證(類似登錄場(chǎng)景),用 appKey(標(biāo)記要申請(qǐng)的權(quán)限有哪些) + appSecret(密碼, 表示你真的擁有這個(gè)權(quán)限)來(lái)申請(qǐng)一個(gè)token,就是我們經(jīng)常用到的 accessToken(通常擁有失效時(shí)間),后續(xù)的每次請(qǐng)求都需要提供accessToken 表明驗(yàn)證權(quán)限通過(guò)。
現(xiàn)在有了統(tǒng)一的appId,此時(shí)如果針對(duì)同一個(gè)業(yè)務(wù)要?jiǎng)澐植煌臋?quán)限,比如同一功能,某些場(chǎng)景需要只讀權(quán)限,某些場(chǎng)景需要讀寫權(quán)限。這樣提供一個(gè)appId和對(duì)應(yīng)的秘鑰appSecret就沒(méi)辦法滿足需求。此時(shí)就需要根據(jù)權(quán)限進(jìn)行賬號(hào)分配,通常使用appKey和appSecret。
由于 appKey 和 appSecret 是成對(duì)出現(xiàn)的賬號(hào), 同一個(gè) appId 可以對(duì)應(yīng)多個(gè) appKey+appSecret,這樣平臺(tái)就為不同的appKey+appSecret對(duì)分配不一樣的權(quán)限。
可以生成兩對(duì)appKey和appSecret。一個(gè)用于刪除,一個(gè)用于讀寫,達(dá)到權(quán)限的細(xì)粒度劃分。如 : appKey1 + appSecect1 只有刪除權(quán)限 但是 appKey2+appSecret2 有讀寫權(quán)限… 這樣你就可以把對(duì)應(yīng)的權(quán)限 放給不同的開(kāi)發(fā)者。其中權(quán)限的配置都是直接跟appKey 做關(guān)聯(lián)的,appKey 也需要添加數(shù)據(jù)庫(kù)索引, 方便快速查找
簡(jiǎn)化的場(chǎng)景:
第一種場(chǎng)景: 通常用于開(kāi)放性接口,像地圖api,會(huì)省去app_id和app_key,此時(shí)相當(dāng)于三者相等,合而為一 appId = appKey = appSecret。這種模式下,帶上app_id的目的僅僅是統(tǒng)計(jì)某一個(gè)用戶調(diào)用接口的次數(shù)而已了。
第二種場(chǎng)景: 當(dāng)每一個(gè)用戶有且僅有一套權(quán)限配置 可以去掉 appKey,直接將app_id = app_key,每個(gè)用戶分配一個(gè)appId+ appSecret就夠了。
也可以采用簽名(signature)的方式: 當(dāng)調(diào)用方向服務(wù)提供方法發(fā)起請(qǐng)求時(shí),帶上(appKey、時(shí)間戳timeStamp、隨機(jī)數(shù)nonce、簽名sign) 簽名sign 可以使用 (AppSecret + 時(shí)間戳 + 隨機(jī)數(shù))使用sha1、md5生成,服務(wù)提供方收到后,生成本地簽名和收到的簽名比對(duì),如果一致,校驗(yàn)成功
簽名流程
圖片
簽名規(guī)則
1.分配appId(開(kāi)發(fā)者標(biāo)識(shí))和appSecret(密鑰),給 不同的調(diào)用方
可以直接通過(guò)平臺(tái)線上申請(qǐng),也可以線下直接頒發(fā)。appId是全局唯一的,每個(gè)appId將對(duì)應(yīng)一個(gè)客戶,密鑰appSecret需要高度保密。
2.加入timeStamp(時(shí)間戳),以服務(wù)端當(dāng)前時(shí)間為準(zhǔn),單位為ms ,5分鐘內(nèi)數(shù)據(jù)有效
時(shí)間戳的目的就是為了減輕DOS攻擊。防止請(qǐng)求被攔截后一直嘗試請(qǐng)求接口。服務(wù)器端設(shè)置時(shí)間戳閥值,如果服務(wù)器時(shí)間 減 請(qǐng)求時(shí)間戳超過(guò)閥值,表示簽名超時(shí),接口調(diào)用失敗。
3.加入臨時(shí)流水號(hào)nonce,至少為10位 ,有效期內(nèi)防重復(fù)提交。
隨機(jī)值nonce 主要是為了增加簽名sign的多變性,也可以保護(hù)接口的冪等性,相鄰的兩次請(qǐng)求nonce不允許重復(fù),如果重復(fù)則認(rèn)為是重復(fù)提交,接口調(diào)用失敗。
- 針對(duì)查詢接口,流水號(hào)只用于日志落地,便于后期日志核查。
- 針對(duì)辦理類接口需校驗(yàn)流水號(hào)在有效期內(nèi)的唯一性,以避免重復(fù)請(qǐng)求。
通過(guò)在接口簽名請(qǐng)求參數(shù)加上 時(shí)間戳timeStamp + 隨機(jī)數(shù)nonce 可以防止 ”重放攻擊“
1)時(shí)間戳(timeStamp):
以服務(wù)端當(dāng)前時(shí)間為準(zhǔn),服務(wù)端要求客戶端發(fā)過(guò)來(lái)的時(shí)間戳,必須是最近60秒內(nèi)(假設(shè)值,自己定義)的。
這樣,即使這個(gè)請(qǐng)求即使被截取了,也只能在60s內(nèi)進(jìn)行重放攻擊。
2)隨機(jī)數(shù)(nonce):
但是,即使設(shè)置了時(shí)間戳,攻擊者還有60s的攻擊時(shí)間呢!
所以我們需要在客戶端請(qǐng)求中再加上一個(gè)隨機(jī)數(shù)(中間黑客不可能自己修改隨機(jī)數(shù),因?yàn)橛袇?shù)簽名的校驗(yàn)?zāi)兀?,服?wù)端會(huì)對(duì)一分鐘內(nèi)請(qǐng)求的隨機(jī)數(shù)進(jìn)行檢查,如果有兩個(gè)相同的,基本可以判定為重放攻擊。
因?yàn)檎G闆r下,在短時(shí)間內(nèi)(比如60s)連續(xù)生成兩個(gè)相同nonce的情況幾乎為0
服務(wù)端“第一次”在接收到這個(gè)nonce的時(shí)候做下面行為:
- 去redis中查找是否有key為nonce:{ nonce}的數(shù)據(jù)
- 如果沒(méi)有,則創(chuàng)建這個(gè)key,把這個(gè)key失效的時(shí)間和驗(yàn)證timestamp失效的時(shí)間一致,比如是60s。
- 如果有,說(shuō)明這個(gè)key在60s內(nèi)已經(jīng)被使用了,那么這個(gè)請(qǐng)求就可以判斷為重放請(qǐng)求。
4.加入簽名字段sign,獲取調(diào)用方傳遞的簽名信息。
通過(guò)在接口簽名請(qǐng)求參數(shù)加上 時(shí)間戳appId + sign 解決身份驗(yàn)證和防止 ”參數(shù)篡改“
- 請(qǐng)求攜帶參數(shù)appId和Sign,只有擁有合法的身份appId和正確的簽名Sign才能放行。這樣就解決了身份驗(yàn)證和參數(shù)篡改問(wèn)題。
- 即使請(qǐng)求參數(shù)被劫持,由于獲取不到appSecret(僅作本地加密使用,不參與網(wǎng)絡(luò)傳輸),也無(wú)法偽造合法的請(qǐng)求。
以上字段放在請(qǐng)求頭中。
API接口設(shè)計(jì)
根據(jù)你的具體需求和業(yè)務(wù)場(chǎng)景,以下是一個(gè)簡(jiǎn)單示例的API接口設(shè)計(jì):
1. 獲取資源列表接口
- URL: /api/resources
- HTTP 方法: GET
- 請(qǐng)求參數(shù):
page (可選): 頁(yè)碼
limit (可選): 每頁(yè)限制數(shù)量
- 響應(yīng):
- 成功狀態(tài)碼: 200 OK
- 響應(yīng)體: 返回資源列表的JSON數(shù)組
2. 創(chuàng)建資源接口
- URL: /api/resources
- HTTP 方法: POST
- 請(qǐng)求參數(shù):
name (必填): 資源名稱
description (可選): 資源描述
- 響應(yīng):
- 成功狀態(tài)碼: 201 Created
- 響應(yīng)體: 返回新創(chuàng)建資源的ID等信息
3. 更新資源接口
- URL: /api/resources/{resourceId}
- HTTP 方法: PUT
- 請(qǐng)求參數(shù):
resourceId (路徑參數(shù), 必填): 資源ID
name (可選): 更新后的資源名稱
description (可選): 更新后的資源描述
- 響應(yīng):
- 成功狀態(tài)碼: 200 OK
4. 刪除資源接口
- URL: /api/resources/{resourceId}
- HTTP 方法: DELETE
- 請(qǐng)求參數(shù):
resourceId (路徑參數(shù), 必填): 資源ID
- 響應(yīng):
- 成功狀態(tài)碼: 204 No Content
安全性考慮
為了確保安全性,可以采取以下措施:
- 使用HTTPS協(xié)議進(jìn)行數(shù)據(jù)傳輸,以保護(hù)通信過(guò)程中的數(shù)據(jù)安全。
- 在請(qǐng)求中使用AK和簽名進(jìn)行身份驗(yàn)證,并對(duì)請(qǐng)求進(jìn)行驗(yàn)簽,在服務(wù)端進(jìn)行校驗(yàn)和鑒權(quán),以防止非法請(qǐng)求和重放攻擊。
- 對(duì)敏感數(shù)據(jù)進(jìn)行加密傳輸,例如使用TLS加密算法對(duì)敏感數(shù)據(jù)進(jìn)行加密。
以上是一個(gè)簡(jiǎn)單的設(shè)計(jì)方案和API接口設(shè)計(jì)示例。具體的實(shí)現(xiàn)細(xì)節(jié)可能因項(xiàng)目需求而有所不同。在實(shí)際開(kāi)發(fā)中,還要考慮錯(cuò)誤處理、異常情況處理、日志記錄等方面。
防止重放攻擊和對(duì)敏感數(shù)據(jù)進(jìn)行加密傳輸都是保護(hù)三方接口安全的重要措施。以下是一些示例代碼,展示了如何實(shí)現(xiàn)這些功能。
防止重放攻擊
抓取報(bào)文原封不動(dòng)重復(fù)發(fā)送如果是付款接口,或者購(gòu)買接口就會(huì)造成損失,因此需要采用防重放的機(jī)制來(lái)做請(qǐng)求驗(yàn)證,如請(qǐng)求參數(shù)上加上timestamp時(shí)間戳+nonce隨機(jī)數(shù)。
重放攻擊是指黑客通過(guò)抓包的方式,得到客戶端的請(qǐng)求數(shù)據(jù)及請(qǐng)求連接,重復(fù)的向服務(wù)器發(fā)送請(qǐng)求的行為。
時(shí)間戳(tamp) + 數(shù)字簽名(sign), 也就是說(shuō)每次發(fā)送請(qǐng)求時(shí)多傳兩個(gè)參數(shù),分別為 tamp 和 sign。
數(shù)字簽名的作用是為了確保請(qǐng)求的有效性。因?yàn)楹灻墙?jīng)過(guò)加密的,只有客戶端和服務(wù)器知道加密方式及密鑰(key),所以第三方模擬不了。我們通過(guò)對(duì)sign的驗(yàn)證來(lái)判斷請(qǐng)求的有效性,如果sign驗(yàn)證失敗則判定為無(wú)效的請(qǐng)求,反之有效。但是數(shù)字簽名并不能阻止重放攻擊,因?yàn)楹诳涂梢宰ト∧愕膖amp和sign(不需做任何修改),然后發(fā)送請(qǐng)求。這個(gè)時(shí)候就要對(duì)時(shí)間戳進(jìn)行驗(yàn)證。
時(shí)間戳的作用是為了確保請(qǐng)求的時(shí)效性。我們將上一次請(qǐng)求的時(shí)間戳進(jìn)行存儲(chǔ),在下一次請(qǐng)求時(shí),將兩次時(shí)間戳進(jìn)行比對(duì)。如果此次請(qǐng)求的時(shí)間戳和上次的相同或小于上一次的時(shí)間戳,則判定此請(qǐng)求為過(guò)時(shí)請(qǐng)求,無(wú)效。因?yàn)檎G闆r下,第二次請(qǐng)求的時(shí)間肯定是比上一次的時(shí)間大的,不可能相等或小于。
如果修改了時(shí)間戳來(lái)滿足時(shí)間的時(shí)效性,sign驗(yàn)簽就不通過(guò)了。
注:如果客戶端是js,一定要對(duì)js做代碼混淆,禁止右鍵等。
1. 使用Nonce和Timestamp
在請(qǐng)求中添加唯一的Nonce(隨機(jī)數(shù))和Timestamp(時(shí)間戳),并將其包含在簽名計(jì)算中。服務(wù)端在驗(yàn)證簽名時(shí),可以檢查Nonce和Timestamp的有效性,并確保請(qǐng)求沒(méi)有被重放。
防止重放攻擊是在三方接口中非常重要的安全措施之一。使用Nonce(一次性隨機(jī)數(shù))和Timestamp(時(shí)間戳)結(jié)合起來(lái),可以有效地防止重放攻擊。下面是實(shí)現(xiàn)此功能的最佳實(shí)踐:
生成Nonce和Timestamp:
- Nonce應(yīng)該是一個(gè)隨機(jī)的、唯一的字符串,可以使用UUID或其他隨機(jī)字符串生成算法來(lái)創(chuàng)建。
- Timestamp表示請(qǐng)求的時(shí)間戳,通常使用標(biāo)準(zhǔn)的Unix時(shí)間戳格式(以秒為單位)。
在每個(gè)請(qǐng)求中包含Nonce和Timestamp:
- 將生成的Nonce和Timestamp作為參數(shù)添加到每個(gè)請(qǐng)求中,可以通過(guò)URL參數(shù)、請(qǐng)求頭或請(qǐng)求體的方式進(jìn)行傳遞。
- 確保Nonce和Timestamp在每個(gè)請(qǐng)求中都是唯一且正確的。
服務(wù)器端驗(yàn)證Nonce和Timestamp:
- 在服務(wù)器端接收到請(qǐng)求后,首先驗(yàn)證Nonce和Timestamp的有效性。
- 檢查Nonce是否已經(jīng)被使用過(guò),如果已經(jīng)被使用過(guò),則可能是重放攻擊,拒絕該請(qǐng)求。
- 檢查Timestamp是否在合理的時(shí)間范圍內(nèi),如果超出預(yù)定的有效期,則認(rèn)為請(qǐng)求無(wú)效。
存儲(chǔ)和管理Nonce:
- 為了驗(yàn)證Nonce是否已經(jīng)被使用過(guò),服務(wù)器需要存儲(chǔ)已經(jīng)使用過(guò)的Nonce。
- 可以使用數(shù)據(jù)庫(kù)、緩存或其他持久化存儲(chǔ)方式來(lái)管理Nonce的狀態(tài)。
- 需要定期清理過(guò)期的Nonce,以防止存儲(chǔ)占用過(guò)多的資源。
設(shè)置有效期:
- 為了限制請(qǐng)求的有效時(shí)間范圍,可以設(shè)置一個(gè)合理的有效期。
- 根據(jù)實(shí)際需求和業(yè)務(wù)場(chǎng)景,選擇適當(dāng)?shù)挠行冢鐜追昼娀驇仔r(shí)。
通過(guò)使用Nonce和Timestamp來(lái)防止重放攻擊,可以保護(hù)三方接口免受惡意重放請(qǐng)求的影響。以上是實(shí)現(xiàn)該功能的最佳實(shí)踐,但具體的實(shí)現(xiàn)方法可能因應(yīng)用程序和技術(shù)棧的不同而有所差異。確保在設(shè)計(jì)和實(shí)施安全措施時(shí)考慮到應(yīng)用程序的特定需求和風(fēng)險(xiǎn)模型。
2. 添加過(guò)期時(shí)間
在請(qǐng)求中添加一個(gè)過(guò)期時(shí)間字段(例如,token的有效期),并在服務(wù)端驗(yàn)證請(qǐng)求的時(shí)間戳是否在有效期內(nèi)。超過(guò)過(guò)期時(shí)間的請(qǐng)求應(yīng)被拒絕。
防篡改、防重放攻擊攔截器
每次HTTP請(qǐng)求,都需要加上timestamp參數(shù),然后把timestamp和其他參數(shù)一起進(jìn)行數(shù)字簽名。HTTP請(qǐng)求從發(fā)出到達(dá)服務(wù)器一般都不會(huì)超過(guò)60s,所以服務(wù)器收到HTTP請(qǐng)求之后,首先判斷時(shí)間戳參數(shù)與當(dāng)前時(shí)間相比較,是否超過(guò)了60s,如果超過(guò)了則認(rèn)為是非法的請(qǐng)求。
一般情況下,從抓包重放請(qǐng)求耗時(shí)遠(yuǎn)遠(yuǎn)超過(guò)了60s,所以此時(shí)請(qǐng)求中的timestamp參數(shù)已經(jīng)失效了,如果修改timestamp參數(shù)為當(dāng)前的時(shí)間戳,則signature參數(shù)對(duì)應(yīng)的數(shù)字簽名就會(huì)失效,因?yàn)椴恢篮灻罔€,沒(méi)有辦法生成新的數(shù)字簽名。
但這種方式的漏洞也是顯而易見(jiàn)的,如果在60s之后進(jìn)行重放攻擊,那就沒(méi)辦法了,所以這種方式不能保證請(qǐng)求僅一次有效 nonce的作用
nonce的意思是僅一次有效的隨機(jī)字符串,要求每次請(qǐng)求時(shí),該參數(shù)要保證不同。我們將每次請(qǐng)求的nonce參數(shù)存儲(chǔ)到一個(gè)“集合”中,每次處理HTTP請(qǐng)求時(shí),首先判斷該請(qǐng)求的nonce參數(shù)是否在該“集合”中,如果存在則認(rèn)為是非法請(qǐng)求。
nonce參數(shù)在首次請(qǐng)求時(shí),已經(jīng)被存儲(chǔ)到了服務(wù)器上的“集合”中,再次發(fā)送請(qǐng)求會(huì)被識(shí)別并拒絕。
nonce參數(shù)作為數(shù)字簽名的一部分,是無(wú)法篡改的,因?yàn)椴恢篮灻罔€,沒(méi)有辦法生成新的數(shù)字簽名。
這種方式也有很大的問(wèn)題,那就是存儲(chǔ)nonce參數(shù)的“集合”會(huì)越來(lái)越大。
nonce的一次性可以解決timestamp參數(shù)60s(防止重放攻擊)的問(wèn)題,timestamp可以解決nonce參數(shù)“集合”越來(lái)越大的問(wèn)題。
public class SignAuthInterceptor implements HandlerInterceptor {
private RedisTemplate<String, String> redisTemplate;
private String key;
public SignAuthInterceptor(RedisTemplate<String, String> redisTemplate, String key) {
this.redisTemplate = redisTemplate;
this.key = key;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 獲取時(shí)間戳
String timestamp = request.getHeader("timestamp");
// 獲取隨機(jī)字符串
String nonceStr = request.getHeader("nonceStr");
// 獲取簽名
String signature = request.getHeader("signature");
// 判斷時(shí)間是否大于xx秒(防止重放攻擊)
long NONCE_STR_TIMEOUT_SECONDS = 60L;
if (StrUtil.isEmpty(timestamp) || DateUtil.between(DateUtil.date(Long.parseLong(timestamp) * 1000), DateUtil.date(), DateUnit.SECOND) > NONCE_STR_TIMEOUT_SECONDS) {
throw new BusinessException("invalid timestamp");
}
// 判斷該用戶的nonceStr參數(shù)是否已經(jīng)在redis中(防止短時(shí)間內(nèi)的重放攻擊)
Boolean haveNonceStr = redisTemplate.hasKey(nonceStr);
if (StrUtil.isEmpty(nonceStr) || Objects.isNull(haveNonceStr) || haveNonceStr) {
throw new BusinessException("invalid nonceStr");
}
// 對(duì)請(qǐng)求頭參數(shù)進(jìn)行簽名
if (StrUtil.isEmpty(signature) || !Objects.equals(signature, this.signature(timestamp, nonceStr, request))) {
throw new BusinessException("invalid signature");
}
// 將本次用戶請(qǐng)求的nonceStr參數(shù)存到redis中設(shè)置xx秒后自動(dòng)刪除
redisTemplate.opsForValue().set(nonceStr, nonceStr, NONCE_STR_TIMEOUT_SECONDS, TimeUnit.SECONDS);
return true;
}
private String signature(String timestamp, String nonceStr, HttpServletRequest request) throws UnsupportedEncodingException {
Map<String, Object> params = new HashMap<>(16);
Enumeration<String> enumeration = request.getParameterNames();
if (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = request.getParameter(name);
params.put(name, URLEncoder.encode(value, CommonConstants.UTF_8));
}
String qs = String.format("%s×tamp=%s&nnotallow=%s&key=%s", this.sortQueryParamString(params), timestamp, nonceStr, key);
log.info("qs:{}", qs);
String sign = SecureUtil.md5(qs).toLowerCase();
log.info("sign:{}", sign);
return sign;
}
/**
* 按照字母順序進(jìn)行升序排序
*
* @param params 請(qǐng)求參數(shù) 。注意請(qǐng)求參數(shù)中不能包含key
* @return 排序后結(jié)果
*/
private String sortQueryParamString(Map<String, Object> params) {
List<String> listKeys = Lists.newArrayList(params.keySet());
Collections.sort(listKeys);
StrBuilder content = StrBuilder.create();
for (String param : listKeys) {
content.append(param).append("=").append(params.get(param).toString()).append("&");
}
if (content.length() > 0) {
return content.subString(0, content.length() - 1);
}
return content.toString();
}
}
注冊(cè)攔截器指定攔截接口
對(duì)敏感數(shù)據(jù)進(jìn)行加密傳輸
使用TLS(傳輸層安全)協(xié)議可以保證通信過(guò)程中的數(shù)據(jù)加密和完整性。以下是一些基本步驟:
- 在服務(wù)器上配置TLS證書(shū)(包括公鑰和私鑰)。
- 客戶端和服務(wù)器之間建立TLS連接??蛻舳讼蚍?wù)器發(fā)送HTTPS請(qǐng)求。
- 在TLS握手期間,客戶端和服務(wù)器協(xié)商加密算法和密鑰交換方法。
- 握手成功后,客戶端和服務(wù)器之間的所有數(shù)據(jù)傳輸都會(huì)經(jīng)過(guò)加密處理。
具體的實(shí)現(xiàn)取決于所使用的編程語(yǔ)言和框架。以下是使用Java的示例代碼,演示如何使用TLS進(jìn)行加密傳輸:
// 創(chuàng)建SSLContext對(duì)象
SSLContext sslContext = SSLContext.getInstance("TLS");
// 初始化SSLContext,加載證書(shū)和私鑰
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(new FileInputStream("keystore.jks"), "password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
// 創(chuàng)建HttpsURLConnection連接
URL url = new URL("https://api.example.com/endpoint");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
// 設(shè)置其他請(qǐng)求參數(shù)、發(fā)送請(qǐng)求、處理響應(yīng)等
這段代碼中,我們創(chuàng)建了一個(gè)SSLContext對(duì)象并初始化它,加載了服務(wù)器的證書(shū)和私鑰。然后,通過(guò)HttpsURLConnection對(duì)象,設(shè)置了TLS的安全套接字工廠,并與指定的URL建立了HTTPS連接。
請(qǐng)注意,你需要將實(shí)際的證書(shū)和私鑰文件(通常是.jks格式)替換為真實(shí)的文件路徑,并提供正確的密碼。
以上代碼只是一個(gè)簡(jiǎn)單的示例,實(shí)際部署時(shí)可能需要根據(jù)具體要求進(jìn)行更多配置。確保在項(xiàng)目中遵循最佳實(shí)踐和安全建議,并與相應(yīng)的開(kāi)發(fā)和運(yùn)維團(tuán)隊(duì)合作,以確保三方接口的安全性。
AK和SK生成方案
開(kāi)發(fā)一個(gè)三方接口,并提供給客戶使用,可以考慮以下方法來(lái)生成AK(Access Key)和SK(Secret Key):
設(shè)計(jì)API密鑰管理系統(tǒng):
- 創(chuàng)建一個(gè)API密鑰管理系統(tǒng),用于生成和管理AK和SK。這個(gè)系統(tǒng)可以是一個(gè)獨(dú)立的服務(wù)器應(yīng)用或與你的主應(yīng)用集成在一起。
生成AK和SK:
- 在API密鑰管理系統(tǒng)中,為每個(gè)客戶生成唯一的AK和SK。
- AK通常是一個(gè)公開(kāi)的標(biāo)識(shí)符,用于標(biāo)識(shí)客戶的身份??梢允褂秒S機(jī)字符串、UUID等方式生成。
- SK是一個(gè)保密的私鑰,用于生成身份驗(yàn)證簽名和加密訪問(wèn)令牌??梢允褂秒S機(jī)字符串、哈希函數(shù)等方式生成,并確保其足夠安全。
*存儲(chǔ)和管理AK和SK:
- 將生成的AK和SK存儲(chǔ)在數(shù)據(jù)庫(kù)或其他持久化存儲(chǔ)中,并與客戶的其他相關(guān)信息關(guān)聯(lián)起來(lái)。
- 需要實(shí)施適當(dāng)?shù)臋?quán)限控制和安全措施,以確保只有授權(quán)的用戶可以訪問(wèn)和管理AK和SK。
- 可以考慮對(duì)SK進(jìn)行加密處理,以增加安全性。
提供API密鑰分發(fā)機(jī)制:
- 客戶可以通過(guò)你提供的界面、API或者自助注冊(cè)流程來(lái)獲取他們的AK和SK。
- 在分發(fā)過(guò)程中,確保以安全的方式將AK和SK傳遞給客戶。例如,使用加密連接或其他安全通道進(jìn)行傳輸。
安全性和最佳實(shí)踐:
- 強(qiáng)烈建議對(duì)API密鑰管理系統(tǒng)進(jìn)行安全審計(jì),并根據(jù)最佳實(shí)踐來(lái)保護(hù)和管理AK和SK。
- 定期輪換AK和SK,以增加安全性并降低潛在風(fēng)險(xiǎn)。
- 在設(shè)計(jì)接口時(shí),使用AK和SK進(jìn)行身份驗(yàn)證和權(quán)限控制,以防止未經(jīng)授權(quán)的訪問(wèn)。
請(qǐng)注意,上述步驟提供了一般性的指導(dǎo),具體實(shí)現(xiàn)可能因你的應(yīng)用程序需求、技術(shù)棧和安全策略而有所不同。確保遵循安全最佳實(shí)踐,并參考相關(guān)的安全文檔和建議,以確保生成的AK和SK的安全性和可靠性。
CREATE TABLE api_credentials (
id INT AUTO_INCREMENT PRIMARY KEY,
app_id VARCHAR(255) NOT NULL,
access_key VARCHAR(255) NOT NULL,
secret_key VARCHAR(255) NOT NULL,
valid_from DATETIME NOT NULL,
valid_to DATETIME NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
allowed_endpoints VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
這個(gè)表包含以下字段:
- id:主鍵,自增的唯一標(biāo)識(shí)符。
- app_id:應(yīng)用程序ID或標(biāo)識(shí)符,用于關(guān)聯(lián)AKSK與特定應(yīng)用程序。
- access_key:訪問(wèn)密鑰(AK),用于標(biāo)識(shí)客戶身份。
- secret_key:秘密密鑰(SK),用于生成簽名和進(jìn)行身份驗(yàn)證。
- valid_from:AKSK有效期起始時(shí)間。
- valid_to:AKSK有效期結(jié)束時(shí)間。
- enabled:是否啟用該AKSK,1表示啟用,0表示禁用。
- allowed_endpoints:逗號(hào)分隔的允許訪問(wèn)的接口/端點(diǎn)列表。
- created_at:記錄創(chuàng)建時(shí)間。
在實(shí)際使用中,你可能需要根據(jù)具體需求對(duì)字段進(jìn)行調(diào)整或添加索引以提高性能。此外,還可以考慮添加其他字段來(lái)滿足你的應(yīng)用程序的需求,例如描述、所屬用戶等。
請(qǐng)注意,具體的設(shè)計(jì)可能會(huì)因你的應(yīng)用程序需求和使用場(chǎng)景而有所不同。確保在實(shí)施前仔細(xì)考慮你的業(yè)務(wù)要求,并遵循良好的數(shù)據(jù)庫(kù)設(shè)計(jì)原則和最佳實(shí)踐。
API接口設(shè)計(jì)補(bǔ)充
圖片
1.使用POST作為接口請(qǐng)求方式
一般調(diào)用接口最常用的兩種方式就是GET和POST。兩者的區(qū)別也很明顯,GET請(qǐng)求會(huì)將參數(shù)暴露在瀏覽器URL中,而且對(duì)長(zhǎng)度也有限制。為了更高的安全性,所有接口都采用POST方式請(qǐng)求。
2.客戶端IP白名單
ip白名單是指將接口的訪問(wèn)權(quán)限對(duì)部分ip進(jìn)行開(kāi)放來(lái)避免其他ip進(jìn)行訪問(wèn)攻擊。
- 設(shè)置ip白名單缺點(diǎn)就是當(dāng)你的客戶端進(jìn)行遷移后,就需要重新聯(lián)系服務(wù)提供者添加新的ip白名單。
- 設(shè)置ip白名單的方式很多,除了傳統(tǒng)的防火墻之外,spring cloud alibaba提供的組件sentinel也支持白名單設(shè)置。
- 為了降低api的復(fù)雜度,推薦使用防火墻規(guī)則進(jìn)行白名單設(shè)置。
3. 單個(gè)接口針對(duì)ip限流
限流是為了更好的維護(hù)系統(tǒng)穩(wěn)定性。
使用redis進(jìn)行接口調(diào)用次數(shù)統(tǒng)計(jì),ip+接口地址作為key,訪問(wèn)次數(shù)作為value,每次請(qǐng)求value+1,設(shè)置過(guò)期時(shí)長(zhǎng)來(lái)限制接口的調(diào)用頻率。
4. 記錄接口請(qǐng)求日志
記錄請(qǐng)求日志,快速定位異常請(qǐng)求位置,排查問(wèn)題原因。(如:用aop來(lái)全局處理接口請(qǐng)求)
5. 敏感數(shù)據(jù)脫敏
在接口調(diào)用過(guò)程中,可能會(huì)涉及到訂單號(hào)等敏感數(shù)據(jù),這類數(shù)據(jù)通常需要脫敏處理
最常用的方式就是加密。加密方式使用安全性比較高的RSA非對(duì)稱加密。非對(duì)稱加密算法有兩個(gè)密鑰,這兩個(gè)密鑰完全不同但又完全匹配。只有使用匹配的一對(duì)公鑰和私鑰,才能完成對(duì)明文的加密和解密過(guò)程。
6.冪等性問(wèn)題
冪等性是指: 任意多次請(qǐng)求的執(zhí)行結(jié)果和一次請(qǐng)求的執(zhí)行結(jié)果所產(chǎn)生的影響相同。
- 說(shuō)的直白一點(diǎn)就是查詢操作無(wú)論查詢多少次都不會(huì)影響數(shù)據(jù)本身,因此查詢操作本身就是冪等的。
- 但是新增操作,每執(zhí)行一次數(shù)據(jù)庫(kù)就會(huì)發(fā)生變化,所以它是非冪等的。
冪等問(wèn)題的解決有很多思路,這里講一種比較嚴(yán)謹(jǐn)?shù)摹?/p>
- 提供一個(gè)生成隨機(jī)數(shù)的接口,隨機(jī)數(shù)全局唯一。調(diào)用接口的時(shí)候帶入隨機(jī)數(shù)。
- 第一次調(diào)用,業(yè)務(wù)處理成功后,將隨機(jī)數(shù)作為key,操作結(jié)果作為value,存入redis,同時(shí)設(shè)置過(guò)期時(shí)長(zhǎng)。
- 第二次調(diào)用,查詢r(jià)edis,如果key存在,則證明是重復(fù)提交,直接返回錯(cuò)誤。
7.版本控制
一套成熟的API文檔,一旦發(fā)布是不允許隨意修改接口的。這時(shí)候如果想新增或者修改接口,就需要加入版本控制,版本號(hào)可以是整數(shù)類型,也可以是浮點(diǎn)數(shù)類型。
一般接口地址都會(huì)帶上版本號(hào),http://ip:port//v1/list , http://ip:port//v2/list
8.響應(yīng)狀態(tài)碼規(guī)范
一個(gè)牛逼的API,還需要提供簡(jiǎn)單明了的響應(yīng)值,根據(jù)狀態(tài)碼就可以大概知道問(wèn)題所在。我們采用http的狀態(tài)碼進(jìn)行數(shù)據(jù)封裝,例如200表示請(qǐng)求成功,4xx表示客戶端錯(cuò)誤,5xx表示服務(wù)器內(nèi)部發(fā)生錯(cuò)誤。
狀態(tài)碼設(shè)計(jì)參考如下:
public enum CodeEnum {// 根據(jù)業(yè)務(wù)需求進(jìn)行添加
SUCCESS(200, "處理成功"),ERROR_PATH(404, "請(qǐng)求地址錯(cuò)誤"),
ERROR_SERVER(505, "服務(wù)器內(nèi)部發(fā)生錯(cuò)誤");
private int code;
private String message;
CodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
9.統(tǒng)一響應(yīng)數(shù)據(jù)格式
為了方便給客戶端響應(yīng),響應(yīng)數(shù)據(jù)會(huì)包含三個(gè)屬性,狀態(tài)碼(code),信息描述(message),響應(yīng)數(shù)據(jù)(data)??蛻舳烁鶕?jù)狀態(tài)碼及信息描述可快速知道接口,如果狀態(tài)碼返回成功,再開(kāi)始處理數(shù)據(jù)。
public class Result implements Serializable {
private static final long serialVersionUID = 793034041048451317L;
private int code;
private String message;
private Object data = null;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
/** * 放入響應(yīng)枚舉 */
public Result fillCode(CodeEnum codeEnum) {
this.setCode(codeEnum.getCode());
this.setMessage(codeEnum.getMessage());
return this;
}
/** * 放入響應(yīng)碼及信息 */
public Result fillCode(int code, String message) {
this.setCode(code);
this.setMessage(message);
return this;
}
/** * 處理成功,放入自定義業(yè)務(wù)數(shù)據(jù)集合 */
public Result fillData(Object data) {
this.setCode(CodeEnum.SUCCESS.getCode());
this.setMessage(CodeEnum.SUCCESS.getMessage());
this.data = data;
return this;
}
}
10.接口文檔
一個(gè)好的API還少不了一個(gè)優(yōu)秀的接口文檔。接口文檔的可讀性非常重要,雖然很多程序員都不喜歡寫文檔,而且不喜歡別人不寫文檔。為了不增加程序員的壓力,推薦使用swagger2或其他接口管理工具,通過(guò)簡(jiǎn)單配置,就可以在開(kāi)發(fā)中測(cè)試接口的連通性,上線后也可以生成離線文檔用于管理API
11.生成簽名sign的詳細(xì)步驟
結(jié)合案例詳細(xì)說(shuō)明怎么生成簽名signature(寫完上面的博客后,得出的感悟)
第1步: 將所有參數(shù)(注意是所有參數(shù),包括appId,timeStamp,nonce),除去sign本身,以及值是空的參數(shù),按key名升序排序存儲(chǔ)。
第2步: 然后把排序后的參數(shù)按 key1value1key2value2…keyXvalueX的方式拼接成一個(gè)字符串。
這里的參數(shù)和值必須是傳輸參數(shù)的原始值,不能是經(jīng)過(guò)處理的,如不能將"轉(zhuǎn)成”后再拼接)
第3步: 把分配給調(diào)用方的密鑰secret拼接在第2步得到的字符串最后面。
即: key1value1key2value2…keyXvalueX + secret
第4步: 計(jì)算第3步字符串的md5值(32位),然后轉(zhuǎn)成大寫,最終得到的字符串作為簽名sign。
即: Md5(key1value1key2value2…keyXvalueX + secret) 轉(zhuǎn)大寫
舉例:
假設(shè)傳輸?shù)臄?shù)據(jù)是
http://www.xxx.com/openApi?sign=sign_value&k1=v1&k2=v2&method=cancel&k3=&kX=vX
請(qǐng)求頭是
appId:zs001timeStamp:1612691221000sign:2B42AAED20E4B2D5BA389F7C344FE91Bnonce:1234567890
實(shí)際情況最好是通過(guò)post方式發(fā)送,其中sign參數(shù)對(duì)應(yīng)的sign_value就是簽名的值。
第一步:拼接字符串。
首先去除sign參數(shù)本身,然后去除值是空的參數(shù)k3,剩下appId=zs001&timeStamp=1612691221000&nnotallow=1234567890&k1=v1&k2=v2&&method=cancel&kX=vX,然后按參數(shù)名字符升序排序,appId=zs001&k1=v1&k2=v2&kX=vX&method=cancel&nnotallow=1234567890&timeStamp=1612691221000
第二步:將參數(shù)名和值的拼接
appIdzs001k1v1k2v2kXvXmethodcancelnonce1234567890timeStamp1612691221000
第三步:在上面拼接得到的字符串前加上密鑰secret
假設(shè)是miyao,得到新的字符串a(chǎn)ppIdzs001k1v1k2v2kXvXmethodcancelnonce1234567890timeStamp1612691221000miyao
第四步:然后將這個(gè)字符串進(jìn)行md5計(jì)算
假設(shè)得到的是abcdef,然后轉(zhuǎn)為大寫,得到ABCDEF這個(gè)值作為簽名sign
注意,計(jì)算md5之前調(diào)用方需確保簽名加密字符串編碼與提供方一致,如統(tǒng)一使用utf-8編碼或者GBK編碼,如果編碼方式不一致則計(jì)算出來(lái)的簽名會(huì)校驗(yàn)失敗。
上面說(shuō)的請(qǐng)求錄音可拼可不拼接,主要還是為了增強(qiáng)簽名的復(fù)雜性
12.什么是token?
Token是什么?
token即 訪問(wèn)令牌access token,用于接口中標(biāo)識(shí)接口調(diào)用者的身份、憑證,減少用戶名和密碼的傳輸次數(shù)。 一般情況下客戶端(接口調(diào)用方)需要先向服務(wù)器端申請(qǐng)一個(gè)接口調(diào)用的賬號(hào),服務(wù)器會(huì)給出一個(gè)appId和一個(gè)appSecret(appSecret用于參數(shù)簽名使用)
注意appSecret保存到客戶端,需要做一些安全處理,防止泄露。
Token的值一般是UUID,服務(wù)端生成Token后需要將token做為key,將一些和token關(guān)聯(lián)的信息作為value保存到緩存服務(wù)器中(redis),當(dāng)一個(gè)請(qǐng)求過(guò)來(lái)后,服務(wù)器就去緩存服務(wù)器中查詢這個(gè)Token是否存在,存在則調(diào)用接口,不存在返回接口錯(cuò)誤,一般通過(guò)攔截器或者過(guò)濾器來(lái)實(shí)現(xiàn)。
Token分為兩種
- API Token(接口令牌): 用于訪問(wèn)不需要用戶登錄的接口,如登錄、注冊(cè)、一些基本數(shù)據(jù)的獲取等。獲取接口令牌需要拿appId、timestamp和sign來(lái)?yè)Q,sign=加密(參數(shù)1+…+參數(shù)n+timestamp+key)
- USER Token(用戶令牌): 用于訪問(wèn)需要用戶登錄之后的接口,如:獲取我的基本信息、保存、修改、刪除等操作。獲取用戶令牌需要拿用戶名和密碼來(lái)?yè)Q
12.1Token+簽名(有用戶狀態(tài)的接口簽名)
上面講的接口簽名方式都是無(wú)狀態(tài)的,在APP開(kāi)放API接口的設(shè)計(jì)中,由于大多數(shù)接口涉及到用戶的個(gè)人信息以及產(chǎn)品的敏感數(shù)據(jù),所以要對(duì)這些接口進(jìn)行身份驗(yàn)證,為了安全起見(jiàn)讓用戶暴露的明文密碼次數(shù)越少越好,然而客戶端與服務(wù)器的交互在請(qǐng)求之間是無(wú)狀態(tài)的,也就是說(shuō),當(dāng)涉及到用戶狀態(tài)時(shí),每次請(qǐng)求都要帶上身份驗(yàn)證信息(令牌token)。
1)Token身份驗(yàn)證
- 用戶登錄向服務(wù)器提供認(rèn)證信息(如賬號(hào)和密碼),服務(wù)器驗(yàn)證成功后返回Token給客戶端;
- 客戶端將Token緩存在本地,后續(xù)每次發(fā)起請(qǐng)求時(shí),都要攜帶此Token;
- 服務(wù)端檢查Token的有效性,有效則放行,無(wú)效(Token錯(cuò)誤或過(guò)期)則拒絕。
弊端:Token被劫持,偽造請(qǐng)求和篡改參數(shù)。
2)Token+簽名驗(yàn)證
與上面接口簽名規(guī)則一樣,為客戶端分配appSecret(密鑰,用于接口加密,不參與傳輸),將appSecret和所有請(qǐng)求參數(shù)組合成一個(gè)字符串,根據(jù)簽名算法生成簽名值,發(fā)送請(qǐng)求時(shí)將簽名值一起發(fā)送給服務(wù)器驗(yàn)證。
這樣,即使Token被劫持,對(duì)方不知道appSecret和簽名算法,就無(wú)法偽造請(qǐng)求和篡改參數(shù),并且有了token后也能正確的獲取到用戶的狀態(tài)
登陸和退出請(qǐng)求
圖片
后續(xù)請(qǐng)求
客戶端: 與上面接口簽名規(guī)則一樣類似,把a(bǔ)ppId改為token即可。
圖片