本身作接口开发的时间也算不短了(三年),想写这篇文章其实差很少已经有一年多的时间了。我将从下面的方向来对我所理解的接口设计作个总结:php
接口参数定义 -> 接口版本化的问题 -> 接口的安全性 -> 接口的代码设计 -> 接口的可读性 -> 接口文档 -> 我遇到的坑html
接口设计中往能够抽象出一些新的公共参数,从事了近三年的接口开发工做中,我目前能想到了一些较为常见的公共接口参数以下:android
公共参数 | 含意 | 定义该参数的意义 |
---|---|---|
timestamp | 毫秒级时间戳 | 1.客户端的请求时间标示 2.后端能够作请求过时验证 3.该参数参与签名算法增长签名的惟一性 |
app_key | 签名公钥 | 签名算法的公钥,后端经过公钥能够获得对应的私钥 |
sign | 接口签名 | 经过请求的参数和定义好的签名算法生成接口签名,做用防止中间人篡改请求参数 |
did | 设备ID | 设备的惟一标示,生成规则例如android的mac地址的md5和ios曾今udid(目前没法获取)的md5, 1:数据收集 2.便于问题追踪 3.消息推送标示 |
接口设计中有个算是历史上的难题 -> 接口版本化。曾经也去调研了不少关于接口版本化的资料和设计,最后我获得的结论大体以下:ios
接口的设计确定绕不开安全这两个字,为了达到尽量的安全,咱们须要尽量的增长被攻击的难度,如下是我了解和使用到的一些常见的手段去增长接口的安全性(https这里就不讨论了):nginx
过时验证/签名验证/重放攻击/限流/转义redis
伪代码以下:算法
// 过时验证
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 随机字符串或随机正整数,与 Timestamp 联合起来, 用于防止重放攻击 例如腾讯云是6位随机正整数 */
$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']);
复制代码
这个过程的关键字:抽象成类 前置中间件 注入json
接着就是咱们代码设计的层面了,如何抽象公共的部分与业务代码解耦。后端
通常写法, 定义个全局函数,而后每一个接口开始时调用该函数:api
// 全局定义一个函数
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)
...
// 启动
$checkParams->start();
}
}
复制代码
关于可读性的不得不提到的就是RESTFUL,这里我就不讨论RESTFUL,你们能够自行补充相关知识。关于接口设计可读性的个人一些思考:
// 响应的格式
{
"code": 200,
"msg": "ok",
"data": {
}
}
复制代码
好的接口文档就是生产力, swagger + api blueprint 自行google吧😄
这里遇到的一个比较大的坑就是http协议历史遗留的bug:
不区分url里的空格 和加号➕
带来的问题就是urldecode会把参数里的+号转为空格,因此这种场景的就得使用rawurldecode防止+转成空格。好比作接口的参数校验的时候~