记一次后台接口之间的签名调用

1,056 阅读2分钟

前言

主平台为对接放分配 appid和 secret,用于请求接口使用。 主平台接口采用HTTPS+JSON方式请求,输入和输出参数都采用UTF-8编码,并对所有接口均对调用方进行身份鉴权,以确认调用方合法性。

公共参数说明

签名

将appid、timestamp、nonce、secret(由主平台为第三方应用分配的密匙)的值进行字典排序,然后在用”&”符号进行连接四个值,再用UTF-8编码转为字节数组之后进行MD5加密,再转为大写的16进制字符串。请求携带参数appid和sign,只有拥有合法的身份appid和正确的签名sign才能放行。用以解决身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到secret(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。

签名类

public class SendSignUtil {

    /**
     * @author leichunhong
     * @description:组装签名
     * @date 2019/2/18 10:43
     * @Param
     * @Return
     */
    public static String createFrsUrl(String appId, String secret) {
        String nonce = UUIDUtil.getUUID();
        String timestamp = String.valueOf(System.currentTimeMillis());
        List<String> list = new ArrayList<>();
        list.add(appId);
        list.add(timestamp);
        list.add(secret);
        list.add(nonce);
        String sign = encrypt(addSymbol(list));
        String suffix = "timestamp=" + timestamp + "&nonce=" + nonce + "&sign=" + sign + "&appId=" + appId;
        return suffix;
    }

    /**
    * @auther  leichunhong
    * @desc:
    * @date  2020-06-03 15:01  
    * @param
    * @return  java.lang.String
    */
    public static String addSymbol(List<String> list) {
        StringBuilder content = new StringBuilder();
        Collections.sort(list);
        for (String element : list) {
            content.append(element).append("&");
        }
        content.deleteCharAt(content.length() - 1);
        return content.toString();
    }

    /**
     * @param
     * @return java.lang.String
     * @auther leichunhong
     * @desc md5转码
     * @date 2020-05-25 16:32
     */
    public static String encrypt(String str) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            e.printStackTrace();
        }
        byte[] md5Bytes = md5.digest(str.getBytes());
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString().toUpperCase();
    }

    public static void main(String[] args) {
        //在地址栏后拼接上参数
        //原始访问地址:http://127.0.0.1:9080/860475033169189
        //带签名访问:http://127.0.0.1:9080/860475033169189?timestamp=xxx&nonce=xxx&sign=xxx&appId=xxx
        System.out.println("原始访问地址:http://127.0.0.1:9080/860475033169189");
        System.out.println("带签名访问:http://127.0.0.1:9080/860475033169189?" + createFrsUrl("c3669f6829464aa2b51a7bc3e4ecec10", "fe4506a359024ea2b5e583a25586b84b"));
    }
}

验签类

@Component
public class CheckHttpUtil {

    @Autowired
    private RedisService redisService;
    
    final String NONCESTR_KEY = "nonce_key";

    /***
    * @Description: 验签
    * @Param: [appId, sign, nonce, timestamp]
    * @return: cn.thinkjoy.springboot.business.response.HttpResponse
    * @Author: leichunhong
    * @Date: 2021-03-02
    */
    public HttpResponse check(String appId, String sign, String nonce, String timestamp) {
        //此参数不通过网络传输 读者可以数据库保存或者 加入配置文件
        String secret = "fe4506a359024ea2b5e583a25586b84b";
        //判断时间
        long now = System.currentTimeMillis();
        long pathTimestamp = Long.parseLong(timestamp);
        //十分钟之外超时 抛异常
        long s = (now - pathTimestamp) / (1000 * 60);
        if (s > 10 || s < 0) {
            throw new PhoneBizException(201, "调用接口超时!");
        }
        //十分钟之内验证nonce重复
        //判断nonce是否用过
        Object obj = redisService.get(NONCESTR_KEY, nonce);
        if (obj != null) {
            throw new PhoneBizException(202, "此接口被调用过!已经被丢弃");
        }
        return testSign(secret, appId, sign, nonce, timestamp);
    }


    /**
     * @author leichunhong
     * @description
     * @date 2019/2/18 10:43
     * @Param
     * @Return
     */
    public HttpResponse testSign(String secret, String appId, String sign, String nonce, String timestamp) {

        List<String> list = new ArrayList<>();
        list.add(appId);
        list.add(timestamp);
        list.add(secret);
        list.add(nonce);
        String nowsign = SendSignUtil.encrypt(SendSignUtil.addSymbol(list));
        if (nowsign.equals(sign)) {
            //redis 存 nonce 值 保存10分钟
            redisService.set(NONCESTR_KEY, nonce, nonce, 10 * 60L);
            return ResultUtil.getHttpResponse(200, "验签验证成功!", null);
        } else {
            throw new PhoneBizException(203, "验签失败!");
        }
    }

}

说明:

1.接口调用如果和当前时间比较是十分钟之外的时间,直接抛出异常调用超时。

2.接口调用如果是十分钟之内验证nonce是否已经使用过,如果使用过证明有相同的接口被调用过,接口抛出异常,此接口被调用过!已经被丢弃。

3.上述两步验证成功之后,用传入的参数生成sign和接口中传过来的sign对比,如果相等验签成功,如果不相等验签失败。

4.ResultUtil为返回封装类,PhoneBizException为异常类,根据情况删除自行封装。