博客 / 詳情

返回

接口安全:防篡改和防重放

接口安全:防篡改和防重放

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 簽名時使用的算法

簽名驗籤的數據部分:

  1. 時間戳:headerX-Timestamp 的值;
  2. Query 參數:Query 請求參數,例如 request?username=aaa&age=18,多個 Query 參數需要對 key 按字典(ASCII 碼)升序排序後,再按照 value1+value2 方法拼接;
  3. 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驛站】公眾號

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.