主题
商户签名验证拦截器
WARNING
又称签名/验签过滤器
,对部分请求需要通过签名方式来验证请求安全性。支持application/x-www-form-urlencoded
、application/json
和url地址
传参
使用场景
- 请求数据防篡改
- 重复提交
配置
参数说明
需要配置商户的信息,
merchants
中可以配置多个。
enabled
:是否启用拦截器,默认不启用false
items
:规则列表,可以配置多个app-id
:应用idapp-secret
:应用密钥(建议32位随机字符串,可以使用uuid
)name
:商户的名称time-verify
:是否启用时间验证,默认false
,例如:现在重复请求一小时之前的数据,提示请求过期time-interval
:时间验证区间,默认30分钟
,可以配置秒,分钟,小时,天,例如:PT30M
30分钟,PT2H
2小时,参考说明one-verify
:是否请求一次有效,默认false
,解决一次消息重复多次的问题,需要配置redis
。one-time-interval
:一次请求的验证时间,默认1小时
,可以配置秒,分钟,小时,天,例如:PT30M
30分钟,PT2H
2小时,参考说明encrypt
:签名加密方式,默认SHA1
,可选值MD2
,MD5
,SHA1
,SHA256
,SHA384
,SHA512
,SHA3_224
,SHA3_256
,SHA3_384
,SHA3_512
,SHA512_224
,SHA512_256
,HMAC_MD5
,HMAC_SHA1
,HMAC_SHA224
,HMAC_SHA256
,HMAC_SHA384
,HMAC_SHA512
sign-type
:签名类型,默认SIMPLE
,SIMPLE
:简单的(只需要对普通的类型参数进行签名);ALL
:需要对全部参数进行签名urls
:拦截的url地址parameter-type
:参数类型,header
请求头,BODY
body参数,query
查询参数。默认、query
headers
^1.5.2:请求头过滤,增加后只有匹配的请求头才可以进入此拦截器,默认为空,全部匹配
示例
yaml
wueasy:
gateway:
filter:
merchant:
enabled: true #是否启用,默认false
items:
- app-id: test #应用id
app-secret: 11e1ebfd58254b84a6f3c1d81d27a562 #应用密钥
name: 测试 #商户名称
time-verify: true #是否启用时间验证,默认false
time-interval: PT30M #时间验证区间,默认30分钟
one-verify: true #是否请求一次有效,默认false
one-time-interval: PT1H #一次请求的验证时间,默认1小时
sign-type: ALL
urls: #拦截的url地址
- /demo/login
#headers: #请求头过滤
# appId: #请求头名称
# - test #对应的值
# - demo
客户端签名示例
- 签名参数必须拼接在url地址中,例如:
http://127.0.0.1:8080/demo/login?appId=test&randomStr=ce9d5159-1c79-4572-b4b1-4640ef03e46b×tamp=1617696265321&sign=e06e59858fb81835030df94db42305d8780acc33
- 需要对所有的参数都进行拼接,最后得到签名值,按照参数名称升序排序
- 如果提交数据的方式为
application/json
,那么body
必须是json
对象{}
,不允许是其他的类型(数组或字符串,对象中可以包含任意类型),并且签名时,需要排除数据和对象字段,只需要普通类型字段
需要传入的参数
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
appId | 是 | string | 应用id |
randomStr | 是 | string | 随机字符串 |
timestamp | 是 | long | 时间戳,毫秒 |
sign | 是 | string | 签名后的值 |
java签名示例
java
package com.wueasy;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.commons.codec.digest.DigestUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.wueasy.base.util.JsonHelper;
import com.wueasy.base.util.StringHelper;
public class Test {
public static void main(String[] args) {
String bodyString = "{\"token\":\"7a081f88ef3b62922c8777d143e90cf0\",\"user\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"authorizeCodeList\":[\"1\",\"2\",\"3\"],\"menuList\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}],\"extendedObject\":{\"a\":\"132******34\",\"user2\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"list\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}]},\"createTime\":1654610049815}";
String appId = "test"; //应用id
String appSecret = "11e1ebfd58254b84a6f3c1d81d27a562";//应用密钥,需要保密,不能传入参数
String randomStr = UUID.randomUUID().toString();//随机字符串
Long timestamp =System.currentTimeMillis();//时间戳,毫秒
//签名参数,按照参数名称升序排序
Map<String, String> signMap = getSignMap(bodyString, "ALL");
signMap.put("appId", appId);
signMap.put("appSecret", appSecret);
signMap.put("timestamp", timestamp+"");
signMap.put("randomStr", randomStr);
//拼接签名字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : signMap.entrySet()) {
String value = entry.getValue();
sb.append(entry.getKey()).append("=").append(value).append("&");
}
String signStr = sb.deleteCharAt(sb.length() - 1).toString();
System.err.println(signStr);
//获取签名后的内容值,使用sha1加密
String sign = DigestUtils.sha1Hex(signStr);
System.err.println(sign);
System.err.println(JsonHelper.toJsonString(signMap));
}
/**
* 获取参数签名map
* @author: fallsea
* @param bodyString
* @param signType
* @return
*/
public static Map<String, String> getSignMap(String bodyString, String signType) {
Map<String, String> signMap = new TreeMap<>(new Comparator<String>() {
public int compare(String obj1, String obj2) {
return obj1.compareTo(obj2);
}
});
if (StringHelper.isNotBlank(bodyString)) {
if (signType == "ALL") {
JsonNode json = JsonHelper.parseTree(bodyString);
getSignAllMap("", json, signMap);
} else {
JsonNode json = JsonHelper.parseTree(bodyString);
json.fieldNames().forEachRemaining((fieldName) -> {
JsonNode item = json.get(fieldName);
if (!item.isArray() && !item.isObject() && !item.isNull()) {
signMap.put(fieldName, item.asText());
}
});
}
}
return signMap;
}
private static void getSignAllMap(String path,JsonNode json,Map<String, String> signMap) {
if(null!=json && !json.isNull()) {
if(json.isObject()) {
json.fieldNames().forEachRemaining((fieldName)->{
JsonNode item = json.get(fieldName);
String path2 = fieldName;
if(StringHelper.isNotBlank(path)) {
path2 = path+"."+fieldName;
}
if(!item.isNull()) {
if(item.isObject()) {
getSignAllMap(path2,item, signMap);
}else if(item.isArray()) {
for (int i = 0; i < item.size(); i++) {
getSignAllMap(path2+"["+i+"]",item.get(i), signMap);
}
}else {
signMap.put(path2, item.asText());
}
}else {
signMap.put(path2, "");
}
});
}else if(json.isArray()){
for (int i = 0; i < json.size(); i++) {
getSignAllMap(path+"["+i+"]",json.get(i), signMap);
}
}else {
signMap.put(path, json.asText());
}
}else {
signMap.put(path, "");
}
}
}
js签名示例
javascript
var bodyString = "{\"token\":\"7a081f88ef3b62922c8777d143e90cf0\",\"user\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"authorizeCodeList\":[\"1\",\"2\",\"3\"],\"menuList\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}],\"extendedObject\":{\"a\":\"132******34\",\"user2\":{\"userId\":\"123\",\"email\":\"123@qq.com\"},\"list\":[{\"menuId\":\"1\",\"menuName\":\"test\"},{\"menuId\":\"1\",\"menuName\":\"test\"}]},\"createTime\":1654610049815}";
console.log(JSON.parse(bodyString))
var obj = JSON.parse(bodyString);
function getSignAllMap(path,json,signJson){
if(null!=json){
let type = Object.prototype.toString.call(json);
if(type == "[object Object]"){
for(let key in json){
let value = json[key];
let path2 = key;
if (path) {
path2 = path + "." + key;
}
if(null!=value && ""!=value && undefined != value){
let type2 = Object.prototype.toString.call(value);
if(type2 == "[object Object]"){
getSignAllMap(path2,value,signJson)
}else if(type2 == "[object Array]"){
for(let i=0;i<value.length;i++){
getSignAllMap(path2+"["+i+"]",value[i],signJson)
}
}else {
signJson[path2] = value;
}
}else{
signJson[path2] = "";
}
}
}else if(type == "[object Array]"){
for(let i=0;i<json.length;i++){
getSignAllMap(path+"["+i+"]",json[i],signJson)
}
}else{
signJson[path] = json;
}
}else{
signJson[path] = "";
}
}
function getSignStr(json){
let signJson = {};
getSignAllMap("",json,signJson)
// 取 key
let keys = [];
for (let key in signJson) {
keys.push(key);
}
// 参数名 ASCII 码从小到大排序(字典序)
keys.sort();
let signStr = "";
for (let i=0;i<keys.length;i++) {
if(""!=signStr){
signStr += "&"
}
signStr += keys[i] + "=" + signJson[keys[i]]
}
return signStr;
}
let signStr= getSignStr(obj)
console.log(signStr)
重要说明
CAUTION
appSecret
作为加密的密钥,一定不能暴露在接口参数中。 最好客户端业务代码进行加密/解密操作,并且混淆代码,提高安全性。