您的位置 首页 php

PHP企业开发-高可用接口服务系统-面向世界的接口参数设计

曾经,我的毕业论文课题是:旅游照片浏览与共享系统中Web Services子系统的设计与实现。当时是和团队一起开发旅游照片浏览与共享系统,其中我负责接口子系统的开发。因此当时急需一个合适的接口开发框架用于支撑项目的业务开发,但通过发现和比较,当时国内成熟、流行且专注于PHP接口开源框架并不多。故而决定自主研发一个接口框架,一来充实毕业论文的设计,二来填补PHP接口框架的空缺。

这就是我最早正式接触接口服务系统开发的时机,并因此和接口系统结下了深厚的缘分。毕业后步入职场,继续将自己的生嫩的开发框架应用在实际商业项目开发中并持续完善优化,一段时间后,在框架稳定并趋于成熟时,我把它作为一个PHP开源框架正式发布上线,命名为:PhalApi,一个PHP轻量级开源接口框架。它的slogan是:助你创造价值!

此开源框架又经过数年的成长,逐渐受到了广大开发人员的认可,并分别应用在电商、医疗、旅游、游戏等行业,为更多的商业项目或非盈利项目而提供快速开发的技术基础与解决方案。在补充了相对完整的开发文档外,与此同时我也编写了《初识PhalApi:探索接口服务开发的技艺》电子书,发表在图灵社区。

在这一章,我们再一次继续探索接口服务系统。可以说,这部分是《初识PhalApi:探索接口服务开发的技艺》一书的简化版,再简化版,但提及的内容和关注点又会有所不同。一方面,此章节更多关注的是如何站在业务角度开发高可用的接口服务系统,无论是使用框架,还是不使用框架,都是可以遵循本章所介绍的技艺进行设计和开发。另一方面,也是最重要的,本章会结合我多年的接口开发经验,结合一线开发的现状以及大型企业级系统开发的约束,全面分享和讲解如何搭建一套更加完善的接口系统,使之能匹配复杂领域需求发展的需要、满足苛刻的特质、并能与其他业务系统共振从而产生更大的凝聚力。

8.1 浅谈接口系统的分类

研发接口框架与开发接口服务系统,有着微妙的差异。前者更关注的是如何直接帮助接口开发人员更好的完成具体的项目开发,为工程师提供友好的接口和设计,减少学习和入门的成本,并间接尽可能能满足业务开发的需要。而后者,则重点聚集于领域业务的开发,面临的是业务逻辑处理与业务需求的快速迭代,同时兼顾客端调用、场景使用和系统之间的对接。

8.1.1 按技术分类

根据随着时间推移而使用的 通讯协议 的不同,可以得到相应协议背后不同实现方式的接口系统。较早期,采用的是 SOAP (Simple Object Access Protocol)简单对象访问协议,并与XML可扩展标记语言一起,用于构建 Web Service 。我当时毕业论文设计采用的技术主要也是Web Service,但当前在实际商业项目中使用较少,只有极少数部分国内企业或国外的技术公司才能继续沿用Web Service。随后,行业内出现了 RPC (Remote Procedure Call)远程过程调用协议,对应的开发类库有PHPRPC。相比于Web Service,PHPRPC会更为轻量。

现在,大部分接口系统使用的是路人皆知的HTTP协议,和比XML格式更为简洁的JSON格式一起,组合成:HTTP+JSON+API。这一实现方式,也是我从毕业后到现在,在从事职业开发过程中,见到使用范围最广的实现方式,包括但不限于开源框架、个人项目、初创公司、中小型接口系统、大型企业级中间层接口。因此,在本章的高可用接口服务系统中,我们也以此实现方式为主。

另一方面,如果基于接口系统的规范和设计理念进行分类,又可分为RESTful和微服务。 RESTful 是一种软件架构设计风格,它的核心思想是:万物皆为资源,并制定了相应的规范,更多内容推荐阅读《RESTful Web APIs》一书。而 微服务 是由 Martin Fowler 大师近几年提出来的概念,并广为流传和被引用。如果之前尚未接触过微服务的同学,可以关注微服务的这几点重要特质:

  • 小,且专注于做一件事情
  • 独立的进程中
  • 轻量级的通信机制
  • 松耦合、独立部署

微服务,可以说是当下接口系统混乱开发的一剂良药,为我们开发接口系统提供了很多启发性的帮助,所以我们这一章也是以微服务为根本的指导思想,致力于为客户端提供高可用的接口服务。

8.1.2 按客户端分类

前面我们按接口系统内部的实现技术或设计方式进行了技术分类,另一方面,也可以根据调用的客户端进行分类。而这种分类的做法,能让我们更加侧重于业务端、客户端的使用场景和业务需要进行考虑和设计。如果说,对于商家来说,“顾客就是上帝”,那么对于我们服务端开发工程师来说,也应秉持“客户端就是上帝”的服务态度。接口服务应该是服务于客户端,而不是限制客户端。不能是“我们服务端有什么,你们客户端就用什么”,而是应该提倡“我们客户端需要什么,我们服务端就提供什么”。

为什么这么说,是因为接口系统开发团队和客户端开发团队是隶属于不同的行政部门,在日常跨部门协作与沟通过程中,经常会发生优先级不一致或沟通不顺畅的情况,更糟糕的是有时会闹得两边的开发团队都不愉快。因此,作为服务端开发同学,我们应该始终以友好的态度,服务于我们的客户端,尽量提供客户需要的接口服务,不管内部实现有多困难。

言归正传,主要的客户端为:浏览器(包括PC端和M端)、App移动应用、服务端系统。

如果客户端是浏览器,那么我们需要对接的是 前端开发工程师 ,他们主要使用的编程语言是Javascript,并且需要在兼顾PC版和M版的同时还要小心翼翼留意浏览器兼容性的问题。或是出于前后端分离的考虑,又或许是传统异步加载的做法推动,我们需要为浏览器提供JSON格式的数据接口,以便浏览器客户端可以进行Ajax调用。此时,基于客户端的特点,需要考虑是否存在跨域问题、是否需要支持 JSONP 并预防XSS攻击、是否需要全站切换HTTPS协议以及HTTP协议下兼容和过渡的考虑、是否适合为接口结果采用CDN缓存及其应急刷新机制……更重要的一点是对于安全性的设计,一方面要防止恶意爬虫对重要商业数据的抓取,另一方面要对用户的敏感个人信息获取与操作时需要进行登录态的身份校验。

如果客户端是App移动应用,那么安全性会相对高一些。因为可以在客户端和服务端之间进行签名校验,虽然也会有被逆向工程破解的可能,但相比于源代码的Javascript,使用Java开发的安卓应用以及使用Object-C/Swift开发的iOS应用,经过编译后的识别难度更大,破解更难。与此同时,移动应用可以说更为灵活,因为可以在很大程度上不受浏览器的约束和限制,例如App可通过WebKit等其他方式内嵌H5页面。但对于移动应用,我认为最为值得关注的是不同版本的App应用与服务端接口之间的兼容性问题。一定要谨记,在开发App新版和升级接口系统的接口服务时,要考虑对旧App版本的兼容。如有必要,还应提供不同编程语言的SDK包,以便客户端快速开发和接入。

最后,我们还有一个特殊的客户端,它不是直接被终端用户使用的应用、页面或软件,而是同样运行在服务端的系统。也就是说,此时的客户端,它本身也是服务端系统,很可能也是接口服务系统。如果客户端的系统和服务端的接口系统都是同一公司内的系统,那么就需要考虑进行内网部署,甚至同机房部署,也就是我们常说的内网接口服务系统。如果是提供给外部公司或者第三方系统接入使用的,则要考虑是否搭建一个接口开放平台。例如国外的 Facebook Twitter Amazon 开放平台,又如国内的 微信开放平台 、支付宝开放平台、 新浪微博 开放平台等。这时,要考虑接入控制、权限控制、配套的计费系统以及控制台管理界面。

以上两种分类的方式,即按技术分类和按客户端分类,前者是帮助了我们梳理接口系统内部实现的要素和关键点,后者则是为我们搭建更加完善配套的接口服务生态系统而提供更多的思考。

8.2 面向世界的接口输入

这一节,主要讨论的内容是接口的输入,包括接口参数校验、电子签名、身份验证、权限控制等安全性的话题。在这中间,我们还扩展延伸了配置优于实现这一思想的探讨。接口的输入,是客户端与服务端之间的桥梁,也是两者之间的安检环节。我们既要保证两端之间通讯的友好性,又要保证服务端不受非法请求的攻击或伤害。

8.2.1 接口参数校验

我对接口参数校验的心路

在我第一次正式实习的时候,就开始参与了接口系统的开发工作,并且在那时正式接触了接口参数这一概念。当时我发现一个非常有趣的现象,那就是接口项目代码中,到处充斥着对接口参数验证、解析、处理、原始的重复代码。而参与维护这些代码的同事,不知道是没有意识到这些代码异味,还是未能察觉接口参数验证背后的共性,对此部分的实现既不做任何改进和优化,也不总结提炼。

然而,秉持着KISS原则和DRY原则,以及出于对接口参数验证这一细小领域的兴趣,那时我就开始尝试对这部分进行思考、分析和设计。我希望自己能合理、恰当地封装接口参数的全部操作,包括判断参数是否传递、默认值的设置 、类型的转换、数据合法性的检测,甚至包括接口参数的描述与文档的结合。经过一段时间的努力,对接口参数验证的封装已初见雏形,我将它应用在日常的项目开发中,极大消除了重复的代码,同时更优雅地完成了对接口参数的操作。

当然,我也希望自己对接口参数验证的提炼也能提供或开放给更多的技术开发人员使用。他们只要简单地写一行代码,甚至不需要编写代码而只需简单配置一下,就能实现他们期望的操作。而在这背后,接口参数验证会自动,且略带智能地完成剩下的事情。如果可以,我更希望这些可以成为一种规范,一种被广为接受的约定。

怀着这样的初心,从实习到毕业设计,从全职工作于各商业项目和系统的开发到正式投身 开源社区 ,最初的接口参数验证,经过不断演进,逐步完善,现在成为了PhalApi开源接口开发框架中不可缺少的一部分。在PhalApi中提供的参数规则,友好地提供了对接口参数规则的定义、配置和描述等。通过对参数的规则配置,而不是对参数进行原始化的代码编写,明显提升了开发的效率和质量,受了众多开发人员的喜欢。除此之外,这些参数规则,还可以用于自动生成接口在线文档,一举多得。正因为如此,这些参数规则成为了PhalApi开源框架中规范和标准,甚至还有开发同学将此模块抽离应用在其他的项目和框架中。

下面,我们通过一个简单的登录接口为例,粗略感性认识一下PhalApi框架的参数规则。如果使用的是PhalApi 2.x 版本,在接口实现类中,可以使用以下配置示例对登录接口的账号和密码这两个参数进行控制。

 <?php
namespace App\Api;
use Phal Api \Api;

class User extends Api {
    public function getRules() {
        return array(
            'login' => array(
                'username' => array('name' => 'username', 'require' => true, 'min' => 1, 'max' => 50, 'desc' => '用户名'),
                'password' => array('name' => 'password', 'require' => true, 'min' => 6, 'max' => 20, 'desc' => '密码'),
            ),
        );
    }
}  

这里,针对/?s=User.Login接口服务,配置了两个参数,一个是账号username,一个是密码password。根据上面的配置,不难理解,账号和密码都是必传参数,并且账号的最小长度为1,最大长度为50;密码的长度则介于6和20之间。

 class User extends Api {
    …… 
    public function login() {
        $username = $this->username;   // 账号参数
        $password = $this->password;   // 密码参数
        // 更多其他操作……
    }
}  

配置好参数规则后,对参数的获取就非常简单了。因为,只需要使用类属性的方式,就能获取对应的参数。正如上面看到的$this->username和$this->password,分别表示账号参数和密码参数。这些类成员属性名称与参数规则中的数组下标值对应。

若客户端传递了合法的账号和密码,相应的参数会自动填充到类成员属性。如果客户端什么参数也不提供,则接口服务会返回类似这样的错误提示:

 {
    "ret": 400,
    "data": [
    ],
    "msg": "客户端非法请求:缺少必要参数username"
}  

除了参数的解析和验证外,通过参数规则,还可以指定数据来源,例如限定来自POST参数、GET参数或者COOKIE等。如果现有的参数规则类型不能满足项目的开发需要,还可以自行扩展添加所需要的参数类型。与其他开源框架一样,PhalApi的参数规则也能支持回调函数的配置与调用。概括起来,共有9种内置参数类型,分别是:

  • 字符串 string
  • 整型 int
  • 浮点 float
  • 布尔值 boolean
  • 日期 date
  • 数组 array
  • 枚举 enum
  • 文件 file
  • 回调 callable/callback

更酷的是,框架会自动根据配置的参数规则自动生成在线文档。刚刚配置的账号和密码参数,对应自动生成的文档,局部截图类似如下:

图8-1 账号与密码参数

以上,都是参数规则的外在表现,但我更想和大家分享的是背后的实现、设计和思路。可以说,整套参数规则体系的构造都是围绕着可重用性、易用性和友好性开展的。从大到小,自顶而下,关键的环节扼要总结有:

  • 1、对参数规则的入口调度
  • 2、多重参数规则的合并
  • 3、根据规则获取参数
  • 4、统一格式化操作
  • 5、具体格式化实现类

首先,我们要在框架的核心执行过程的合适位置,添加对参数规则的入口调度。这个时机就是在接口类基类PhalApi\Api::createMemberValue()方法内,它要完成的任务是,根据配置的参数规则把客户端传递的参数转换成接口类的成员属性。

图8-2 PhalApi 2.x的核心时序图

打开PhalApi 2.x的源代码,可以看到这里的实现只是一个 foreach 循环,这是概念视角的高度抽象,在它背后则是规约视角和实现视角的细节。先来看下入口源代码的写法:

 <?php
namespace PhalApi;

class Api {
    /**
     * 按参数规则解析生成接口参数
     * 根据配置的参数规则,解析过滤,并将接口参数存放于类成员变量
     * @uses Api::getApiRules()
     */
    protected function createMemberValue() {
        foreach ($this->getApiRules() as $key => $rule) {
            $this->$key = DI()->request->getByRule($rule);
        }
    }
}  

注意到,这里面依赖了另外两处的实现,一个是多套参数规则的合并, 另一个是根据规则获取参数。将多套参数规则的合并的实现放到接口类内是经过精心设计的。因为参数规则的配置本身就直接来源于接口类的getRules()钩子方法,即来自接口实现子类本身,再进一步,这里的参数规则又可再细分为通用接口参数规则和指定接口参数规则。其次,还有一个原因是因为在接口类内处理配置文件./config/app.php中的apiCommonRules公共参数,也更为贴切。最后,对于配置了白名单的接口服务,还可以在接口类这一层做临时的动态调整。细细品来,这是一个微妙的设计。

接下来,让我们把放大镜挪到PhalApi\Request::getByRule(),继续探索根据规则获取参数的奥妙。此刻,会在请求类Request内完成参数获取的操作,为什么放到Request类也是有原因的。第一,请求类蕴含了各种来源的参数,例如:$_POST、$_GET、$_COOKIE、$_SERVER、$_REQUEST,可以直接从中提炼出需要的数据源;第二,如果缺少必要参数,则可以在这一环节直接抛出异常,给客户端相应的错误提示;最后但不是最重要的,我们还可以对服务端配置的参数进行合法性的校验,以及完成对底层的调用。下面源代码很好诠释了这三层意思。

 <?php
namespace PhalApi;

class Request {
    /**
     * 根据规则获取参数
     * 根据提供的参数规则,进行参数创建工作,并返回错误信息
     * @param $rule array('name' => '', 'type' => '', 'defalt' => ...) 参数规则
     * @return mixed
     * @throws BadRequestException
     * @throws InternalServerErrorException
     */
    public function getByRule($rule) {
        $rs = NULL;

        if (!isset($rule['name'])) {
            throw new InternalServerErrorException(T('miss name for rule'));
        }

        // 获取接口参数级别的数据集
        $data = !empty($rule['source']) && substr(php_sapi_name(), 0, 3) != 'cli' 
            ? $this->getDataBySource($rule['source']) 
            : $this->data;

        $rs = Parser::format($rule['name'], $rule, $data);

        if ($rs === NULL && (isset($rule['require']) && $rule['require'])) {
            throw new BadRequestException(T('{name} require, but miss', array('name' => $rule['name'])));
        }

        return $rs;
    }
}  

继续把镜头放大,拉近我们与第4个环节统一格式化操作的距离。考虑到在大部分情况下,字符串string是使用频率最次也是最为通用的类型,因此如果没有指定参数类型,默认类型就是字符串。而对于没有指定参数默认值的话,则默认值为NULL。完成这些必要的初始化和准备工作后,就到了体现PHP动态脚本语言优势的地方了。根据开发人员配置的参数规则中的类型,可以组装成既定的格式化具体实现类名称,结合依赖注入,实现单例模式下对格式化类实例的资源管理,最后调用统一接口规约下的接口PhalApi\Request\Formatter::parse()。出于对章节完整性的考虑,请允许我再贴下相关的 源代码 片段。

 <?php
namespace PhalApi\Request;
/**
 * Parser 变量格式化类
 */
class Parser 
    /**
     * 统一格式化操作
     */ 
    public static function format($varName, $rule, $params) {
        ……
        return static::formatAllType($type, $value, $rule);
    }

    /**
     * 统一分发处理
     */
    protected static function formatAllType($type, $value, $rule) {
        $diKey = '_formatter' . ucfirst($type);
        $diDefautl = '\\PhalApi\\Request\\Formatter\\' . ucfirst($type) . 'Formatter';

        $formatter = \PhalApi\DI()->get($diKey, $diDefautl);

        if (!($formatter instanceof Formatter)) {
            throw new InternalServerErrorException(
                \PhalApi\T('invalid type: {type} for rule: {name}', array('type' => $type, 'name' => $rule['name']))
            );
        }

        return $formatter->parse($value, $rule);
    }
}  

最后一步,我们终于迎来到了剧情的大结局。前面有说到,PhalApi 2.x内置了9种参数类型,并且默认类型是字符串。所以,以字符串的格式化实现类StringFormatter为例,举一反三,从而了解另外8种格式化实现类的思路和自定义扩展格式化类的开发规范。

 <?php
namespace PhalApi\Request\Formatter;

/**
 * StringFormatter  格式化字符串
 */
class StringFormatter extends BaseFormatter implements Formatter {
    /**
     * 对字符串进行格式化
     */
    public function parse($value, $rule) {
        $rs = strval($this->filterByStrLen(strval($value), $rule));
        $this->filterByRegex($rs, $rule);
        return $rs;
    }
}  

这一层是属于实现视角,更多是实现细节,包括但不限于对参数进行类型转换,范围检测和反序列化解析。

整套设计最终映射到代码模型,涉及到的类、依赖关系和包嵌套层级,可以参考使用PHPDoc生成的图形。

图8-3 参数规则格式化类图

至此,我们就已经快速领略了PhalApi 2.x中参数规则的实现思路和微架构设计。在《初识PhalApi》一书中,我更多介绍的是如何使用参数规则,在这里,我重点分享的是如何站在框架的角度为技术开发人员设计一套友好、易用的参数规则。如果你也打算这么做,可以稍微参考一下这里的做法。

Yii的声明验证规则与 Symfony 的验证

当然,除了重复造轮子,更推荐的做法是直接使用开源框架提供的验证器。例如 Yii 的声明验证规则,包括CFormModel表单验证和和CActiveRecord模型验证;另外又如Symfony的Validation验证。从某种程度上说,验证这块是个非常值得深入研究的细分领域,也已经不少优秀的资料对此进行介绍和总结。

下面,我们再来简单回顾或了解下Yii和Symfony对参数验证的使用方式。

摘自Yii 1.1 权威指南,有以下示例:

 class LoginForm extends CFormModel
{
    public $username;
    public $password;

    public function rules()
    {
        return array(
            array('username, password', 'required'),
            array('password', 'authenticate'),
        );
    }

    public function authenticate($attribute,$params)
    {
        ……
    }
}  

在登录表单中,为账号和密码配置了相应的规则,其中账号和密码都是必须的,并且密码还会通过LoginForm::authenticate()再做进一步的验证。

PhalApi的参数规则,很大的灵感来自Yii。因为Yii中有很多可以通过配置就能实现的功能,除了表单验证外,还有模型、视图组件等。这些配置都很强大,但强大到有点复杂,甚至难以记忆和使用。每次我在使用Yii开发时,都要到官网搜索一遍,找到类似的示例代码或者查看类手册才能知道该具体如何配置。所以,在设计PhalApi参数规则时,我在基于Yii的灵感上做了一些创新,尽量做到易用和简单。顺便分享一下,很多出名的画家,都是从临摹他人的作品开始的,只有在融会贯通后,再加入自己的思想,方能设计出更加精湛的艺术品。同样的道理,在设计框架时,也应该这样,集百家之精华于一身,博取众长,方能研发出更为贴心动人的项目。

另一方面,摘自Symfony官方文档,我们也可以看到表单验证的相关配置。

 public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('myField', TextType::class, array(
            'required' => true,
            'constraints' => array(new Length(array('min' => 3)))
        ))
    ;
}  

纵使未曾使用过Symfony框架,也不难理解上面配置的意思:对于字段myField,指定了文本类型,并且是必须字段,最短长度是3。多么通俗易懂啊。逆向还原容易,同样正向使用也平易近人。

8.2.2 惯例优于配置,配置优于实现

配置的魅力如此之大,以至于我不得不单独用一节的篇幅再对其进行总结和分享。简而言之,本节基于我们都认可这样的价值观为前提,即致力于编写人容易理解的代码。而配置则是通过此价值观的绿色通道。

首先,惯例优于配置。

Ruby是一门优秀的编程语言,而且也处处体现了惯例优于配置的理念。通过简单的代码示例,可以体会这一约定。如普通的操作:

 def say_hello(name)
    # 先打声招呼
    puts "hello #{name}"
end  

对于返回布尔类型的操作,则在方法名后面添加一个问号。

 def more_money?()
    # 假设有点钱
    return true
end  

对于一些危险的、可能会抛出异常的操作,则会在方法名后面追加一个叹号。

 def borrow_me!(money)
    #  假设钱不够
    raise "Not enough momey!"
end  

为了串联进来,假设有这么一个用户故事:你好某某人,有钱吗?借一点给我吧! 则对应的代码片段可以表达成:

 say_hello '某某人'

borrow_me! 100 if more_money?  

对应运行的效果类似如下:

 hello 某某人
./test.rb:13:in `borrow_me!': Not enough momey! (RuntimeError)  

就这么一个简单的案例,可以体会到Ruby元编程语言下约定成俗的做法,而这些大大小小的约定,不仅没有增加开发人员的认知负担,反而构成了Ruby开发人员的共同开发语言。无需过多的解释,Ruby程序员都有快速解读这些符号和约定背后的含义。

其次,配置优于实现。

可以说,拥有惯例的开发团队,会有更高效的合作以及更为畅快的沟通。因为大家都能快速明白简明代码所体现的意图和目的,不存在混淆和错乱。如果缺少开发语言的特性支持,或者所在的开发团队缺少约定编程的氛围,可以退而求其次,采用配置优于实现的做法。

关于配置的做法,在以使用注解的JAVA为例,可以看到其身影,如需要运行一组单元测试,可以这样:

     @RunWith(Suite.class)  
    @Suite.SuiteClasses({    
        Test1.class,   
        Test2.class,  
        Test3.class,  

        TestSuite2.class  
    })  
    public class TestSuite1 {  
    }  

相当于以下JUnit 3中的实现代码:

 public class TestSuite1 {  
    public static Test suite() {  
        TestSuite suite = new TestSuite("Test for package1");  

        suite.addTest(new JUnit4TestAdapter(Test1.class));  
        suite.addTest(new JUnit4TestAdapter(Test2.class));        
        suite.addTest(new JUnit4TestAdapter(Test3.class));  

        suite.addTest(new JUnit4TestAdapter(TestSuite2.class));  
        return suite;  
    }  
}  

通过配置,而是不编码实现,可以更容易传达所需要做的事情,而且配置的背后则是更为规范一致的约定,以及更为严格的检测,从而减少人为的失误的机会。最后,不仅是效率的回报,还是高质量上的获益。

而这一点,和我们在PhalApi、Yii、Symfony中使用配置所达到的效果是类似的。以PhalApi的参数规则为例,除了可以用规则配置取代重复实现的代码外,开发人员还可以使用参数规则库来维护整个项目的规则,甚至还可以将这些配置进行序列化存储,再通过可视化的管理后台进行在线维护和管理。这都得益于配置的应用。

在过往的开发中,我也曾遇到类似的场景,在开发活动系统的功能时,我发现可以把类似常用的活动功能开发抽离成可配置的形式,最后也印证了采用配置而不是实现编程,可取得效率上的提升以及高质量的回报。在曾经关于设计模式的分享里,我把这一模式总结为:开发-配置-使用模式 (DEVELOP-CONFIG-USE PATTERN)。它概括起来是:

  • 问题 :对已有的功能模块,仍然通过编码实现来重复调用。
  • 解决方案 :一次开发后,通过配置而不是代码实现,来使用或定制已有且可重用的功能。
  • 效果 :最大限度减少人为编码的错误,并统一规范的检测、验证、解析过程。
  • 已知应用 :nginx配置、Yii表单验证规则。

在配置的基础上,我们可再向前一步,向声明式编程靠拢。配置式编程固然是好,但视不同的上下文而定,有时还可以再进一步,进入到声明式编程的范畴。

考虑以下表单,

图8-4 登录表单

对登录表单数据进行验证的JavaScript代码片段如下:

 validator.init([{
        dom: iptMobileDom,
        rules: [{
            strategy: 'isNotEmpty',
            errorMsg: '手机号码不能为空'
        }, {
            strategy: 'isMobile',
            errorMsg: '手机号码格式错误'
        }]
    }, {
        dom: iptAuthcodeDom,
        rules: [{
            strategy: 'isNotEmpty',
            errorMsg: '验证码不能为空'
        }]
    }]);  

上面的代码不难理解,作用是对手机号和验证码进行验证,并且通过配置规则可以快速实现对表单的验证。但这样依然有两个缺点:一是具体的规则不能直观说明需要验证的内容,二是当其他场景需要进行类似验证时需要重复编写相同的规则。也就是说,虽然我们有了规则配置,但还是嫌它过于繁琐,那有没有更“懒人”的做法呢?

答案是:有的!我们可以采用声明式编程,即:我们应该告诉代码(同时传达给我们的同伴),我们需要验证什么,而不是着重于具体要怎么验证。听起来,就是第四代编程语言。

首先,可以定义一个规则集合常量:

 const rules = {
          mobile : {
              isNotEmpty : {
                strategy : 'isNotEmpty',
                errorMsg : '手机号码不能为空'
              },
              format : {
                strategy : 'isMobile',
                errorMsg : '手机号码格式错误'
              }
          },
          authcode : {
              isNotEmpty : {
                strategy : 'isNotEmpty',
                errorMsg : '验证码不能为空'
              }
          }
      };  

接着,使用上面元规则进行自由地组合使用,声明需要验证的内容。调整后的代码为:

 validator.init([
        {
            dom : iptMobileDom,
            rules : [rules.mobile.isNotEmpty, rules.mobile.format]
        },
        {
            dom : iptAuthcodeDom,
            rules : [rules.authcode.isNotEmpty]
        }
    ]);  

这样之后,再来看下其他类似的场景,我们即使不细看扩展丰富后的规则,也可以很容易理解明白下面代码的意图。

 validator.init([
        {
            dom : iptPasswordDom,
            rules : [rules.password.isNotEmpty, rules.password.format]
        },
        {
            dom : iptMobileDom,
            rules : [rules.mobile.isNotEmpty, rules.mobile.format]
        },
        {
            dom : iptAuthcodeDom,
            rules : [rules.authcode.isNotEmpty]
        }
    ]);  

在上面常见的表单验证的场景中,可以发现其中一些微妙的关系。需要待验证的DOM节点是可变的,因为不同的场景界面会有不同的class,同时需要进行验证的条件也是可变的,即会存在组合的情况。但是各个规则条件又是可以共用的,最后每一条规则条件都是可重用的一条元规则。故此,把不变的元规则抽取成规则集合,既方便重用,又能增进开发人员之间跨时空的理解。

稍微提炼一下,便可得到以下的设计模型:

图8-5 表单规则的设计模型

为什么说惯例优于配置,配置优于实现呢?因为对于要做一件事件,惯例可以说是不用写任何代码就能轻松实现的;配置是需要简单地写一些任何语言都能识别的普通字符就可以了;而实现则是需要根据不同的编程语言而进行具体的开发。

再次回顾概念视角、规约视角和实现视角这三种面向对象的视角,具体的实现代码则是对应了实现视角,再说得通俗一点(但不一定是对的),惯例为上策,配置为中策,实现为下策。

8.2.3 签名、验证、权限与安全性

如果说客户端提供的参数是不可信任的话,那么对于客户端的身份和来源则更要谨慎判别,最大限度全面提升接口服务系统的防护能力和安全性。至少有以下这几个方面,我觉得是有必要认真考虑的。

  • 渠道接入方
  • 签名校验
  • 登录态检测
  • 加密通讯
  • 权限控制

下面分别简单说之。

不同于网站,不同于直接面向终端用户的系统,接口服务系统的直接客户实际上是手机设备、第三方系统、嵌入式硬件等,而不是活生生的人群。因此,首先我们要规划好等接入的渠道接入方,以便管控各个渠道的访问、版本控制等。如果开发的接口主要为移动设备的应用提供服务,那么我们就要区分好是iOS还是Android,是App 1.0 还是App 2.0,是内测版还是公测版。如果客户端主要是公司内部业务系统,就要区分好是哪个业务系统,为不同的接入渠道分配单独的凭证,方便在出现问题或者流量异常时快速定位业务相关方。如果我们搭建的是一个开放式的接口平台,那么渠道接入方的管理就更不可少了。这些管理包括渠道的有效日期、各自的密钥、主体信息、请求统计,甚至还包括计费系统。

HTTP可以说是一个公开透明的通讯协议,每次请求所传递的参数都是一览无遗,就像一个光秃秃的小屁孩,走在大街上,毫无隐私可言。如果不采取任何保护措施,我们的接口服务就会很容易受到伪造的请求。因此,我们引入了电子签名这一概念。签名是指客户端根据接口服务系统分配的唯一凭证和密钥,根据约定的签名算法进行身份验证。只有当签名核对正确后,才认为是合法的请求。签名验证的算法设计很简单,在PHP在使用ksort、http_build_query、md5等函数就能快速设计一个简单可靠的方案了。

HTTP协议除了是公开透明外,还是一个无状态的请求,不带记忆功能。虽然也可以通过COOKIE或SESSION来增强会话的能力,但通常不建议在接口服务系统中使用这一方案,这也是为什么PhalApi开源框架一直都没有提供对SESSION的封装和接口。更好的做法是,在用户成功登录后,为用户当前会话分配一个TOKEN登录凭证,然后后续客户端在请求接口服务时带上此TOKEN参数。就可以在确保是合法的客户端请求的同时,确保是真实的用户本人。

话说回来,一个光秃秃的小屁孩穿梭在人群中,难免不雅,因此我们可以帮他穿上衣服,或者在别人看到这个短视频前打上马赛克。同样在接口使用HTTP通讯的过程中,我们也可以对客户端的请求以及服务端返回的结果进行加密通讯。例如使用RSA加密算法,这一做法在银行提供的接口服务系统中应用得较为广泛。但在一般的普通商业项目中,过重的加密通讯反而会影响客户端的易用性,这一点需要权衡。

最后,是接口服务系统的权限控制。权限控制在系统层面是针对渠道接入方的权限控制,可以指定特定渠道接入方可以使用哪一类或哪部分接口服务。在业务层面,还可以细分控制当前已登录的用户具备哪些操作的能力或权限,例如分为游客、普通会员、高级会员等。在设计时,可以参考基于角色权限控制的RBAC,或访问权限控制的ACL。

综合渠道接入方的区分、签名校验、登录态检测、加密通讯、权限控制,都是从不同的侧面增强接口服务系统的安全性。但要注意的是,没有绝对的安全,只有相对的安全。纵使全部这些都做到了,对于客户端的重复请求、爬虫的抓取,还是有所欠缺的。在开发接口系统过程中,要综合考虑,做出合适的设计。

文章来源:智云一二三科技

文章标题:PHP企业开发-高可用接口服务系统-面向世界的接口参数设计

文章地址:https://www.zhihuclub.com/77709.shtml

关于作者: 智云科技

热门文章

网站地图