前言
自己做接口开发的时间也算不短了 (三年), 想写这篇文章其实差不多已经有一年多的时间了我将从下面的方向来对我所理解的接口设计做个总结:
接口参数定义 -> 接口版本化的问题 -> 接口的安全性 -> 接口的代码设计 -> 接口的可读性 -> 接口文档 -> 我遇到的坑
接口参数定义
接口设计中往可以抽象出一些新的公共参数, 从事了近三年的接口开发工作中, 我目前能想到了一些较为常见的公共接口参数如下:
公共参数 | 含意 | 定义该参数的意义 |
---|---|---|
timestamp | 毫秒级时间戳 | 1. 客户端的请求时间标示 2. 后端可以做请求过期验证 3. 该参数参与签名算法增加签名的唯一性 |
app_key | 签名公钥 | 签名算法的公钥,后端通过公钥可以得到对应的私钥 |
sign | 接口签名 | 通过请求的参数和定义好的签名算法生成接口签名,作用防止中间人篡改请求参数 |
did | 设备 ID | 设备的唯一标示,生成规则例如 android 的 mac 地址的 md5 和 ios 曾今 udid(目前无法获取) 的 md5, 1: 数据收集 2. 便于问题追踪 3. 消息推送标示 |
接口版本化的问题
接口设计中有个算是历史上的难题 -> 接口版本化曾经也去调研了很多关于接口版本化的资料和设计, 最后我得到的结论大致如下:
接口的版本区分为
大版本
原则: 大版本的数量最多控制到 5 个以内 (我个人跟倾向于 3 个), 超过版本限制的版本提示升级到新版本
方案
uri 携带版本号, 例如: v1/user/get
请求参数, 例如: user/get?v=1.0
小版本
原则: 自己把控吧
方案
uri 携带版本号, 例如: v1/user/get_01
请求参数, 小数点右边就是小版本, 例如: user/get?v=1.1
接口的安全性
接口的设计肯定绕不开安全这两个字, 为了达到尽可能的安全, 我们需要尽可能的增加被攻击的难度, 以下是我了解和使用到的一些常见的手段去增加接口的安全性 (https 这里就不讨论了):
过期验证 / 签名验证 / 重访攻击 / 限流 / 转义
伪代码如下:
- // 过期验证
- if (microtime(true)*1000 - $_REQUEST['timestamp']> 5000) {
- throw new \Exception(401, 'Expired request');
- }
- // 签名验证 (公钥校验省略)
- $params = ksort($_REQUEST);
- unset($params['sign']);
- $sign = md5(sha1(implode('-', $params) . $_REQUEST['app_key']));
- if ($sign !== $_REQUEST['sign']) {
- throw new \Exception(401, 'Invalid sign');
- }
- /**
- * 重访攻击
- * @params noise string 随机字符串
- */
- $key = md5("{$_REQUEST['REQUEST_URI']}-{$_REQUEST['timestamp']}-{$_REQUEST['noise']}-{$_REQUEST['did']}");
- if ($redisInstance->exists($key)) {
- throw new \Exception(401, 'Repeated request');
- }
- // 限流
- $key = md5("{$_REQUEST['REQUEST_URI']}-{$_REQUEST['REMOTE_ADDR']}-{$_REQUEST['did']}");
- if ($redisInstance->get($key)> 60) {
- throw new \Exception(401, 'Request limit');
- }
- $redisInstance->incre($key);
- // 转义
- $username = htmlspecialchars($_REQUEST['username']);
接口的代码设计 -> 解耦业务 即插即用
这个过程的关键字: 抽象成类 前置中间件 注入
接着就是我们代码设计的层面了, 如何抽象公共的部分与业务代码解耦
一般写法, 定义个全局函数, 然后每个接口开始时调用该函数:
- // 全局定义一个函数
- function check () {
- // 校验公共参数
- # code ...
- // 校验签名
- # code ...
- // 校验频率
- # code ...
- // 等等...
- }
二般写法, 定义个父类方法, 然后每个接口类继承该接口, 构造函数调用改方法, 其实和上面的换汤不换药:
- // 父类方法
- class father
- {
- public function __construct()
- {
- $this->check();
- }
- public function check () {
- // 校验公共参数
- # code ...
- // 校验签名
- # code ...
- // 校验频率
- # code ...
- // 等等...
- }
- }
重点来了, 我提倡的第三般写法, 对象链和前置中间件:
- /**
- * 检验抽象类
- */
- abstract class Check
- {
- /**
- * 下一个 check 实体
- *
- * @var object
- */
- private $nextCheckInstance;
- /**
- * 校验方法
- *
- * @param Request $request 请求对象
- */
- abstract public function operate(Request $request);
- /**
- * 设置责任链上的下一个对象
- *
- * @param Check $check
- */
- public function setNext(Check $check)
- {
- $this->nextCheckInstance = $check;
- return $check;
- }
- /**
- * 启动
- *
- * @param Request $request 请求对象
- */
- public function start(Request $request)
- {
- $this->doCheck($request);
- // 调用下一个对象
- if (! empty($this->nextCheckInstance)) {
- $this->nextCheckInstance->start($request);
- }
- }
- }
- // 校验公共参数类
- class ParamsCheck extends Check
- {
- public function operate()
- {
- // 校验公共参数
- # code ...
- }
- }
- // 校验签名类
- class SignCheck extends Check
- {
- public function operate()
- {
- // 校验签名
- # code ...
- }
- }
- // 等等...
- // 前置中间件类
- class FrontMiddleware
- {
- public function run()
- {
- // 初始化一个: 必传参数校验的 check
- $checkParams = new ParamsCheck();
- // 初始化一个: 签名 check
- $checkSign = new SignCheck();
- // 初始化一个: 频率 check
- $checkFrequent = new FrequentCheck();
- // 等等...
- // 构成对象链
- $checkParams->setNext($checkSign)
- ->setNext($checkFrequent)
- ...
- // 启动
- $checkArguments->start();
- }
- }
接口的可读性
关于可读性的不得不提到的就是 RESTFUL, 这里我就不讨论 RESTFUL, 大家可以自行补充相关知识关于接口设计可读性的我的一些思考:
url
非 RESTFUL: 资源 / 资源 / 操作 (动词), 例如 content/article/get -> 获取内容资源下的一篇文章资源
RESTFUL: 资源 / 资源 / 资源, 例如 get content/article/1 -> 获取内容资源下文章 ID 为 1 的文章资源
method
非 RESTFUL: get 便于查 nginx 日志, 上传资源 post, 没啥硬性要求
RESTFUL: 符合 RESTFUL 的思想
request params: 个人更青睐于下划线命名, 适当的单词缩写
response params: 响应的 code 要符合 http status
200 -> 正常
400 -> 缺少公共必传参数或者业务必传参数
401 -> 接口校验失败 例如签名
403 -> 没有该接口的访问权限
499 -> 上游服务响应时间超过接口设置的超时时间
500 -> 代码错误
501 -> 不支持的接口 method
502 -> 上游服务返回的数据格式不正确
503 -> 上游服务超时
504 -> 上游服务不可用
- // 响应的格式
- {
- "code": 200,
- "msg": "ok",
- "data": {
- }
- }
接口文档
好的接口文档就是生产力, swagger + api blueprint 自行 google 吧
我遇到的坑
这里遇到的一个比较大的坑就是 http 协议历史遗留的 bug:
不区分 url 里的空格 和加号
带来的问题就是 urldecode 会把参数里的 + 号转为空格, 所以这种场景的就得使用 rawurldecode 防止 + 转成空格比如做接口的参数校验的时候~
来源: https://segmentfault.com/a/1190000013703264