为了保证 api 接口安全,防止数据被篡改,需要设计 api 签名机制。以下为签名过程
接口签名算法
1. 获取参数 一共 4 部分的参数
- path
- query
- body
- 时间戳 {timestamp}
- 随机字符串 {nocestr}
2. 合并参数,然后排序(body 中可能嵌套多层 json,需要递归对对象属性排序,数组的顺序不变)
3. 对上一步对象转为字符串,然后 md5 加密
4. 再用用户 token 为 key,对 md5 加密后的字符串用 hmacSHA512 加密得到 sign
前端签名实现如下
1 2 3 4 5 6 7 8
| import hmacSHA512 from "crypto-js/hmac-sha512"; import md5 from "crypto-js/md5"; const timestamp = +new Date(); const nocestr = generateNoceStr(); const data = Object.assign({}, payload.rawData, { timestamp, nocestr }); const canonical_string = payload.method + md5(JSON.stringify(sortObject(data))); const sign = hmacSHA512(canonical_string, token).toString(); options.headers = { sign, timestamp, nocestr };
|
1 2 3 4 5 6 7 8
| function generateNoceStr(length = 16) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let noceStr = "", maxPos = chars.length; while (length--) noceStr += chars[(Math.random() * maxPos) | 0]; return noceStr; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| function sortObject(obj) { if (Object.prototype.toString.call(obj) === "[object Object]") { const sortData = {}; Object.keys(obj) .sort() .forEach((key) => { if (Object.prototype.toString.call(obj[key]) === "[object Object]") { sortData[key] = sortObject(obj[key]); } else if (Array.isArray(obj[key])) { console.log(obj[key], "数组"); sortData[key] = obj[key].map((e) => { return sortObject(e); }); } else if (typeof obj[key] === "number") { sortData[key] = obj[key].toString(); } else if (typeof obj[key] === "string") { sortData[key] = obj[key]; } else { sortData[key] = obj[key]; } }); return sortData; } else { return obj; } }
|
后端验签实现如下(加在后端合适的位置,比如全局中间件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const timestamp = ctx.headers.timestamp; const nocestr = ctx.headers.nocestr; const sign = ctx.headers.sign; if (!timestamp) { ctx.body = { errno: 400, errmsg: "验签失败,没有时间戳" }; return; } if (!nocestr) { ctx.body = { errno: 400, errmsg: "验签失败,没有随机数" }; return; } if (new Date().getTime() - Number(timestamp) > 1000 * 60) { ctx.body = { errno: 400, errmsg: "验签失败,时间超时" }; return; } if (!sign) { ctx.body = { errno: 400, errmsg: "验签失败,没有签名" }; return; } const rawData = Object.assign({}, ctx.request.body, ctx.query, ctx.params, { timestamp, nocestr }); const mySign = hmacSHA512(ctx.request.method + md5(JSON.stringify(sortObject(rawData))).toString(), headerToken).toString();
if (sign !== mySign) { ctx.body = { errno: 400, errmsg: "验签失败" }; return; }
|
添加请求防止重放
- 原理就是把签名存到 redis 中,再次请求查一下 redis 内是否有该签名。有的话就是重放请求。
1 2 3 4 5 6 7
| // 验签成功后 if (await ctx.app.redis.get('request:sign:' + sign)) { ctx.body = { errno: 400, errmsg: '请求失效' }; return; } await ctx.app.redis.set('request:sign:' + sign, sign); await ctx.app.redis.expire('request:sign:' + sign, 60);// 60秒后,就通不过时间戳判断
|
版权声明: 此文章版权归houxiaozhao所有,如有转载,请注明来自原作者