聊聊三方接口調(diào)用設(shè)計指南
在為第三方系統(tǒng)提供接口的時候,肯定要考慮接口數(shù)據(jù)的安全問題,比如數(shù)據(jù)是否被篡改,數(shù)據(jù)是否已經(jīng)過時,數(shù)據(jù)是否可以重復(fù)提交等問題。
在設(shè)計三方接口調(diào)用的方案時,需要考慮到安全性和可用性。以下是一種設(shè)計方案的概述,其中包括使用API密鑰(Access Key/Secret Key)進行身份驗證和設(shè)置回調(diào)地址。
設(shè)計方案概述
1.API密鑰生成: 為每個三方應(yīng)用生成唯一的API密鑰對(AK/SK),其中AK用于標(biāo)識應(yīng)用,SK用于進行簽名和加密。
AK:Access Key Id,用于標(biāo)示用戶。
SK:Secret Access Key,是用戶用于加密認證字符串和用來驗證認證字符串的密鑰,其中SK必須保密。
通過使用Access Key Id / Secret Access Key加密的方法來驗證某個請求的發(fā)送者身份。
2.接口鑒權(quán): 在進行接口調(diào)用時,客戶端需要使用AK和請求參數(shù)生成簽名,并將其放入請求頭或參數(shù)中以進行身份驗證。
3.回調(diào)地址設(shè)置: 三方應(yīng)用提供回調(diào)地址,用于接收異步通知和回調(diào)結(jié)果。
4.接口API設(shè)計: 設(shè)計接口的URL、HTTP方法、請求參數(shù)、響應(yīng)格式等細節(jié)。
權(quán)限劃分
appID:應(yīng)用的唯一標(biāo)識
用來標(biāo)識你的開發(fā)者賬號的,即:用戶id,可以在數(shù)據(jù)庫添加索引,方便快速查找,同一個 appId 可以對應(yīng)多個 appKey+appSecret,達到權(quán)限的
appKey:公匙(相當(dāng)于賬號)
公開的,調(diào)用服務(wù)所需要的密鑰。是用戶的身份認證標(biāo)識,用于調(diào)用平臺可用服務(wù),可以簡單理解成是賬號。
appSecret:私匙(相當(dāng)于密碼)
簽名的密鑰,是跟appKey配套使用的,可以簡單理解成是密碼。
token:令牌(過期失效)
使用方法
- 向第三方服務(wù)器請求授權(quán)時,帶上AppKey和AppSecret(需存在服務(wù)器端)
- 第三方服務(wù)器驗證appKey和appSecret在數(shù)據(jù)庫、緩存中有沒有記錄
- 如果有,生成一串唯一的字符串(token令牌),返回給服務(wù)器,服務(wù)器再返回給客戶端
- 后續(xù)客戶端每次請求都需要帶上token令牌
為什么要有appKey + appSecret 這種成對出現(xiàn)的機制呢?
因為要加密, 通常用在首次驗證(類似登錄場景),用 appKey(標(biāo)記要申請的權(quán)限有哪些) + appSecret(密碼, 表示你真的擁有這個權(quán)限)來申請一個token,就是我們經(jīng)常用到的 accessToken(通常擁有失效時間),后續(xù)的每次請求都需要提供accessToken 表明驗證權(quán)限通過。
現(xiàn)在有了統(tǒng)一的appId,此時如果針對同一個業(yè)務(wù)要劃分不同的權(quán)限,比如同一功能,某些場景需要只讀權(quán)限,某些場景需要讀寫權(quán)限。這樣提供一個appId和對應(yīng)的秘鑰appSecret就沒辦法滿足需求。此時就需要根據(jù)權(quán)限進行賬號分配,通常使用appKey和appSecret。
由于 appKey 和 appSecret 是成對出現(xiàn)的賬號, 同一個 appId 可以對應(yīng)多個 appKey+appSecret,這樣平臺就為不同的appKey+appSecret對分配不一樣的權(quán)限。
可以生成兩對appKey和appSecret。一個用于刪除,一個用于讀寫,達到權(quán)限的細粒度劃分。如 : appKey1 + appSecect1 只有刪除權(quán)限 但是 appKey2+appSecret2 有讀寫權(quán)限… 這樣你就可以把對應(yīng)的權(quán)限 放給不同的開發(fā)者。其中權(quán)限的配置都是直接跟appKey 做關(guān)聯(lián)的,appKey 也需要添加數(shù)據(jù)庫索引, 方便快速查找
簡化的場景:
第一種場景: 通常用于開放性接口,像地圖api,會省去app_id和app_key,此時相當(dāng)于三者相等,合而為一 appId = appKey = appSecret。這種模式下,帶上app_id的目的僅僅是統(tǒng)計某一個用戶調(diào)用接口的次數(shù)而已了。
第二種場景: 當(dāng)每一個用戶有且僅有一套權(quán)限配置可以去掉 appKey,直接將app_id = app_key,每個用戶分配一個appId+ appSecret就夠了。
也可以采用簽名(signature)的方式: 當(dāng)調(diào)用方向服務(wù)提供方法發(fā)起請求時,帶上(appKey、時間戳timeStamp、隨機數(shù)nonce、簽名sign),簽名sign 可以使用 (AppSecret + 時間戳 + 隨機數(shù))使用sha1、md5生成,服務(wù)提供方收到后,生成本地簽名和收到的簽名比對,如果一致,校驗成功。
簽名流程
圖片
簽名規(guī)則
1.分配appId(開發(fā)者標(biāo)識)和appSecret(密鑰),給不同的調(diào)用方
可以直接通過平臺線上申請,也可以線下直接頒發(fā)。appId是全局唯一的,每個appId將對應(yīng)一個客戶,密鑰appSecret需要高度保密。
2.加入timeStamp(時間戳),以服務(wù)端當(dāng)前時間為準(zhǔn),單位為ms ,5分鐘內(nèi)數(shù)據(jù)有效
時間戳的目的就是為了減輕DOS攻擊。防止請求被攔截后一直嘗試請求接口。服務(wù)器端設(shè)置時間戳閥值,如果服務(wù)器時間減請求時間戳超過閥值,表示簽名超時,接口調(diào)用失敗。
3.加入臨時流水號nonce,至少為10位 ,有效期內(nèi)防重復(fù)提交。
隨機值nonce 主要是為了增加簽名sign的多變性,也可以保護接口的冪等性,相鄰的兩次請求nonce不允許重復(fù),如果重復(fù)則認為是重復(fù)提交,接口調(diào)用失敗。
- 針對查詢接口,流水號只用于日志落地,便于后期日志核查。
- 針對辦理類接口需校驗流水號在有效期內(nèi)的唯一性,以避免重復(fù)請求。
通過在接口簽名請求參數(shù)加上 時間戳timeStamp + 隨機數(shù)nonce 可以防止 ”重放攻擊“
- 時間戳(timeStamp):
以服務(wù)端當(dāng)前時間為準(zhǔn),服務(wù)端要求客戶端發(fā)過來的時間戳,必須是最近60秒內(nèi)(假設(shè)值,自己定義)的。
這樣,即使這個請求即使被截取了,也只能在60s內(nèi)進行重放攻擊。
- 隨機數(shù)(nonce):
但是,即使設(shè)置了時間戳,攻擊者還有60s的攻擊時間呢!
所以我們需要在客戶端請求中再加上一個隨機數(shù)(中間黑客不可能自己修改隨機數(shù),因為有參數(shù)簽名的校驗?zāi)兀?,服?wù)端會對一分鐘內(nèi)請求的隨機數(shù)進行檢查,如果有兩個相同的,基本可以判定為重放攻擊。
因為正常情況下,在短時間內(nèi)(比如60s)連續(xù)生成兩個相同nonce的情況幾乎為0
服務(wù)端“第一次”在接收到這個nonce的時候做下面行為:
- 去redis中查找是否有key為nonce:{ nonce}的數(shù)據(jù)
- 如果沒有,則創(chuàng)建這個key,把這個key失效的時間和驗證timestamp失效的時間一致,比如是60s。
- 如果有,說明這個key在60s內(nèi)已經(jīng)被使用了,那么這個請求就可以判斷為重放請求。
4.加入簽名字段sign,獲取調(diào)用方傳遞的簽名信息。
通過在接口簽名請求參數(shù)加上 時間戳appId + sign 解決身份驗證和防止 ”參數(shù)篡改“
- 請求攜帶參數(shù)appId和Sign,只有擁有合法的身份appId和正確的簽名Sign才能放行。這樣就解決了身份驗證和參數(shù)篡改問題。
- 即使請求參數(shù)被劫持,由于獲取不到appSecret(僅作本地加密使用,不參與網(wǎng)絡(luò)傳輸),也無法偽造合法的請求。
以上字段放在請求頭中。
API接口設(shè)計
根據(jù)你的具體需求和業(yè)務(wù)場景,以下是一個簡單示例的API接口設(shè)計:
1. 獲取資源列表接口
- URL: /api/resources
- HTTP 方法: GET
- 請求參數(shù)
page (可選): 頁碼
limit (可選): 每頁限制數(shù)量
- 響應(yīng)
成功狀態(tài)碼: 200 OK
響應(yīng)體: 返回資源列表的JSON數(shù)組
2. 創(chuàng)建資源接口
- URL: /api/resources
- HTTP 方法: POST
- 請求參數(shù)
name (必填): 資源名稱
description (可選): 資源描述
- 響應(yīng)
成功狀態(tài)碼: 201 Created
響應(yīng)體: 返回新創(chuàng)建資源的ID等信息
3. 更新資源接口
- URL: /api/resources/{resourceId}
- HTTP 方法: PUT
- 請求參數(shù)
resourceId (路徑參數(shù), 必填): 資源ID
name (可選): 更新后的資源名稱
description (可選): 更新后的資源描述
- 響應(yīng)
成功狀態(tài)碼: 200 OK
4. 刪除資源接口
- URL: /api/resources/{resourceId}
- HTTP 方法: DELETE
- 請求參數(shù)
resourceId (路徑參數(shù), 必填): 資源ID
- 響應(yīng)
成功狀態(tài)碼: 204 No Content
安全性考慮
為了確保安全性,可以采取以下措施:
- 使用HTTPS協(xié)議進行數(shù)據(jù)傳輸,以保護通信過程中的數(shù)據(jù)安全。
- 在請求中使用AK和簽名進行身份驗證,并對請求進行驗簽,在服務(wù)端進行校驗和鑒權(quán),以防止非法請求和重放攻擊。
- 對敏感數(shù)據(jù)進行加密傳輸,例如使用TLS加密算法對敏感數(shù)據(jù)進行加密。
以上是一個簡單的設(shè)計方案和API接口設(shè)計示例。具體的實現(xiàn)細節(jié)可能因項目需求而有所不同。在實際開發(fā)中,還要考慮錯誤處理、異常情況處理、日志記錄等方面。
防止重放攻擊和對敏感數(shù)據(jù)進行加密傳輸都是保護三方接口安全的重要措施。以下是一些示例代碼,展示了如何實現(xiàn)這些功能。
防止重放攻擊
抓取報文原封不動重復(fù)發(fā)送如果是付款接口,或者購買接口就會造成損失,因此需要采用防重放的機制來做請求驗證,如請求參數(shù)上加上timestamp時間戳+nonce隨機數(shù)。
重放攻擊是指黑客通過抓包的方式,得到客戶端的請求數(shù)據(jù)及請求連接,重復(fù)的向服務(wù)器發(fā)送請求的行為。
時間戳(tamp) + 數(shù)字簽名(sign), 也就是說每次發(fā)送請求時多傳兩個參數(shù),分別為 tamp 和 sign。
數(shù)字簽名的作用是為了確保請求的有效性。因為簽名是經(jīng)過加密的,只有客戶端和服務(wù)器知道加密方式及密鑰(key),所以第三方模擬不了。我們通過對sign的驗證來判斷請求的有效性,如果sign驗證失敗則判定為無效的請求,反之有效。但是數(shù)字簽名并不能阻止重放攻擊,因為黑客可以抓取你的tamp和sign(不需做任何修改),然后發(fā)送請求。這個時候就要對時間戳進行驗證。
時間戳的作用是為了確保請求的時效性。我們將上一次請求的時間戳進行存儲,在下一次請求時,將兩次時間戳進行比對。如果此次請求的時間戳和上次的相同或小于上一次的時間戳,則判定此請求為過時請求,無效。因為正常情況下,第二次請求的時間肯定是比上一次的時間大的,不可能相等或小于。
如果修改了時間戳來滿足時間的時效性,sign驗簽就不通過了。
注:如果客戶端是js,一定要對js做代碼混淆,禁止右鍵等。
1. 使用Nonce和Timestamp
在請求中添加唯一的Nonce(隨機數(shù))和Timestamp(時間戳),并將其包含在簽名計算中。服務(wù)端在驗證簽名時,可以檢查Nonce和Timestamp的有效性,并確保請求沒有被重放。
防止重放攻擊是在三方接口中非常重要的安全措施之一。使用Nonce(一次性隨機數(shù))和Timestamp(時間戳)結(jié)合起來,可以有效地防止重放攻擊。下面是實現(xiàn)此功能的最佳實踐:
生成Nonce和Timestamp:
- Nonce應(yīng)該是一個隨機的、唯一的字符串,可以使用UUID或其他隨機字符串生成算法來創(chuàng)建。
- Timestamp表示請求的時間戳,通常使用標(biāo)準(zhǔn)的Unix時間戳格式(以秒為單位)。
在每個請求中包含Nonce和Timestamp:
- 將生成的Nonce和Timestamp作為參數(shù)添加到每個請求中,可以通過URL參數(shù)、請求頭或請求體的方式進行傳遞。
- 確保Nonce和Timestamp在每個請求中都是唯一且正確的。
服務(wù)器端驗證Nonce和Timestamp:
- 在服務(wù)器端接收到請求后,首先驗證Nonce和Timestamp的有效性。
- 檢查Nonce是否已經(jīng)被使用過,如果已經(jīng)被使用過,則可能是重放攻擊,拒絕該請求。
- 檢查Timestamp是否在合理的時間范圍內(nèi),如果超出預(yù)定的有效期,則認為請求無效。
存儲和管理Nonce:
- 為了驗證Nonce是否已經(jīng)被使用過,服務(wù)器需要存儲已經(jīng)使用過的Nonce。
- 可以使用數(shù)據(jù)庫、緩存或其他持久化存儲方式來管理Nonce的狀態(tài)。
- 需要定期清理過期的Nonce,以防止存儲占用過多的資源。
設(shè)置有效期:
- 為了限制請求的有效時間范圍,可以設(shè)置一個合理的有效期。
- 根據(jù)實際需求和業(yè)務(wù)場景,選擇適當(dāng)?shù)挠行?,例如幾分鐘或幾小時。
通過使用Nonce和Timestamp來防止重放攻擊,可以保護三方接口免受惡意重放請求的影響。以上是實現(xiàn)該功能的最佳實踐,但具體的實現(xiàn)方法可能因應(yīng)用程序和技術(shù)棧的不同而有所差異。確保在設(shè)計和實施安全措施時考慮到應(yīng)用程序的特定需求和風(fēng)險模型。
2. 添加過期時間
在請求中添加一個過期時間字段(例如,token的有效期),并在服務(wù)端驗證請求的時間戳是否在有效期內(nèi)。超過過期時間的請求應(yīng)被拒絕。
防篡改、防重放攻擊攔截器
每次HTTP請求,都需要加上timestamp參數(shù),然后把timestamp和其他參數(shù)一起進行數(shù)字簽名。HTTP請求從發(fā)出到達服務(wù)器一般都不會超過60s,所以服務(wù)器收到HTTP請求之后,首先判斷時間戳參數(shù)與當(dāng)前時間相比較,是否超過了60s,如果超過了則認為是非法的請求。
一般情況下,從抓包重放請求耗時遠遠超過了60s,所以此時請求中的timestamp參數(shù)已經(jīng)失效了,如果修改timestamp參數(shù)為當(dāng)前的時間戳,則signature參數(shù)對應(yīng)的數(shù)字簽名就會失效,因為不知道簽名秘鑰,沒有辦法生成新的數(shù)字簽名。
但這種方式的漏洞也是顯而易見的,如果在60s之后進行重放攻擊,那就沒辦法了,所以這種方式不能保證請求僅一次有效 nonce的作用
nonce的意思是僅一次有效的隨機字符串,要求每次請求時,該參數(shù)要保證不同。我們將每次請求的nonce參數(shù)存儲到一個“集合”中,每次處理HTTP請求時,首先判斷該請求的nonce參數(shù)是否在該“集合”中,如果存在則認為是非法請求。
nonce參數(shù)在首次請求時,已經(jīng)被存儲到了服務(wù)器上的“集合”中,再次發(fā)送請求會被識別并拒絕。
nonce參數(shù)作為數(shù)字簽名的一部分,是無法篡改的,因為不知道簽名秘鑰,沒有辦法生成新的數(shù)字簽名。
這種方式也有很大的問題,那就是存儲nonce參數(shù)的“集合”會越來越大。
nonce的一次性可以解決timestamp參數(shù)60s(防止重放攻擊)的問題,timestamp可以解決nonce參數(shù)“集合”越來越大的問題。
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 {
// 獲取時間戳
String timestamp = request.getHeader("timestamp");
// 獲取隨機字符串
String nonceStr = request.getHeader("nonceStr");
// 獲取簽名
String signature = request.getHeader("signature");
// 判斷時間是否大于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中(防止短時間內(nèi)的重放攻擊)
Boolean haveNonceStr = redisTemplate.hasKey(nonceStr);
if (StrUtil.isEmpty(nonceStr) || Objects.isNull(haveNonceStr) || haveNonceStr) {
throw new BusinessException("invalid nonceStr");
}
// 對請求頭參數(shù)進行簽名
if (StrUtil.isEmpty(signature) || !Objects.equals(signature, this.signature(timestamp, nonceStr, request))) {
throw new BusinessException("invalid signature");
}
// 將本次用戶請求的nonceStr參數(shù)存到redis中設(shè)置xx秒后自動刪除
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;
}
/**
* 按照字母順序進行升序排序
*
* @param params 請求參數(shù) 。注意請求參數(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();
}
}
注冊攔截器指定攔截接口
對敏感數(shù)據(jù)進行加密傳輸
使用TLS(傳輸層安全)協(xié)議可以保證通信過程中的數(shù)據(jù)加密和完整性。以下是一些基本步驟:
- 在服務(wù)器上配置TLS證書(包括公鑰和私鑰)。
- 客戶端和服務(wù)器之間建立TLS連接??蛻舳讼蚍?wù)器發(fā)送HTTPS請求。
- 在TLS握手期間,客戶端和服務(wù)器協(xié)商加密算法和密鑰交換方法。
- 握手成功后,客戶端和服務(wù)器之間的所有數(shù)據(jù)傳輸都會經(jīng)過加密處理。
具體的實現(xiàn)取決于所使用的編程語言和框架。以下是使用Java的示例代碼,演示如何使用TLS進行加密傳輸:
// 創(chuàng)建SSLContext對象
SSLContext sslContext = SSLContext.getInstance("TLS");
// 初始化SSLContext,加載證書和私鑰
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è)置其他請求參數(shù)、發(fā)送請求、處理響應(yīng)等
這段代碼中,我們創(chuàng)建了一個SSLContext對象并初始化它,加載了服務(wù)器的證書和私鑰。然后,通過HttpsURLConnection對象,設(shè)置了TLS的安全套接字工廠,并與指定的URL建立了HTTPS連接。
請注意,你需要將實際的證書和私鑰文件(通常是.jks格式)替換為真實的文件路徑,并提供正確的密碼。
以上代碼只是一個簡單的示例,實際部署時可能需要根據(jù)具體要求進行更多配置。確保在項目中遵循最佳實踐和安全建議,并與相應(yīng)的開發(fā)和運維團隊合作,以確保三方接口的安全性。
AK和SK生成方案
開發(fā)一個三方接口,并提供給客戶使用,可以考慮以下方法來生成AK(Access Key)和SK(Secret Key):
設(shè)計API密鑰管理系統(tǒng):
- 創(chuàng)建一個API密鑰管理系統(tǒng),用于生成和管理AK和SK。這個系統(tǒng)可以是一個獨立的服務(wù)器應(yīng)用或與你的主應(yīng)用集成在一起。
生成AK和SK:
- 在API密鑰管理系統(tǒng)中,為每個客戶生成唯一的AK和SK。
- AK通常是一個公開的標(biāo)識符,用于標(biāo)識客戶的身份??梢允褂秒S機字符串、UUID等方式生成。
- SK是一個保密的私鑰,用于生成身份驗證簽名和加密訪問令牌??梢允褂秒S機字符串、哈希函數(shù)等方式生成,并確保其足夠安全。
*存儲和管理AK和SK:
- 將生成的AK和SK存儲在數(shù)據(jù)庫或其他持久化存儲中,并與客戶的其他相關(guān)信息關(guān)聯(lián)起來。
- 需要實施適當(dāng)?shù)臋?quán)限控制和安全措施,以確保只有授權(quán)的用戶可以訪問和管理AK和SK。
- 可以考慮對SK進行加密處理,以增加安全性。
提供API密鑰分發(fā)機制:
- 客戶可以通過你提供的界面、API或者自助注冊流程來獲取他們的AK和SK。
- 在分發(fā)過程中,確保以安全的方式將AK和SK傳遞給客戶。例如,使用加密連接或其他安全通道進行傳輸。
安全性和最佳實踐:
- 強烈建議對API密鑰管理系統(tǒng)進行安全審計,并根據(jù)最佳實踐來保護和管理AK和SK。
- 定期輪換AK和SK,以增加安全性并降低潛在風(fēng)險。
- 在設(shè)計接口時,使用AK和SK進行身份驗證和權(quán)限控制,以防止未經(jīng)授權(quán)的訪問。
請注意,上述步驟提供了一般性的指導(dǎo),具體實現(xiàn)可能因你的應(yīng)用程序需求、技術(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
);
這個表包含以下字段:
- id:主鍵,自增的唯一標(biāo)識符。
- app_id:應(yīng)用程序ID或標(biāo)識符,用于關(guān)聯(lián)AKSK與特定應(yīng)用程序。
- access_key:訪問密鑰(AK),用于標(biāo)識客戶身份。
- secret_key:秘密密鑰(SK),用于生成簽名和進行身份驗證。
- valid_from:AKSK有效期起始時間。
- valid_to:AKSK有效期結(jié)束時間。
- enabled:是否啟用該AKSK,1表示啟用,0表示禁用。
- allowed_endpoints:逗號分隔的允許訪問的接口/端點列表。
- created_at:記錄創(chuàng)建時間。
在實際使用中,你可能需要根據(jù)具體需求對字段進行調(diào)整或添加索引以提高性能。此外,還可以考慮添加其他字段來滿足你的應(yīng)用程序的需求,例如描述、所屬用戶等。
請注意,具體的設(shè)計可能會因你的應(yīng)用程序需求和使用場景而有所不同。確保在實施前仔細考慮你的業(yè)務(wù)要求,并遵循良好的數(shù)據(jù)庫設(shè)計原則和最佳實踐。
API接口設(shè)計補充
圖片
1.使用POST作為接口請求方式
一般調(diào)用接口最常用的兩種方式就是GET和POST。兩者的區(qū)別也很明顯,GET請求會將參數(shù)暴露在瀏覽器URL中,而且對長度也有限制。為了更高的安全性,所有接口都采用POST方式請求。
2.客戶端IP白名單
ip白名單是指將接口的訪問權(quán)限對部分ip進行開放來避免其他ip進行訪問攻擊。
- 設(shè)置ip白名單缺點就是當(dāng)你的客戶端進行遷移后,就需要重新聯(lián)系服務(wù)提供者添加新的ip白名單。
- 設(shè)置ip白名單的方式很多,除了傳統(tǒng)的防火墻之外,spring cloud alibaba提供的組件sentinel也支持白名單設(shè)置。
- 為了降低api的復(fù)雜度,推薦使用防火墻規(guī)則進行白名單設(shè)置。
3. 單個接口針對ip限流
限流是為了更好的維護系統(tǒng)穩(wěn)定性。
使用redis進行接口調(diào)用次數(shù)統(tǒng)計,ip+接口地址作為key,訪問次數(shù)作為value,每次請求value+1,設(shè)置過期時長來限制接口的調(diào)用頻率。
4. 記錄接口請求日志
記錄請求日志,快速定位異常請求位置,排查問題原因。(如:用aop來全局處理接口請求)
5. 敏感數(shù)據(jù)脫敏
在接口調(diào)用過程中,可能會涉及到訂單號等敏感數(shù)據(jù),這類數(shù)據(jù)通常需要脫敏處理
最常用的方式就是加密。加密方式使用安全性比較高的RSA非對稱加密。非對稱加密算法有兩個密鑰,這兩個密鑰完全不同但又完全匹配。只有使用匹配的一對公鑰和私鑰,才能完成對明文的加密和解密過程。
6.冪等性問題
冪等性是指: 任意多次請求的執(zhí)行結(jié)果和一次請求的執(zhí)行結(jié)果所產(chǎn)生的影響相同。
- 說的直白一點就是查詢操作無論查詢多少次都不會影響數(shù)據(jù)本身,因此查詢操作本身就是冪等的。
- 但是新增操作,每執(zhí)行一次數(shù)據(jù)庫就會發(fā)生變化,所以它是非冪等的。
冪等問題的解決有很多思路,這里講一種比較嚴(yán)謹(jǐn)?shù)摹?/p>
- 提供一個生成隨機數(shù)的接口,隨機數(shù)全局唯一。調(diào)用接口的時候帶入隨機數(shù)。
- 第一次調(diào)用,業(yè)務(wù)處理成功后,將隨機數(shù)作為key,操作結(jié)果作為value,存入redis,同時設(shè)置過期時長。
- 第二次調(diào)用,查詢redis,如果key存在,則證明是重復(fù)提交,直接返回錯誤。
7.版本控制
一套成熟的API文檔,一旦發(fā)布是不允許隨意修改接口的。這時候如果想新增或者修改接口,就需要加入版本控制,版本號可以是整數(shù)類型,也可以是浮點數(shù)類型。
一般接口地址都會帶上版本號,http://ip:port//v1/list , http://ip:port//v2/list
8.響應(yīng)狀態(tài)碼規(guī)范
一個牛逼的API,還需要提供簡單明了的響應(yīng)值,根據(jù)狀態(tài)碼就可以大概知道問題所在。我們采用http的狀態(tài)碼進行數(shù)據(jù)封裝,例如200表示請求成功,4xx表示客戶端錯誤,5xx表示服務(wù)器內(nèi)部發(fā)生錯誤。
狀態(tài)碼設(shè)計參考如下:
public enum CodeEnum {// 根據(jù)業(yè)務(wù)需求進行添加
SUCCESS(200, "處理成功"),ERROR_PATH(404, "請求地址錯誤"),
ERROR_SERVER(505, "服務(wù)器內(nèi)部發(fā)生錯誤");
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ù)會包含三個屬性,狀態(tài)碼(code),信息描述(message),響應(yīng)數(shù)據(jù)(data)??蛻舳烁鶕?jù)狀態(tài)碼及信息描述可快速知道接口,如果狀態(tà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.接口文檔
一個好的API還少不了一個優(yōu)秀的接口文檔。接口文檔的可讀性非常重要,雖然很多程序員都不喜歡寫文檔,而且不喜歡別人不寫文檔。為了不增加程序員的壓力,推薦使用swagger2或其他接口管理工具,通過簡單配置,就可以在開發(fā)中測試接口的連通性,上線后也可以生成離線文檔用于管理API
11.生成簽名sign的詳細步驟
結(jié)合案例詳細說明怎么生成簽名signature(寫完上面的博客后,得出的感悟)
第1步: 將所有參數(shù)(注意是所有參數(shù),包括appId,timeStamp,nonce),除去sign本身,以及值是空的參數(shù),按key名升序排序存儲。
第2步: 然后把排序后的參數(shù)按 key1value1key2value2…keyXvalueX的方式拼接成一個字符串。
這里的參數(shù)和值必須是傳輸參數(shù)的原始值,不能是經(jīng)過處理的,如不能將"轉(zhuǎn)成”后再拼接)
第3步: 把分配給調(diào)用方的密鑰secret拼接在第2步得到的字符串最后面。
即: key1value1key2value2…keyXvalueX + secret
第4步: 計算第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
請求頭是
appId:zs001timeStamp:1612691221000sign:2B42AAED20E4B2D5BA389F7C344FE91Bnonce:1234567890
實際情況最好是通過post方式發(fā)送,其中sign參數(shù)對應(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
第四步:然后將這個字符串進行md5計算
假設(shè)得到的是abcdef,然后轉(zhuǎn)為大寫,得到ABCDEF這個值作為簽名sign
注意,計算md5之前調(diào)用方需確保簽名加密字符串編碼與提供方一致,如統(tǒng)一使用utf-8編碼或者GBK編碼,如果編碼方式不一致則計算出來的簽名會校驗失敗。
上面說的請求錄音可拼可不拼接,主要還是為了增強簽名的復(fù)雜性
12.1.什么是token?
Token是什么?
token即 訪問令牌access token,用于接口中標(biāo)識接口調(diào)用者的身份、憑證,減少用戶名和密碼的傳輸次數(shù)。 一般情況下客戶端(接口調(diào)用方)需要先向服務(wù)器端申請一個接口調(diào)用的賬號,服務(wù)器會給出一個appId和一個appSecret(appSecret用于參數(shù)簽名使用)
注意appSecret保存到客戶端,需要做一些安全處理,防止泄露。
Token的值一般是UUID,服務(wù)端生成Token后需要將token做為key,將一些和token關(guān)聯(lián)的信息作為value保存到緩存服務(wù)器中(redis),當(dāng)一個請求過來后,服務(wù)器就去緩存服務(wù)器中查詢這個Token是否存在,存在則調(diào)用接口,不存在返回接口錯誤,一般通過攔截器或者過濾器來實現(xiàn)。
Token分為兩種
- API Token(接口令牌): 用于訪問不需要用戶登錄的接口,如登錄、注冊、一些基本數(shù)據(jù)的獲取等。獲取接口令牌需要拿appId、timestamp和sign來換,sign=加密(參數(shù)1+…+參數(shù)n+timestamp+key)
- USER Token(用戶令牌): 用于訪問需要用戶登錄之后的接口,如:獲取我的基本信息、保存、修改、刪除等操作。獲取用戶令牌需要拿用戶名和密碼來換
12.2.Token+簽名(有用戶狀態(tài)的接口簽名)
上面講的接口簽名方式都是無狀態(tài)的,在APP開放API接口的設(shè)計中,由于大多數(shù)接口涉及到用戶的個人信息以及產(chǎn)品的敏感數(shù)據(jù),所以要對這些接口進行身份驗證,為了安全起見讓用戶暴露的明文密碼次數(shù)越少越好,然而客戶端與服務(wù)器的交互在請求之間是無狀態(tài)的,也就是說,當(dāng)涉及到用戶狀態(tài)時,每次請求都要帶上身份驗證信息(令牌token)。
1.Token身份驗證
- 用戶登錄向服務(wù)器提供認證信息(如賬號和密碼),服務(wù)器驗證成功后返回Token給客戶端;
- 客戶端將Token緩存在本地,后續(xù)每次發(fā)起請求時,都要攜帶此Token;
- 服務(wù)端檢查Token的有效性,有效則放行,無效(Token錯誤或過期)則拒絕。
弊端:Token被劫持,偽造請求和篡改參數(shù)。
2.Token+簽名驗證
與上面接口簽名規(guī)則一樣,為客戶端分配appSecret(密鑰,用于接口加密,不參與傳輸),將appSecret和所有請求參數(shù)組合成一個字符串,根據(jù)簽名算法生成簽名值,發(fā)送請求時將簽名值一起發(fā)送給服務(wù)器驗證。
這樣,即使Token被劫持,對方不知道appSecret和簽名算法,就無法偽造請求和篡改參數(shù),并且有了token后也能正確的獲取到用戶的狀態(tài)
登陸和退出請求:
圖片
后續(xù)請求:
客戶端: 與上面接口簽名規(guī)則一樣類似,把appId改為token即可。