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