java实现API sign签名校验

java实现API sign签名校验

1. 前言

目的:为防止中间人攻击。

场景:

  1. 项目内部前后端调用,这种场景只需要做普通参数的签名校验和过期请求校验,目的是为了防止攻击者劫持请求url后非法请求接口。

  2. 开放平台向第三方应用提供能力,这种场景除了普通参数校验和请求过期校验外,还要考虑3d应用的授权机制,不被授权的应用就算传入了合法的参数也不能被允许请求成功。

2. 签名生成策略

接下来详述场景2,其实场景1也包含在场景2内部。

  1. 举例请求url:

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

    请求参数为:

    参数名 位置 备注 举例
    X-Access-Key header 客户端授权码,服务端提供,和accessSecret配对(场景1无此参数) app1
    X-Access-Token header 当前登录用户token d7b5808c3f443eb5a496225468c7e4a5
    X-UTCTime header 当前发送请求时的时间 2022-02-16T09:12:43.083Z
    X-Random header 请求随机数 341be97d9aff90c9978347f66f945b77
    orderType query 订单类型 1001
    requestFrom query 订单来源 IOS
    pageNum query 分页参数 10
    pageSize query 分页参数 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-Key header 客户端授权码,服务端提供,和accessSecret配对(场景1无此参数) app1
    X-Access-Token header 当前登录用户token d7b5808c3f443eb5a496225468c7e4a5
    X-UTCTime header 当前发送请求时的时间 2022-02-16T09:12:43.083Z
    X-Random header 请求随机数 341be97d9aff90c9978347f66f945b77
    orderType query 订单类型 1001
    requestFrom query 订单来源 IOS
    pageNum query 分页参数 10
    pageSize query 分页参数 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章