为了保证 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所有,如有转载,请注明来自原作者