对外接口签名生成方式

接口签名生成方式

前言

当某个系统对外部系统提供接口访问时,为提高接口请求安全性,往往会在接口访问时添加签名,当外部系统访问本系统签名验证成功时才能正常返回数据,一般接口提供方会与外部系统提前约定好,不同外部系统用 appKey 加以区分,并且不同 appKey 对应不同秘钥(secretKey)

签名生成方式

以下以 Get 请求为例:

  1. 第一步:在请求参数中添加 appKey 和时间戳 timestamp,将所有请求参数(除了 sign )按照字母排序
  2. 第二步:使用&将第一步参数拼接成如下形式( k1=v1&k2=v2&k3=v3 ... ),并且将秘钥(secretKey)拼接在最后,最终字符串为 k1=v1&k2=v2&k3=v3secretKey
  3. 第三步:将第二步中的字符串使用MD5加密生成签名(sign )
  4. 第四步:将签名(sign )作为入参传入

以Java代码为例

定义两个项目,sign-provider 为接口提供方,sign-consumer 调用 sign-provider 提供的接口

sign-provider

接口提供方,提供接口为:
http://127.0.0.1:9091/provider/hello?query=2&offset=0&limit=10&appKey=A&sign={{sign}}&timestamp={{timestamp}}

其中 appKey 参数非固定传值,此处假设接口提供方与 sign-consumer 约定其 appKey 为 A ,secretKey(秘钥) 为 123456,sign 由接口调用方 sign-consumer 根据入参和秘钥拼接并通过MD5加密生成,具体规则看 签名生成方式

具体代码

Java 复制代码
package com.example.signprovider;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.TreeMap;

@Slf4j
@RestController
@RequestMapping("/provider")
public class HelloController {

    private static final long EXPIRE_TIME = 5;

    /**
     * 不同系统对应不同 appKey 和 secretKey
     */
    private static final Map<String, String> APP_KEY_MAP = new HashMap<>();

    static {
        APP_KEY_MAP.put("A", "123456");
    }

    @GetMapping("/hello")
    public String hello(RequestBean requestBean) {
        //获取客户端 appKey
        String appKey = requestBean.getAppKey();
        Assert.isTrue(APP_KEY_MAP.containsKey(appKey), "无效appKey!");
        //客户端传入的签名
        String requestSign = requestBean.getSign();
        //检查有无传入签名
        Assert.hasText(requestSign, "无效签名!");
        long requestTime = requestBean.getTimestamp();
        //如果请求发起时间与当前时间超过expireTime,则接口请求过期
        Assert.isTrue(System.currentTimeMillis() / 1000 - requestTime <= EXPIRE_TIME, "请求过期!");
        //生成签名
        String sign = "";
        try {
            sign = getSign(requestBean, APP_KEY_MAP.get(appKey));
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException("获取签名失败!");
        }
        //比对签名与传入签名是否一致
        Assert.isTrue(requestSign.equals(sign), "无效签名!");
        return "接口调用成功:" + requestBean;
    }

    private String getSign(RequestBean requestBean, String secretKey) throws IllegalAccessException {
        Map<String, Object> map = new TreeMap<>(String::compareTo);
        Field[] fields = requestBean.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (!"sign".equals(field.getName())) {
                field.setAccessible(true);
                map.put(field.getName(), field.get(requestBean));
            }
        }
        StringJoiner stringJoiner = new StringJoiner("&");
        map.forEach((k, v) -> stringJoiner.add(k + "=" + v));
        log.debug("stringJoiner:" + stringJoiner);
        String paramStr = stringJoiner + secretKey;
        //MD5加密
        return DigestUtils.md5DigestAsHex(paramStr.getBytes(StandardCharsets.UTF_8));
    }

}

如上:使用的MD5加密方法为 spring 提供的工具类 org.springframework.util.DigestUtils

sign-consumer

sign-provider 对外提供 API 为:
http://127.0.0.1:9091/provider/hello?query=2&offset=0&limit=10&appKey=A&sign={{sign}}&timestamp={{timestamp}}

其中 appKey、sign、timestamp 等参数均可由系统内部提供,所以 sign-consumer 对外提供接口为:
http://127.0.0.1:9092/consumer/hello?query=2&offset=0&limit=10

具体代码

Java 复制代码
package com.example.signconsumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.StringJoiner;
import java.util.TreeMap;

@Slf4j
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    private static final String APP_KEY = "A";
    private static final String SECRET_KEY = "123456";

    private static final String URL = "http://127.0.0.1:9091/provider/hello";

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/hello")
    public String hello(RequestBean requestBean) {
        //使用 TreeMap 可对key排序
        Map<String, Object> params = new TreeMap<>();
        params.put("appKey", APP_KEY);
        params.put("timestamp", System.currentTimeMillis() / 1000);
        params.put("limit", requestBean.getLimit());
        params.put("offset", requestBean.getOffset());
        params.put("query", requestBean.getQuery());
        //生成签名
        String sign = getSign(params);
        log.debug("sign:{}", sign);
        params.put("sign", sign);
        StringJoiner stringJoiner = new StringJoiner("&");
        params.forEach((k, v) -> stringJoiner.add(k + "=" + v));
        ResponseEntity<String> result = restTemplate.getForEntity(URL + "?" + stringJoiner, String.class);
        return result.getBody();
    }

    private String getSign(Map<String, Object> params) {
        StringJoiner stringJoiner = new StringJoiner("&");
        params.forEach((k, v) -> stringJoiner.add(k + "=" + v));
        log.debug("stringJoiner:" + stringJoiner);
        String paramStr = stringJoiner.toString() + SECRET_KEY;
        return DigestUtils.md5DigestAsHex(paramStr.getBytes(StandardCharsets.UTF_8));
    }
}

测试

浏览器调用 sign-consumer 提供的接口:

代码路径

https://github.com/husgithub/sign-test

git 地址:

复制代码
git@github.com:husgithub/sign-test.git

通过 PostMan 测试

通过调用 sign-provider 提供的接口可以测试 sign-consumer 提供的功能是否正确,PostMan 提供编写脚本的能力,在 JS 脚本中我们可以生成 timestamp 、sign 参数的值

打开 PostMan 后定位到 Pre-request Script 栏,可在此写 JS 脚本:

脚本如下:

复制代码
console.log("start......");
var timestamp = Math.floor(new Date().getTime()/1000);
pm.globals.set("timestamp", timestamp);
console.log("----");
console.log(request.url);
//console.log(pm.request.url.query.get("timestamp"));
var paramStr = request.url.split("?")[1];
console.log("url参数字符串为:"+paramStr);
console.log("分割字符串参数......");
var map = new Map();
var paramArr = paramStr.split("&");
for(var i=0;i<paramArr.length;i++){
    var p = paramArr[i].split("=");
    if("sign"!=(p[0])){
        if("timestamp"==p[0]){
            map.set(p[0],timestamp);
        }else{
            map.set(p[0],p[1]);
        }
    }
}
console.log(paramArr);
console.log(map.size);

//对map排序
var arrayObj = Array.from(map);
arrayObj.sort(function (a, b) {
    return a[0].localeCompare(b[0])
});
var sortParamStr = "";
for (var [key, value] of arrayObj) {
    console.log(key + ' = ' + value);
    sortParamStr += "&"+key+"="+value;
}
console.log(sortParamStr.substring(1));
//添加秘钥
var signStr = sortParamStr.substring(1)+"123456";
//生成签名
var sign = CryptoJS.MD5(signStr).toString();
console.log(sign);
pm.globals.set("sign", sign);

如下图:

通过 {{sign}} 的方式可以定义变量,之后可以通过 js 脚本对变量进行赋值

通过 request.url 可以获取请求 URL:

复制代码
request.url

pm.globals.set("sign", sign); 表示对 {{sign}} 括号内的参数赋值:

复制代码
pm.globals.set("sign", sign);

测试

通过 View -> Show Postman Console 可以打开 PostMan Console 控制台

相关推荐
daidaidaiyu14 小时前
Spring IOC 源码学习 事务相关的 BeanDefinition 解析过程 (XML)
java·spring
wal131452014 小时前
Dify发布V1.13.1版本,Hologres 向量数据库支持、HITL 邮件 Markdown 渲染及多项安全加固
数据库·安全·dify
鬼蛟15 小时前
Spring————事务
android·java·spring
陈天伟教授15 小时前
人工智能应用- 预测新冠病毒传染性:04. 中国:强力措施遏制疫情
前端·人工智能·安全·xss·csrf
西门吹-禅15 小时前
【sap fiori cds up error】
java·服务器·sap cap cds
敲代码的嘎仔16 小时前
Java后端面试——SSM框架面试题
java·面试·职场和发展·mybatis·ssm·springboot·八股
NGC_661116 小时前
Spring与SpringBoot
spring
大傻^16 小时前
Spring AI Alibaba RAG实战:基于向量存储的检索增强生成
java·人工智能·spring
大傻^16 小时前
Spring AI Alibaba 快速入门:基于通义千问的AI应用开发环境搭建
java·人工智能·后端·spring·springai·springaialibaba
jxkejiiii16 小时前
巧用手机原生功能,零成本给重要文档加密防护
安全·智能手机