资讯专栏INFORMATION COLUMN

我所理解的接口设计

taoszu / 2576人阅读

摘要:前言自己做接口开发的时间也算不短了三年,想写这篇文章其实差不多已经有一年多的时间了。

前言

自己做接口开发的时间也算不短了(三年),想写这篇文章其实差不多已经有一年多的时间了。我将从下面的方向来对我所理解的接口设计做个总结:

接口参数定义 -> 接口版本化的问题 -> 接口的安全性 -> 接口的代码设计 -> 接口的可读性 -> 接口文档 -> 我遇到的坑
接口参数定义

接口设计中往可以抽象出一些新的公共参数,从事了近三年的接口开发工作中,我目前能想到了一些较为常见的公共接口参数如下:

公共参数 含意 定义该参数的意义
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 随机字符串或随机正整数,与 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"]);
接口的代码设计 -> 解耦业务 即插即用
这个过程的关键字:抽象成类 前置中间件 注入

接着就是我们代码设计的层面了,如何抽象公共的部分与业务代码解耦。

一般写法, 定义个全局函数,然后每个接口开始时调用该函数:

// 全局定义一个函数
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->operate($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,大家可以自行补充相关知识。关于接口设计可读性的我的一些思考:

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://www.ucloud.cn/yun/28399.html

相关文章

  • 谈谈我所理解面向对象

    摘要:众多面向对象的编程思想虽不尽一致,但是无论哪种面向对象编程语言都具有以下的共通功能。原型编程以类为中心的传统面向对象编程,是以类为基础生成新对象。而原型模式的面向对象编程语言没有类这样一个概念。 什么是面向对象?这个问题往往会问到刚毕业的新手or实习生上,也是往往作为一个技术面试的开头题。在这里我们不去谈如何答(fu)好(yan)问(guo)题(qu),仅谈谈我所理解的面向对象。 从历...

    avwu 评论0 收藏0
  • 我所理解简单项目结构

    摘要:将图片都放入文件夹下指定公共的名字。匹配删除的文件根目录开启在控制台输出信息启用删除文件插入开关说一些可能没用的站在前端角度不懂的很多很多时候一个项目都是由一个小组完成的,小组成员可能包括产品,前端,后端,测试,运营等等。 不急,先听我唠会嗑~ 随着js发展的如此迅速,市场上越来越多的前端框架可以方便开发者使用。 本人大四渣渣一名,先后实习了两个地方,第一家公司用vuejs,实话...

    _DangJin 评论0 收藏0
  • RESTful实践(具体应用)思考

    摘要:其他交互一般会遵循一些数据结构协议或者状态值,比如不同的操作结果对应不同的状态值,且出错会返回指定的错误信息方便前端进行提示等。 RESTful这种架构已经具有很长的时间和历程了,但似乎最近restful这个词出现的频率特别高,目前不是很清楚是因为我自个儿现在是以restful风格写程序产生的孕妇效应,还是单页面程序开发的流行造成的。 其实一开始我也是不想写这篇文章的,因为网络上与re...

    myshell 评论0 收藏0
  • 简述我所理解 PHP Trait

    摘要:和组合的语义定义了一种减少复杂性的方式,避免传统多继承和类相关典型问题。队列的目的是将耗时的任务延时处理,比如发送邮件,从而大幅度缩短请求和相应的时间。同样的道理,根据引入不同的来完成对应的功能。 showImg(https://segmentfault.com/img/remote/1460000010868178); Trait 概念 在常规的 PHP 开发中,我们都习惯于先编写一...

    gecko23 评论0 收藏0
  • 我所理解模板方法模式

    摘要:定义在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为模板方法模式参与角色抽象类抽象类不仅负责实现模板方法,还负责声明在模板方法中所使用到的抽象方法。 定义 在父类中定义处理流程的框架,在子类中实现具体处理的模式就称为模板方法模式 参与角色 抽象类(AbstractClass) 抽象类不仅负责实现模板方法,还负责声明在模板方法中所使用到的抽象方法。 具体类(子类) 该角色负责...

    Y3G 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<