RestAPI 设计

在多次项目的大量工程实践之后, 目前采用一下标准响应模式:

  • 只有 GET/POST 方式做响应, 剔除掉 PUT/DELETE 方式
  • 采用自定义的 *-X-Authorization 做 Header 认证参数而不用默认 Authorization
  • 签名采用从参数列表 KEY 正序附加密钥生成签名 MD5
  • 支持 i18n 多语言必须要 Header 追加 *-X-Language 标识语言从参数, 默认 en 英语
  • 请求的内容类型为 application/x-www-form-urlencoded 传递
  • 自定义的 *-X-Version Header 版本转发, 版本更新之后接口参数变化需要该参数决定转发新老接口

默认的响应格式 BODY 结构如下:

lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// HTTP:200, 成功直接返回需要数据结构
// 默认 200 代表成功直接返回数据结构体即可
{
"username": "meteorcat",
"token": "this is token"
}

// HTTP:400, 通用异常如下所示
{
"status": 400,
// 自定义的特殊错误码, 用于客户端接收做特殊拦截
// 这种额外扩展码可以根据业务来决定
"code": "321a",
// 错误列表, 可以让客户端每次提取首行在系统展示
// 这里的 fields 列表的 KEY 可以作为指定输入框标红的情况
//
// 比如帐号密码输入页面, 返回 { "username": "error msg","password": "error msg", }
// 这种情况可能 username/password 输入框需要弹出红色错误提示情况, 就可以提取标识对应输入框
//
// 而如果没有多参数必须要返回错误就直接响应 { "error": "error msg" } 对象组即可
"messages": {
"field1": "Error message 1",
"field2": "Error message 2",
}
}

// HTTP:401, 未授权的请求, 代表需要重定向去获取授权
{
"status": 401,
"error": 401,
"messages": {
"error": "Invalid Authorization"
}
}

// HTTP:404, 找不到数据响应, 提醒用户没有数据
{
"status": 404,
"error": 404,
"messages": {
"error": "Not found Application."
}
}

// HTTP:429, 请求次数过多时使用的状态码, 触发服务器的限流
{
"status": 429,
"error": 429,
"messages": {
"error": "You must wait 15 seconds before making another request."
}
}

这种情况请求数据的结构看起来就像以下方式

1
2
3
4
POST /event/send HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
NOVA-X-Authorization: xxef32g1sfdd

NOVA-X-Authorization 就是自定义 *-X-Authorization 自定义授权头, 用来获取内存的用户数据

注意: {项目标识}-X-Authorization 之类 Header 要设定成动态配置来方便重新设立项目时可以直接复用

参数签名

为了防止参数传递的时候被拦截并且修改, 所有带有参数的请求必须带有以下参数

  • time: UTC毫秒时间戳, 无视时区的全球通用时间戳, 用于保证请求实效性(一般30~60s内)

  • sign: 参数签名MD5哈希, 用于确保所有参数没有被修改过, 如果无法匹配就代表参数非法

这里有个前提就是你的接口服务是 多应用还是单应用 模式?

  • 单应用: 客户端和服务端直接约定 APP_KEY 即可, 直接用该 KEY 对参数做签名

  • 多应用: 需要在管理后台动态创建应用并且生成单独 APP_ID 和 APP_KEY 来做标识应用的参数签名

这里先讲解单应用的请求流程, 后面再扩展多应用的请求方式

单应用一般应用于公司独有不对外的项目服务, 直接内部预定好拿到 APP_KEY 做参数签名, 比如下面所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

$appKey = 'd6c331c8ff490223bfbf38bb06ab1077';// 签名用到的 APP_KEY
$millisecond = intval(round(microtime(true) * 1000)); // 获取UTC毫秒

// 1.构建参数
$parameter = [
'username' => 'meteorcat',
'password' => 'meteorcat',
'time' => $millisecond, // 这里可以假设当前时间戳为 1776136397660
];


// 2. 需要按照 KEY 正序佩列
// 这部分的请求字符串为 password=meteorcat&time=1776136397660&username=meteorcat
// 注意: 不要用 http_build_query 函数做直接签名, 内部会自动处理成 url-encode
ksort($parameter);


// 3. 对参数列表重新构建成 key1=key2&key1=key2 字符串
$data = [];
foreach ($parameter as $k => $v) {
$data[] = "$k=$v";
}
$dataString = implode('&', $data);


// 4. 合并成 {parameter} + APP_KEY
// 签名字符串为: password=meteorcat&time=1776136397660&username=meteorcatd6c331c8ff490223bfbf38bb06ab1077
$sign = md5("{$dataString}{$appKey}");
// 最后得出签名哈希: 04b88aa7a7f36799bd952457922e996c

// 5. 提交参数最后附加上 sign 字段
$parameter['sign'] = $sign;

之后客户端提交的 HTTP 参数请求如下

1
2
3
4
5
POST /user/login HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded

password=meteorcat&time=1776136397660&username=meteorcat&sign=04b88aa7a7f36799bd952457922e996c

而服务端的需要处理流程也比较方便

  1. 提取 time 字段和服务器判断 UTC 时间戳误差是否大于 30~60s 之中

  2. 提取 sign 字段对请求参数重新做 KEY 正序排序再用 APP_KEY 验证MD5哈希签名

这种方式就可以用来处理大部分日常请求的问题, 另外还有版本升级的问题

多应用服务

带有多应用性质一般是要对外发布的通用接口服务, 比较常见的场景就是对外游戏发行SDK的情况.

这里也是采用 Header 自定义头标识指定应用即可, 比如以下方式:

  • 自定义的 *-X-App Header 传入系统内部的应用唯一标识

  • 应用对外标识不推荐使用数据库表的 ID, 会被拿着 int 值遍历导出旗下所有应用数据

  • 管理后台创建应用时, 都会创建 APP_ID 和 APP_IDENT, 前者为 int 值内部使用, 后台随机字符串外部使用

比如这种按照上面的参数签名方式多应用请求方法其实也不需要做太大修改:

1
2
3
4
5
6
POST /user/login HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
NOVA-X-App: v45RgSrnCrxf324x

password=meteorcat&time=1776136397660&username=meteorcat&sign=04b88aa7a7f36799bd952457922e996c

NOVA-X-App 就是自定义 *-X-App 规则的 Header, 而服务端只需要做以下处理

  1. 从 Redis/MySQL 取出对应 APP_IDENT 的应用信息, 不存在就抛出 404 错误

  2. 从获取的应用数据结构提取出 APP_KEY 对参数签名做 MD5 哈希验证处理

这种就是多应用的接口处理方式, 其实也和单应用差不多, 只有请求头附加上对应用标识索引对应 APP 而已

多语言翻译

如果采用你的接口采用面向全球化服务, 那么就需要注意错误和提示返回的文本必须大务符合对应语言文本.

客户端是能获取设备系统语言, 但是应该以服务端的语言字段标识为准, 客户端本地语言需要自己做关联

这里提供一份日常语言 i18n 支持表, 一般需要实现以下语言表功能:

标识 备注
en 英语, 系统默认的语言
zh-CN 简体中文
zh-TW 繁体中文
ja 日文
ko 韩语

采用 BCP-47 本地化的扩展实现, 以上语言表选取实现即可, 如果不支持就默认返回 en

具体的本地化规范: https://tools.ietf.org/html/bcp47

一般采用国际规范的 BCP-47 标识传递 *-X-Language 的 Header 即可, 多应用+本地化 请求如下:

1
2
3
4
5
6
7
POST /user/login HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
NOVA-X-App: v45RgSrnCrxf324x
NOVA-X-Language: zh-CN

password=meteorcat&time=1776136397660&username=meteorcat&sign=04b88aa7a7f36799bd952457922e996c

这里的 NOVA-X-Language 就是自定义的 *-X-Language 规则 Header

版本升级

版本升级并不是接口必选项, 只有要求应用不强制更新的情况才需要用到

比如某个接入SDK的应用v1.0.1版本修改了字段名, 老版本应用不设置强制升级的情况

这种情况下就需要声明 *-X-Version 版本调度, 这里其实挺有意思, 服务端可以按照以下方式选择性处理

  • 反向代理: 如果接口变动很多, 直接独立成两个项目利用从 Nginx 层面转发到不同两个项目入口

  • 内部转发: 通过识别 Header 来做业务层面的请求处理, 也就是自己去处理内部版本代码

这部分见仁见智, 其实个人觉得直接分出划新目录让 Nginx 来识别 Header 转发到指定老项目其实更简洁

需要在 Git 版本库上面同步版本号 tag 方便迁移部署

靠 Header 识别版本转发到对应新老项目请求入口, 避免在原来项目上做 屎上雕花 的操作

而 Nginx 配置配置也十分简单:

1
2
3
4
5
6
7
8
9
10
11
12
map $http_nova_x_version $api_backend {
~^1\. http://api-v1:8080; # 1.x 全走老项目
~^2\. http://api-v2:8080; # 2.x 走新项目
default http://api-v2:8080; # 默认/未传版本 → 新项目
}

server {
location / {
proxy_pass $api_backend;
proxy_set_header NOVA-X-Version $http_nova_x_version;
}
}

后续的请求好在地址会各自转分到对应项目之中

  • {NOVA-X-Version=v1}: 转发到 http://api-v1, 对应本地 /www/nova/v1, 默认也是在该链路地址

  • {NOVA-X-Version=v2}: 转发到 http://api-v2, 对应本地 /www/nova/v2

Header 即路由, Nginx 即调度器, Git Tag 即版本版本地址