接口安全:防篡改和防重放
API 作為應用程序之間的橋樑,承載了大量的數據交換任務。然而,那些暴露在互聯網中的接口也可能成為攻擊者的目標。為了確保數據傳輸的安全性,我們必須採取有效的安全措施來防範篡改攻擊和重放攻擊。下面我們將以 Spring Boot 應用中的 API 安全設計為例,講解下如何實施這些安全措施。
什麼是篡改攻擊和重放攻擊
篡改攻擊
篡改攻擊利用了數據在傳輸過程中的不安全性。攻擊者可以通過中間人攻擊(Man-in-the-Middle, MITM)等方式攔截數據包,然後修改其中的關鍵字段,比如金額、用户身份信息等,再將修改後的數據包發送給接收方,以達到惡意目的。這種攻擊可以發生在客户端到服務器之間的任何點,尤其是在沒有加密或加密強度不足的情況下。
比如:攻擊者截獲用户認證請求,可以修改其認證信息,冒充合法用户登錄系統。
重放攻擊
重放攻擊是指攻擊者攔截並重新發送之前有效的數據包,以達到重複執行某項操作的目的。這種攻擊利用了系統的狀態管理和時間敏感性不足的問題。重放攻擊依賴於數據包的有效性和可重複性。攻擊者截獲一個有效的數據包(如登錄請求),然後在適當的時候再次發送這個數據包,從而繞過系統的身份驗證或其他控制機制。
比如:攻擊者截獲一次成功的轉賬請求,然後多次重放該請求,導致用户賬户被多次扣款。
防禦大法
-
HTTPS(本文不做詳細贅述)HTTPS通過SSL/TLS協議對數據進行加密,確保數據在傳輸過程中不被竊聽或篡改。此外,HTTPS還提供了身份驗證機制,確保通信雙方的身份。
-
基於數字簽名防篡改
- 數字簽名基於密鑰的加密技術。
- 發送方使用使用約定好的密鑰對傳輸參數進行簽名,生成一個簽名值,並將簽名值放入請求
header中; - 接收方使用約定的密鑰對請求參數再次進行簽名;
- 接收方對兩次簽名的值進行對比,對比一致則認為請求合法,不一致則説明請求被篡改。
-
基於時間戳防重放
- 時間戳是一種確保數據新鮮度的方法。每個請求中加入時間戳,並在服務器端檢查時間戳的新鮮度。時間戳確保請求在一定時間內是有效的。服務器收到請求後,會檢查時間戳是否在允許的時間範圍內。如果時間戳過期,則拒絕請求。
- 發送方每次請求都在請求
header中放入時間戳參數,並且時間戳要和傳輸參數一起進行數字簽名; - 接收方收到請求後,首先取請求
header中的時間戳並與當前時間進行比較,如果時間差超過了預設的閾值,則認為簽名過期。
簽名驗籤編碼思路
進行簽名驗籤的 Header 信息:
| 參數 | 類型 | 説明 |
|---|---|---|
X-Signature |
header |
請求的簽名值 |
X-Timestamp |
header |
請求時的時間戳 |
X-Algorithm |
header |
簽名時使用的算法 |
簽名驗籤的數據部分:
- 時間戳:
header中X-Timestamp的值; Query參數:Query請求參數,例如request?username=aaa&age=18,多個Query參數需要對key按字典(ASCII碼)升序排序後,再按照value1+value2方法拼接;Body數據:實際的請求體內容。
過濾器編碼如下:
@Slf4j
public class SignatureCheckFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
CacheRequestBodyWrapper requestWrapper = new CacheRequestBodyWrapper(request);
// 獲取請求頭中的簽名和時間戳
String signature = requestWrapper.getHeader("X-Signature");
String timestampStr = requestWrapper.getHeader("X-Timestamp");
String algorithm = requestWrapper.getHeader("X-Algorithm");
if (Objects.isNull(signature) || Objects.isNull(timestampStr)) {
log.warn("Missing required headers");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required headers");
return;
}
// 重放時間限制
long timestamp = Long.parseLong(timestampStr);
if (System.currentTimeMillis() - timestamp >= 60 * 1000) { // 這裏寫死60s,實際可做配置
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Signature has been expired.");
return;
}
// 獲取 query 請求字符串
String requestQuery = getRequestQueryStr(requestWrapper);
// 獲取 body 數據
String body = getRequestBody(requestWrapper);
// 按照規則進行簽名
String signData = timestamp + requestQuery + body;
String newSignature = DigestUtils.getSignature(signData, algorithm, "UTF-8");
log.info("計算出的新的簽名值:----------->>>>>> {}", newSignature);
log.info("header裏面的簽名值:---------->>>>>> {}", signature);
if (!newSignature.equals(signature)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Signature verification failed");
return;
}
filterChain.doFilter(requestWrapper, response); // 注意這裏傳遞的是 cachedRequest
}
/**
* 獲取請求 body
*
* @param request HttpServletRequest
* @return 請求 body 字符串
*/
private String getRequestBody(CacheRequestBodyWrapper request) throws IOException, UnsupportedEncodingException {
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
/**
* 獲取 query 請求字符串
*
* @param request HttpServletRequest
* @return 請求字符串
*/
private String getRequestQueryStr(HttpServletRequest request) {
List<String> reqList = new ArrayList<>();
Enumeration<String> reqEnu = request.getParameterNames();
while (reqEnu.hasMoreElements()) {
reqList.add(reqEnu.nextElement());
}
Collections.sort(reqList);
StringBuilder requestQuery = new StringBuilder();
for (String key : reqList) {
String value = request.getParameter(key);
if (value != null) {
requestQuery.append(value);
}
}
log.info("獲取的query請求字符串是:------>>> {}", requestQuery);
return requestQuery.toString();
}
}
註冊過濾器:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<SignatureCheckFilter> signatureCheckFilter(){
FilterRegistrationBean<SignatureCheckFilter> registrationBean = new FilterRegistrationBean<>();
SignatureCheckFilter filter = new SignatureCheckFilter();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/xxx/xxxxxx/*"); // 指定需要處理的url
registrationBean.setOrder(1);
return registrationBean;
}
實現防篡改和防重放攻擊的方式有很多種,本文只是簡單介紹了基於數字簽名和時間戳的實現方案,並提供了一個編碼思路。希望通過這些內容,大家可以有所啓發,並能夠在實際項目中靈活運用這些安全措施。
【Java驛站】持續給大家更新,掃描下方👇👇👇二維碼,關注【Java驛站】公眾號