接口签名生成方式
前言
当某个系统对外部系统提供接口访问时,为提高接口请求安全性,往往会在接口访问时添加签名,当外部系统访问本系统签名验证成功时才能正常返回数据,一般接口提供方会与外部系统提前约定好,不同外部系统用 appKey 加以区分,并且不同 appKey 对应不同秘钥(secretKey)
签名生成方式
以下以 Get 请求为例:
- 第一步:在请求参数中添加 appKey 和时间戳 timestamp,将所有请求参数(除了 sign )按照字母排序
- 第二步:使用&将第一步参数拼接成如下形式( k1=v1&k2=v2&k3=v3 … ),并且将秘钥(secretKey)拼接在最后,最终字符串为 k1=v1&k2=v2&k3=v3secretKey
- 第三步:将第二步中的字符串使用MD5加密生成签名(sign )
- 第四步:将签名(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}}×tamp={{timestamp}}
其中 appKey 参数非固定传值,此处假设接口提供方与 sign-consumer 约定其 appKey 为 A ,secretKey(秘钥) 为 123456,sign 由接口调用方 sign-consumer 根据入参和秘钥拼接并通过MD5加密生成,具体规则看 签名生成方式
具体代码
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) {//获取客户端 appKeyString 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}}×tamp={{timestamp}}
其中 appKey、sign、timestamp 等参数均可由系统内部提供,所以 sign-consumer 对外提供接口为:
http://127.0.0.1:9092/consumer/hello?query=2&offset=0&limit=10
具体代码
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";@Autowiredprivate 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 控制台