背景

作为你一个前端,可能你常常自诩,你是一个无情的API调用机器,调框架API,调服务端API;但下文可能会震惊到你,至少已经让我崩溃了好几天。

从10月就接到一项需求,需要将我们平台的数据同步到集团另一个平台,how? 通过开放API!!!

这个开放API 有多难调,我大概描述一下

  • 对称加密,对方平台将给我发放一对秘钥;这似乎所有API都会这样做,毕竟安全第一!!!
  • MD5计算, 哦不,是MD5计算后转base64;这个其实也正常,为了数据传输防篡改,请求bodymd5加密也很常见;
  • 请求头摘要加签,加签是什么鬼?摘要是什么鬼?没错,还是为了安全,而且这个玩法很复杂,是我以前没见过的;
  • 只有线上环境可调,线下不可调,what??;但最难的是,环境隔离,就是不能跨机房调用,大白话就是线下机房不能调线上机房(玩法类似于我高中时的小灵通,就是只能拨本市的号码,异地恋是不可能的),这预示着什么?

预示着热更新不可用,本地调不可行?改一行代码,就需要去预发环境(预发环境属于线上机房)部署一次(> 5min), 然后调一下刚刚部署的接口,发现不对,然后又改一行代码,又部署,又调,不断重复!!!Seriously???

可能你现在想知道,是哪个平台商提供了这个API, 悄悄告诉你, 算了,你还是自己看吧:https://help.aliyun.com/docum...

花式调接口

postman 是一个好工具,但有时postman也可能力不从心(postwoman也一样),比如今天要讲的机房内调用。
所以,下面讲到的招式,可能常规前端开发不一定遇得到,也不一定用得上,但看一看又不要钱,万一赚了喃。

招式1:动态传参

通常我们调服务端API接口不通,通常只有两种情况:

  • 服务端傻逼,写了一个根本调不通的接口(我这里不存在,因为传说是通的,至少他向我证明了网关是通的);
  • 前端傻逼,参数没按照指示来

所以,我默认按我是傻逼的方案来做,就是服务端的入参,我都通过我的接口动态传上去,哪不对我就改哪里,听起来可能有点懵逼,我画个图:有点草率。

但事情,原没有我想的那么简单,我确实懵逼了,因为这个接口,远不是入参正确就能调通那么简单。
因为我还得md5计算正确、请求头设置正确、签名正确;

而这些请求头也需要动态改变,不过这时我灵光乍现:上面这个防止自己傻逼方案, 和接口代理有什么区别, 所以我顿时有了方案2:接口代理

方案2:接口代理

proxy, 前端可能都不陌生,webpack-dev-server 就有这个功能,多是用来解决接口跨域。所以,我何必要什么动态传参,我直接加个代理不更简单,所以方案是这样的:

两行代码搞定:

// plugin.js加入插件
export const httpProxy = {
  enable: true,
  package: 'egg-http-proxy',
};

// config.js 加入配置
  config.httpProxy = {
    '/aapi': {
      target: 'http://api-gateway.test.com',
      pathRewrite: {'^/aoneapi' : ''}
    }
  };

这个方案,听起来很美好,热更新有了,线下调试有了;但由于我们平台底层有一些中间件,所以当目标平台响应了400 或 401,我们平台(node-server) 就会拦截,返回一个302重定向, 这就会导致我看不到目标平台真实的错误响应,但改底层拦截改动很大,也会影响到其他同事的开发。

方案3: 远程server直接调

鉴于前面的curl尝试, 证明最直接的接口调用,最短的链路,可以避免最少的错误

curl -v -X GET http://test.goaway.com/checkpreload -H 'Authorization: APPCODE f7f526fd3adf2f38d46'

因为NodeJs 是脚本语言,区别于Java这种编译型语言。这时候好处就体现了,考虑到服务器上有源代码,那就可以直接通过 node 命令去唤起接口调用, 所以现在链路变成了这样:

我在我的仓库中加入了一个文件(test.js),伪代码:

const rp = require('request-promise');
const { createHash, createHmac } = require('crypto');

async function handle(str = []) {
  // 省去具体实现
}

// 获取请求相关数据
const arg = process.argv.slice(ArgStart);
handle(arg);

所以当我把代码部署上去时,我就可以运行node test.js ..., like:

这样执行一下,我能自定义响应体,可以看到请求头是否符合规范,错误发生时可以清楚看到错误响应;最重要的,当我知道错误时,我可以直接在服务器上编辑代码,然后再接着运行命令测试,这样时间就大大节省了。这一切的实现,都归功于NodeJs一门脚本语言!!!

分享个知识: md5

这一次调试,我的卡点可以大概纠结于四个阶段:

  • 机房隔离,网络不通
  • Invalid md5
  • Invalid signalture
  • Authentication error since of operator not configured according to ak

卡的时间最久的,就是Invalid md5;由于最开始官方了提供了SDK,里面提供了md5计算方法:

static getContentMD5(body) {
  const hash = crypto_1.createHash('md5');
  hash.update(body);
  return hash.digest('hex');
}

但问题,就出在这,这个MD5计算和官方文档提到的不一致:

可以看出,在经过md5计算后,又用base64的方式进行了编码。
而官方SDK提供的只需要如下修改一下,就可以获得一致的结果:

static getContentMD5(body) {
  const hash = crypto_1.createHash('md5');
  hash.update(body);
  return hash.digest('base64');
}

那digest方法这个入参到底有什么机密?

细品digest入参

NodeJs官方文档

在crypto:0.1.92版本,入参的typescript是这样定义的;

type BinaryToTextEncoding = "base64" | "base64url" | "hex"

除了上面这三个参数(在0.1.94版本,已经没有base64url选项),其实这个入参还能为空, 为空时返回Buffer, 否则返回string, 所有在日常场景入参都是存在的,因为我们拿着一个Buffer 意义不大,当对hello world做md5计算时,得到的buffer是: <Buffer fc 3f f9 8e 8c 6a 0d 30 87 d5 15 c0 47 3f 86 77>
那为什么不直接用Buffer,因为对象不利于传输!!!

base64 vs hex

又可以理解为base64 vs base16;
所以当我们传入hex时,达到的结果是:fc3ff98e8c6a0d3087d515c0473f8677,这个结果正好就是上面的buffer字符串化。

而传入base64, 得到的结果是:/D/5joxqDTCH1RXARz+Gdw==
因为Buffer 本身是由一串串16进制数组成的,所以转为hex,就会很容易,而转成base64,就需要再编码

第一步: 转化为二进制数:1111-1010-0011.....-0111-0111(32 * 4 = 128)

第二步:转化为6个bit为一小组,然后三个一大组: 111110-100011-.....-011101-11(7 * 3 * 6 + 2)

第三步:补0,补=:00111110-00100011-....-00011101-00110000-=-=

第四步:对照表转字符:/-D-....-d-w-=-=

至少头尾是一致的,套路应该没啥问题。
至于何时用base64, 何时用base16; 对于md5计算,这个主要看服务端心情!但对于大字节流,比如文件,图片的传输,都会选用base64, 因为这可以节省流量,提高传输效率。

那base64Url 又是什么? base64Url 又称为安全的Base64,和 base64的区别仅仅在于63号和64号字符的转换上;

由于"/","="等是URL中的保留字符或不安全字符,因此如果直接在URL中传输Base64编码,保留字符和不安全字符会被替换为%XX的形式,对后端来说解码不方便。如果不替换,就会造成URL注入漏洞。所以 base64Url 其目的在于解决+、\、=在url的传输问题。

结尾

写个文章也要有个仪式感,不能虎头蛇尾,这里就结尾感叹一声吧:做个前端好难!!!

欢迎关注我的前端公众号:前端黑洞

03-05 21:53