godruoyi

godruoyi 查看完整档案

重庆编辑西南大学  |  计算机科学与技术 编辑敲代码  |  PHP攻城狮 编辑 godruoyi.com 编辑
编辑

二楞徐的闲谈杂鱼

个人动态

godruoyi 赞了文章 · 7月21日

Laravel获取客户端IP,你的姿势对吗

在Laravel中通常使用Illuminate\Http\Request::ip()方法来获取客户端的IP地址。但在某些情况下,它获取到的结果不一定是你所期望的,这些情况包括:

  • 你的应用部署在负载均衡后面
  • 你的应用使用了CDN加速
  • 你的应用部署在其它反向代理后面

那怎样才能获取正确的IP呢?在Laravel中可以使用fideloper/proxy拓展包来解决(本文只讨论Laravel 5.5及以后版本的情况,因为从该版本开始Laravel已经默认集成了该拓展包)。它提供了一个名为App\Http\Middleware\TrustedProxies的中间件,这个中间件可以帮助你设置可信任代理。比方说你的负载均衡服务器的IP是192.168.1.1,那你只需要将这个IP配置到$proxies属性里即可:

/**
 * The trusted proxies for this application.
 *
 * @var array|string
 */
protected $proxies = '192.168.1.1';

有些朋友就会问,我的负载均衡服务器IP不固定怎么办(比如AWS的ELB)?这种情况也能解决,但是需要十分谨慎。首先你需要配置你的应用服务器不响应任何非负载均衡过来的请求,这样做的目的是严格控制请求来源,保证所接收到请求是可信的(比如在AWS里面可以通过设置security groups来实现)。然后再将$proxies设置为*,表示始终信任上层代理进来的请求,即可。

当然,$proxies也可以是数组,如果你有多层反向代理,则需要可配置多个IP地址。这里的IP既可以是IPv4也可以是IPv6,并且可以使用CIDR风格的IP范围,比如:144.220.0.0/16

我本人就接手过一个项目,它的反向代理比上述情况更复杂:我们的应用部署在多个AWS云服务器实例之上,并由ELB进行负载均衡,由于该项目有全球访问的需求,我们在ELB前面还用CloudFront做了CDN加速。前面有介绍ELB的IP是非固定的,并且CloudFront的IP也是非固定。针对这种情况,我们只能逐一分析。对于ELB层,我们使用控制请求源并设置$proxies*即可。而对于CloudFront,好在AWS为开发者提供了CloudFront节点服务器的IP范围,所以我们只要将官网提供的CIDR信息配置到$proxies属性里面即可。当然CloudFront的IP范围可能随时会改变,所以我们会定时抓取接口并将结果缓存,以保证准确性和效率。

原理

了解了如何正确配置TrustedProxies,我们还要学习原理,知其所以然。分析一下App\Http\Middleware\TrustedProxies的源码,不难发现,这个中间件最终做的一件事情,就是调用Symfony\Component\HttpFoundation::setTrustedProxies()方法,将你配置的$proxies赋值到Symfony\Component\HttpFoundation类的$trustedProxies属性中去。看到这你也就明白了,其实这个功能实际是由底层的Symfony提供的,fideloper/proxy拓展包只是帮忙适配了一下Laravel而已(Symfony大法好呀🤘)。

接下来分析源码,打开文件vendor/symfony/http-foundation/Request.php,阅读一下这个方法:

public function getClientIps()
{
    $ip = $this->server->get('REMOTE_ADDR');

    if (!$this->isFromTrustedProxy()) {
        return [$ip];
    }

    return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}

很容易理解,如果你未配置TrustedProxies或者这个请求不是来自可信任的代理,那么就直接返回REMOTE_ADDR地址,这也是为什么获取不到正确IP的原因。如果这个请求来自可信任代理,就会从X-Forwarded-For头中获取客户端的IP。

首先认识一下REMOTE_ADDR,它是服务器(nginx/apache)与客户端进行TCP连接时获取的真实客户端地址,是不可伪造的。比如你使用了负载均衡,那么在应用里获得的REMOTE_ADDR就是负载均衡服务器的地址,否则就是客户机的地址。所以isFromTrustedProxy()方法也是基于REMOTE_ADDR来做判断的。

然后是X-Forwarded-For,它是HTTP协议里常见的一个拓展头,用于记录从客户端到应用服务器之间所经过的代理服务器或者负载均衡的地址,包括客户端地址。格式如下:

X-Forwarded-For: client, proxy1, proxy2, proxy3

每一层代理服务器都会将上一层代理的地址追加到这个头里面来,也就是我们常在nginx配置文件中见到的这项配置:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

所以想要获取到真实的客户端IP,就需要通过这个头部来获取。但需要注意的是,X-Forwarded-For是可以被随意伪造的,比方说我随意构造一个HTTP请求:

$ curl -H "X-Forwarded-For: 192.168.1.1, 192.168.1.2, 192.168.1.3" https://example.com

正因为这种可伪造性,导致我们不能直接使用X-Forwarded-For里的第一个IP作为最终结果。不用担心,Symfony已经帮我们处理了这一切。关于Symfony具体的做法,感兴趣的朋友可以直接查看getTrustedValues()方法的源码,我大致描述一下过程:

首先从HTTP头部中取出X-Forwarded-ForForwarded的值生成IP列表。这里为什么会去取Forwarded头呢?事实上X-Forwarded-For目前不属于任何一份既有规范,这个消息首部的标准版本是Forwarded,格式如下:

Forwarded: by=<identifier>; for=<identifier>; host=<host>; proto=<http|https>

而Symfony兼顾了两种头部格式的处理,但如果这两头同时存在Symfony会抛出冲突异常,你可以通过设置Trusted Header移除其中一个来避免冲突异常。拿到IP列表后,再通过normalizeAndFilterClientIps()方法来滤出客户端IP列表。normalizeAndFilterClientIps()方法会将输入的IP一个一个地判断是否为开发者配置的可信任IP,如果是则从列表中移除,剩余的则是客户端IP列表。但特别重要的一点是,normalizeAndFilterClientIps()方法在返回结果的时候会调用array_reverse()方法将客户端IP列表进行逆序。也许你会有疑问,为什么要将结果逆序返回呢?明明协议中规定第一个才是“真实”的客户端IP,但恰恰是这个逆序,才保证了结果的安全。我们来举个实例就明白了:

假设我们服务器的反向代理链条是这样的:192.168.66.1 -> 192.168.66.2 -> 192.168.66.3,最后一个是应用服务器IP,并且我们的程序中已将192.168.66.1192.168.66.2添加到了可信任代理中。这时有个恶意用户访问了我们的站点,他的主机IP是192.168.1.1,他在访问我们的站点时构造了X-Forwarded-For

$ curl -H "X-Forwarded-For: 192.168.1.3, 192.168.1.2" https://example.com

这个恶意请求最终到达应用服务器后的X-Forwarded-For实际上是这样的:

X-Forwarded-For: 192.168.1.3, 192.168.1.2, 192.168.1.1, 192.168.66.1

程序在normalizeAndFilterClientIps()方法过滤掉可信任代理IP后,剩余的结果为:192.168.1.3, 192.168.1.2, 192.168.1.1。很显然,如果不进行逆序处理,我们使用Illuminate\Http\Request::ip()获取到的IP则是恶意用户构造的192.168.1.3,而逆序处理后获得的IP则是真实的192.168.1.1。所以这个逆序很关键。

了解上述原理以后,即使你不使用Laravel或者Symfony框架,也可以在自己的项目中实现正确的逻辑,而不是从某度CV一段错误的代码,让自己的应用面临风险。

配置文件

有些开发者喜欢讲将配置统一到config/目录下,而不是直接在中间件中进行配置,你只需要运行以下命令,就可以发布配置文件trustedproxies.php

$ php artisan vendor:publish --provider="Fideloper\Proxy\TrustedProxyServiceProvider"

当然,如果你有分环境配置的需求,可自行使用env()方法进行拓展。但是请注意,中间件里的$proxies属性是优先于配置文件的,当$proxies属性有值的时候,配置文件里设置的值将失效,请勿踩坑。

查看原文

赞 5 收藏 4 评论 0

godruoyi 关注了用户 · 7月12日

书旅 @shulv

个人微信公众号:IT猿圈

关注 11

godruoyi 发布了文章 · 2019-12-10

基于雪花算法的 PHP ID 生成器

雪花算法的 PHP 实现 - godruoyi

Snowflake 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的 ID 号码。

其组成为:

  • 第一个 bit 为未使用的符号位。
  • 第二部分由 41 位的时间戳(毫秒)构成,他的取值是当前时间相对于某一时间的偏移量。
  • 第三部分和第四部分的 5 个 bit 位表示数据中心和机器ID,其能表示的最大值为 2^5 -1 = 31;
  • 最后部分由 12 个 bit 组成,其表示每个工作节点每毫秒生成的序列号 ID,同一毫秒内最多可生成 2^12 -1 即 4095 个 ID。

需要注意的是:

  • 在分布式环境中,5 个 bit 位的 datacenter 和 worker 表示最多能部署 31 个数据中心,每个数据中心最多可部署 31 台节点。
  • 41 位的二进制长度最多能表示 2^41 -1 毫秒即 69 年,所以雪花算法最多能正常使用 69 年,为了能最大限度的使用该算法,你应该为其指定一个开始时间。
由上可知,雪花算法生成的 ID 并不能保证唯一,如当两个不同请求同一时刻进入相同的数据中心的相同节点时,而此时该节点生成的 sequence 又是相同时,就会导致生成的 ID 重复。

所以要想使用雪花算法生成唯一的 ID,就需要保证同一节点同一毫秒内生成的序列号是唯一的。基于此,我们在 SDK 中集成了多种序列号提供者:

  • RandomSequenceResolver(随机生成)
  • RedisSequenceResolver (基于 redis psetex 和 incrby 生成)
  • LaravelSequenceResolver(基于 redis psetex 和 incrby 生成)
  • SwooleSequenceResolver(基于 swoole_lock 锁)

不同的提供者只需要保证同一毫秒生成的序列号不同,就能得到唯一的 ID。

要求

  1. PHP >= 7.0
  2. Composer

安装

$ composer require godruoyi/php-snowflake -vvv

使用

  1. 简单使用.
$snowflake = new \Godruoyi\Snowflake\Snowflake;

$snowflake->id();
// 1537200202186752
  1. 指定数据中心ID及机器ID.
$snowflake = new \Godruoyi\Snowflake\Snowflake($datacenterId, $workerId);

$snowflake->id();
  1. 指定开始时间.
$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->setStartTimeStamp(strtotime('2019-09-09')*1000);

$snowflake->id();

高级

  1. 在 Laravel 中使用

因为 SDK 相对简单,我们并没有提供 Laravel 的扩展包,你可通过下面的方式快速集成到 Laravel 中。

// App\Providers\AppServiceProvider

use Godruoyi\Snowflake\Snowflake;
use Godruoyi\Snowflake\LaravelSequenceResolver;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('snowflake', function () {
            return (new Snowflake())
                ->setStartTimeStamp(strtotime('2019-10-10')*1000)
                ->setSequenceResolver(new LaravelSequenceResolver($this->app->get('cache')->store()));
        });
    }
}
  1. 自定义序列号解决器

你可以通过实现 GodruoyiSnowflakeSequenceResolver 接口来自定义序列号解决器。

class YourSequence implements SequenceResolver
{
    /**
     *  {@inheritdoc}
     */
    public function sequence(int $currentTime)
    {
          // Just test.
        return mt_rand(0, 1);
    }
}

// usage

$snowflake->setSequenceResolver(new YourSequence);
$snowflake->id();

你也可以直接使用闭包:

$snowflake = new \Godruoyi\Snowflake\Snowflake;
$snowflake->setSequenceResolver(function ($currentTime) {
    static $lastTime;
    static $sequence;

    if ($lastTime == $currentTime) {
        ++$sequence;
    } else {
        $sequence = 0;
    }

    $lastTime = $currentTime;

    return $sequence;
})->id();

如果您在使用过程中遇到任何问题,欢迎提交 「PR」。

查看原文

赞 13 收藏 8 评论 1

godruoyi 回答了问题 · 2019-08-21

解决laravel有两个字段插入不了数据怎么回事?

Likebar model 里没设置 fillable

/**
 * The attributes that are mass assignable.
 *
 * @var array
 */
protected $fillable = [];

关注 3 回答 2

godruoyi 发布了文章 · 2019-05-27

Nginx 配置跨越支持

_1558941837_uJBftFoaMk.jpg

用你最美的姿态,去「跨域」那座山。阅读原文

在日常的开放中,我们经常遇到跨域的问题,常用的处理方式都是在代码层添加 cors 支持,但若你有 Nginx 配置权限,在 Nginx 上处理跨域将使得程序异常简单和高效。

代理

假设我们的前端域名为 example.com,API 服务架设在 api.example.com 域名下,那我们可以通过代理的形式来配置跨越请求,具体的配置为:

  • 在 Nginx 的 example.com 虚拟主机文件中配置如下的代理
  • 配置成功重启后,前端即可用 example.com/api 的方式和 API 交互
# /etc/nginx/sites-enabled/example.com.conf

location /api/ {
    proxy_pass http://api.example.com/;    
}

这种方式的原理是将 API 提供的服务,代理到前端域名的二级目录下,从而避免跨域。

Response Header

当然由于很多情况下我们不想将服务代理到前端域名二级目下,那可以通过在 Http Response 中添加 Header 来解决跨越,具体配置如下:

# /etc/nginx/snippets/cors.conf;

if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,Content-Disposition' always;
    add_header 'Access-Control-Max-Age' 1728000 always;

    add_header 'Content-Length' 0;
    add_header 'Content-Type' 'text/plain; charset=utf-8';

    return 204;
}

if ($request_method ~* "(GET|POST|DELETE|PUT)") {
    add_header 'Access-Control-Allow-Origin' '*' always;
}
关于何时会发起 OPTIONS 请求及 OPTIONS 请求的内容,可参考阮老师的这篇文章—— 跨域资源共享 CORS 详解

然后在 API 服务域名下添加 CORS 支持即可

# /etc/nginx/sites-enabled/api.example.com.conf

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
    // 引入 cors 配置
    include snippets/cors.conf;

    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
    ...
    ...
}
注意 include snippets/cors.conf 这段代码的位置,若直接放在 location 中,是不起作用的,如下所示:
location / {
    include snippets/cors.conf;

    try_files $uri $uri/ /index.php?$query_string;
}

这是因为下面的 try_files 将请求 Forward 到了 location ~ \.php$ 这个 block 下,在此之前添加的 add_header 命令是无效的。

enjoy ~_~

查看原文

赞 6 收藏 5 评论 2

godruoyi 评论了文章 · 2018-08-09

Let's Encrypt 泛域名证书申请及配置

file

Let's Encrypt 在今年 3 月份就已经推出泛域名证书支持了,以前我一直是使用的单域名证书,加上站点开启了 HSTS 支持,当新增网站应用时不得不为其单独申请证书,十分不便。

目前比较常用的为 Let's Encrypt 生成证书的工具比较多,如

这里我们将使用 acme.sh 这个工具来安装 Let's Encrypt 证书。acme.sh 是一个非常优秀的证书生成工具,其 官网 更是有详细的中文文档支持 。

安装

你可以通过下面的脚本来安装 acme.sh

curl  https://get.acme.sh | sh
该操作需要服务器支持 socatcurl 模块。(apt install socat curl)

安装成功后,会在当前文件夹下生成 .acme.sh 文件夹。

生成证书

acme.sh 实现了 acme 协议支持的所有验证协议,一般有两种方式验证: httpdns 验证。由于泛域名证书的解析目前仅支持 DNS 方式验证,下面我们将通过 DNS 方式来验证你的域名所有权。
acme.sh  --issue  --dns  -d godruoyi.com -d *.godruoyi.com

这种方式会将相应的解析记录显示出来,然后你需要在你的域名管理面板中添加这条 txt 记录。并等待解析完成之后,重新用下面命令生成证书:

acme.sh  --renew   -d mydomain.com
注意第二次这里用的是 --renew,当然我们并不想这么麻烦,dns 方式的真正强大之处在于可以使用域名解析商提供的 api 自动添加 txt 记录完成验证。

根据你的域名服务商类型,选择对应的 DNS API。如

1、腾讯云

这里申请 API Token,获取到 IDToken 后执行:

file

export DP_Id="id"
export DP_Key="token"

2、阿里云

这里申请阿里云 Accesskey

file

获取到 KEYSecret 后执行下面命令:

export Ali_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
export Ali_Secret="jlsdflanljkljlfdsaklkjflsa"

3、生成证书

在配置好上述设置后,就可通过

.acme.sh/acme.sh --issue --dns dns_dp -d godruoyi.com -d *.godruoyi.com

来生成证书,注意这里第一个域名为顶级域名,后面个为泛域名。

这种方式将自动为你的域名添加一条 txt 解析,验证成功后,这条解析记录会被删除,所以对你来说是无感的,就是要等 120秒

证书生成成功后,默认保存在 .acme.sh/你的顶级域名 中。

配置 Nginx

下面我们来为 Nginx 配置 SSL 证书支持。

1、移动下列证书到 /etc/nginx/ssl 文件夹,若无该文件夹,自行创建。

cp ~/.acme.sh/godruoyi.com/fullchain.cer /etc/nginx/ssl/fullchain.cer
cp ~/.acme.sh/godruoyi.com/godruoyi.com.key /etc/nginx/ssl/godruoyi.key

2、新建 ssl-params.conf 并把它放到 Nginx 的 snippets 目录中。

下面的这些配置来自 提高安全性的最佳 Nginx 配置,建议参考。
# /etc/nginx/snippets/ssl-params.conf

server_tokens   off;

ssl_session_cache        shared:SSL:10m;
ssl_session_timeout      60m;

ssl_session_tickets      on;

ssl_stapling             on;
ssl_stapling_verify      on;

resolver                 8.8.4.4 8.8.8.8  valid=300s;
resolver_timeout         10s;
ssl_prefer_server_ciphers on;

# 证书路径 绝对地址
ssl_certificate          /etc/nginx/ssl/fullchain.cer;
ssl_certificate_key      /etc/nginx/ssl/godruoyi.key;

ssl_protocols            TLSv1 TLSv1.1 TLSv1.2;

ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

add_header Strict-Transport-Security "max-age=31536000;includeSubDomains;preload";
add_header  X-Frame-Options  deny;
add_header  X-Content-Type-Options  nosniff;
add_header x-xss-protection "1; mode=block";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https:; connect-src 'self' https:; img-src 'self' data: https: blob:; style-src 'unsafe-inline' https:; font-src https:";

3、接下来在 Nginx 主配置文件中开启 SSL 支持

# /etc/nginx/nginx.conf

http {
    ....
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
}

完整的 Nginx 配置文件请参考 我的 Nginx 配置

4、配置虚拟主机

# /etc/nginx/sites-available/godruoyi.com

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name godruoyi.com www.godruoyi.com;
    return 301 https://$server_name$request_uri;
}

server {
    # 注意我们设置该站点为默认站点,并移除了 nginx 默认的 default 配置
    listen 443 ssl http2 fastopen=3 reuseport default_server;
    listen [::]:443 ssl http2 fastopen=3 reuseport default_server;

    server_name www.godruoyi.com godruoyi.com;

    # 引入 SSL 及 PHP 配置
    include snippets/fastcgi-php.conf;
    include snippets/ssl-params.conf;

    root /home/godruoyi/websites/godruoyi.com/public;

    access_log /home/godruoyi/websites/godruoyi.com/storage/logs/nginx-access.log;
    error_log  /home/godruoyi/websites/godruoyi.com/storage/logs/nginx-error.log error;

    index index.php;

    # 当访问域名是不  godruoyi.com 强制跳转到 https://godruoyi.com
    if ($host != 'godruoyi.com' ) {
        rewrite ^/(.*)$ https://godruoyi.com/$1 permanent;
    }
}

再来看一个 admin.godruoyi.com 的配置

# /etc/nginx/sites-available/admin.godruoyi.com

server {
    listen 80;
    listen [::]:80;

    server_name admin.godruoyi.com;
    return 301 https://$server_name$request_uri;
}
server {
    # 如果多个域名配置在同一主机,这里只需要监听到 433 就可以了,
    # 不需要再添加 ssl http2 fastopen=3 reuseport default_server 之类的了
    listen 443;      
    listen [::]:443;

    root /home/godruoyi/websites/admin.godruoyi.com/public-admin;

    access_log /home/godruoyi/websites/admin.godruoyi.com/storage/logs/nginx-access.log;
    error_log  /home/godruoyi/websites/admin.godruoyi.com/storage/logs/nginx-error.log error;

    server_name admin.godruoyi.com;

    index index.php;

    client_max_body_size 20M;

    include snippets/fastcgi-php.conf;
    include snippets/ssl-params.conf;

    if ($host != 'admin.godruoyi.com' ) {
        rewrite ^/(.*)$ https://admin.godruoyi.com/$1 permanent;
    }
}

4、虚拟主机配置完成,接下来为其配置软连接测试成功后就可以重启 Nginx 啦。

sudo ln -s /etc/nginx/sites-available/godruoyi.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/admin.godruoyi.com /etc/nginx/sites-enabled/

# 测试配置是否成功
sudo nginx -t

sudo service nginx restart

以上所有配置你都可以在 这里 找到,别忘了点颗小星星哟

文章首发于 二楞的闲谈杂鱼


参考链接

查看原文

godruoyi 评论了文章 · 2018-07-27

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

godruoyi 评论了文章 · 2018-07-27

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

godruoyi 评论了文章 · 2018-07-27

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

godruoyi 评论了文章 · 2018-07-27

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

认证与成就

  • 获得 420 次点赞
  • 获得 19 枚徽章 获得 1 枚金徽章, 获得 5 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-06-25
个人主页被 2.1k 人浏览