首頁 > 軟體

Java實現API sign簽名校驗的方法詳解

2022-07-12 14:02:10

1. 前言

目的:為防止中間人攻擊。

場景:

  • 專案內部前後端呼叫,這種場景只需要做普通引數的簽名校驗和過期請求校驗,目的是為了防止攻擊者劫持請求 url 後非法請求介面。
  • 開放平臺向第三方應用提供能力,這種場景除了普通引數校驗和請求過期校驗外,還要考慮 3d 應用的授權機制,不被授權的應用就算傳入了合法的引數也不能被允許請求成功。

2. 簽名生成策略

接下來詳述場景 2,其實場景 1 也包含在場景 2 內部。

1.舉例請求 url:

http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10

請求引數為:

引數名位置備註舉例
X-Access-Keyheader使用者端授權碼,伺服器端提供,和 accessSecret 配對(場景 1 無此引數)app1
X-Access-Tokenheader當前登入使用者 tokend7b5808c3f443eb5a496225468c7e4a5
X-UTCTimeheader當前傳送請求時的時間2022-02-16T09:12:43.083Z
X-Randomheader請求亂數341be97d9aff90c9978347f66f945b77
orderTypequery訂單型別1001
requestFromquery訂單來源IOS
pageNumquery分頁引數10
pageSizequery分頁引數2

2.設原始引數為 stringA,stringA 中新增 X-Access-Key、X-UTCTime、X-Random 固定引數,將 stringA 內非空引數值和 header 的引數按照引數名 ASCII 碼從小到大排序(字典序),使用 URL 鍵值對的格式(即 key1=value1&key2=value2…)拼接成字串 stringB。

注意如下規則:

  • 引數名 ASCII 碼從小到大排序(字典序);
  • 如果引數的值為空不參與簽名;
  • 引數名區分大小寫;
  • 驗證呼叫返回或主動通知簽名時,傳送的 sign 引數不參與簽名,將生成的簽名與該 sign 值作校驗。
// 最終拼接為stringB:
orderType=1001X-UTCTime=2022-02-16T09:12:43.083ZpageSize=10X-Access-Key=app1X-Access-Token=d7b5808c3f443eb5a496225468c7e4a5pageNum=2requestFrom=IOSX-Random=341be97d9aff90c9978347f66f945b77

3.在 stringB 最後拼接上 accessSecret (金鑰) 得到 stringC 字串,並對 stringC 進行 MD5 運算,得到 sign 值。

// 最後拼上accessSecret得到stringC:
orderType=1001X-UTCTime=2022-02-16T09:12:43.083ZpageSize=10X-Access-Key=app1X-Access-Token=d7b5808c3f443eb5a496225468c7e4a5pageNum=2requestFrom=IOSX-Random=341be97d9aff90c9978347f66f945b77&accessSecret=192006250b4c09247ec02edce69f6a2d
// md5加密得到最終簽名結果sign:
sign=e1a4907ef03adee3fa8d395552814f4e

4.將原始的請求 url 拼接上 sign 形成最終的請求 url。

http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10&sign=0f5a3cc534961d129a25d52d7ed8d003

5.最終請求 url 如下:

http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10&sign=0f5a3cc534961d129a25d52d7ed8d003

引數名位置備註舉例
X-Access-Keyheader使用者端授權碼,伺服器端提供,和 accessSecret 配對(場景 1 無此引數)app1
X-Access-Tokenheader當前登入使用者 tokend7b5808c3f443eb5a496225468c7e4a5
X-UTCTimeheader當前傳送請求時的時間2022-02-16T09:12:43.083Z
X-Randomheader請求亂數341be97d9aff90c9978347f66f945b77
orderTypequery訂單型別1001
requestFromquery訂單來源IOS
pageNumquery分頁引數10
pageSizequery分頁引數2

6.伺服器端 gateway 同樣做 sign 簽名加密和校驗,如果校驗不通過則說明請求非法,直接拒絕,通過則下發到業務服務進行正常請求處理。

3. API 簽名演演算法 Java 實現

public class SignUtil {

    /**
     * 生成簽名
     *
     * @param accessSecret accessSecret
     * @param url          url
     * @param headers      headers
     * @param body         post的body體
     * @param <T>          body體泛型
     * @return sign
     */
    public static <T> String sign(String accessSecret, String url, Map<String, Object> headers, T body) throws IllegalAccessException {
        Map<String, Object> signMap = new HashMap<>();
        if (headers != null) {
            signMap.putAll(headers);
        }
        Map<String, Object> paramMap = getUrlParams(url);
        if (paramMap != null) {
            signMap.putAll(paramMap);
        }
        Map<String, Object> bodyMap = getBodyParams(body);
        if (bodyMap != null) {
            signMap.putAll(bodyMap);
        }

        StringBuffer sb = new StringBuffer();
        signMap.forEach((k, v) -> {
            sb.append(k).append("=").append(v).append("&");
        });
        sb.append("accessSecret=").append(accessSecret);
        return stringToMD5(sb.toString());
    }

    private static Map<String, Object> getUrlParams(String url) {
        if (StringUtils.isBlank(url) || !url.contains("?")) {
            return null;
        }
        Map<String, Object> paramMap = new HashMap<>();
        String params = url.split("\?")[1];
        for (String param : params.split("&")) {
            String[] p = param.split("=");
            paramMap.put(p[0], p[1]);
        }
        return paramMap;
    }

    private static <T> Map<String, Object> getBodyParams(T body) throws IllegalAccessException {
        if (body == null) {
            return null;
        }
        Map<String, Object> bodyMap = new HashMap<>();
        for (Field field : body.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            bodyMap.put(field.getName(), field.get(body));
        }
        return bodyMap;
    }

    private static String stringToMD5(String plainText) {
        byte[] secretBytes = null;
        try {
            secretBytes = MessageDigest.getInstance("md5").digest(
                    plainText.getBytes());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("沒有這個md5演演算法!");
        }

        return new BigInteger(1, secretBytes).toString(16);
    }
}

4. 測試一下

public class App {
    public static void main(String[] args) throws IllegalAccessException {
        String url = "http://api.abc.com/a-service/orders?orderType=1001&requestFrom=IOS&pageNum=2&pageSize=10";
        Map<String, Object> headerMap = new HashMap<>();
        headerMap.put("X-Access-Key", "app1");
        headerMap.put("X-Access-Token", "d7b5808c3f443eb5a496225468c7e4a5");
        headerMap.put("X-UTCTime", generateDate());
        headerMap.put("X-Random", "341be97d9aff90c9978347f66f945b77");
        BodyVO body = new BodyVO(100000001L, "yangcan", new Date());

        String sign = SignUtil.sign("sdfsdfdsfdsfds", url, headerMap, body);
        System.out.println(sign);
    }

    /**
     * 獲取當前時間的UTC格式
     */
    private static String generateDate() {
        Date now = new Date();
        DateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.US);
        return format.format(now);
    }

    @Data
    @AllArgsConstructor
    public static class BodyVO {
        private Long ycId;
        private String ycName;
        private Date ycTime;
    }
}

輸出:

sign = 4f52eb34b30129a8d511dc803044086b

到此這篇關於Java實現API sign簽名校驗的方法詳解的文章就介紹到這了,更多相關Java API簽名校驗內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com