RESTful API 设计实践

Stella981
• 阅读 673

RESTful API 为网络应用程序设计提供了一套统一、合理的风格。它只是一种风格,而不是标准,所以也就没有一套统一的标准去规范化这些设计,本文从实践的角度出发,讨论 RESTful API 设计上的一些细节,探讨如何设计出一套好用、合理、精炼的 API。

版本

按照 RESTful API 的风格,不同版本的 API 应该是同一种资源的不同表现形式,所以将版本号放在报头,是最符合学术界对 RESTful API 的的定义,但是实际操作情况下,将版本号放在报头不直观,而且操作起来也不方便,反而不如直接放在 URL 来得直接。所以现在两种方式都很常见,各人可以按照自己的喜好来挑一种方式来实现。

个人倾向于直接放在报头中,客户在迁移版本时,会更加方便,基本上不用改任何内容,版本交叉使用也方便。

将版本号放在 URL:

将版本号放在 Accept 报头中:

  • Accept: application/json;version=1
  • Accept: application/json;version=2

路径

在 RESTful 中一条 URL 表示的是一个独立的资源,是该资源的唯一标志。命名上应该具有自描述性,给人一种直觉上的关联,比如 https://api.caixw.io/users/1 让人一看就知道表示的是 ID 为 1 的用户。统一使用名词复数,会使 URL 看起来更加规整,而且对开发者和使用者来说,有个统一的规定,也更容易理解和实现。

一此无法使用 CRUD 表示的操作,应该尽量抽象成相应的操作,比如登录和注销,应该是添加或是删除一个登录的 token:

过滤参数

使用 GET 获取数据时,当数据量过大时,并不是所有数据都是用户需要一次获取的,这时候在服务端对结果进行过滤、排序、分页、查询等功能再返回会是一个比较友好的操作。这些功能应该通过 URL 的查询参数实现。参数值应该尽量避免无意义的值,比如 state=1 从开发者的角度来说,或许可以省略好多的工作量,但是从使用者的角度来说,很难记住这个值 1 代表的是什么意思,当 state 有许多的不同的值时,结果会更加糟糕,所以给出一个确切的字符串来表示:state=lock,会是一个好习惯。

请求方法

RESTful 的核心思想就是将各个不同的 URL 理解成逻辑上的资源,针对资源做 CRUD 的操作。而这些 CRUD 的操作分别对应着不同的 HTTP 请求方法:

方法

幂等[1]

安全[2]

描述

GET

获取资源(一项或是多项)

POST

在服务器新建一个资源

PUT

替换当前资源(客户需要提供完整的资源属性)

PATCH

修改当前资源部分属性

DELETE

删除当前资源

OPTIONS

获取当前资源所支持的方法列表

HEAD

仅获取报头信息,不包含资源本身的内容。

[1] 幂等表示任意多次操作所产生的影响与一次操作产生的影响相同,即使用相同参数重复操作,获取的结果也是相同的。

[2] 安全是指该操作是否会对服务器内容作出修改。

当 PUT 所指的资源还不存在时,其功能上和 POST 是极为相似的,唯一的不同是 PUT 预先知道了资源的地址,所以多次操作时,均指向同一地址。而 POST 则会每次操作都添加一条新资源;当 PUT 所指的资源存在时,其功能与 PATCH 相似,都为修改资源内容,只不过 PUT 要求修改资源的所有数据,而 PATCH 只修改资源的部分数据。所以 PUT 在不同的情况下,分别可以扮演 POST 和 PATCH 两种角色。

用户在分析一个 API 时,可能会用到 HEAD 和 OPTIONS 方法,但是现实中,很少有哪些应用是真正去实现这两个方法的,开发者可根据自身情况看是否需要提供该接口。在跨域操作中,浏览器在每访问一个 API 之前,会访问该 API 的 OPTIONS 方法,以确定服务器是否允许该 API 的访问,所以如果你的接口需要跨域,对 OPTIONS 请求方法的处理是必需的。

状态码

完整的状态码可参考 W3C 的相关文档。这里列出几个常有的状态值:

  • 200:[GET] 服务器成功返回用户请求的数据;
  • 201:[POST] 用户成功创建一个新的资源;
  • 204:[DELETE, PUT] 服务器无需返回任何内容;
  • 400:提交的数据不符合要求;
  • 401:登录信息验证不通过;
  • 404:未的到该资源;
  • 500:服务端错误。

错误处理

HTTP 状态码本身就是一套完善的错误描述机制,状态码相当于错误 ID,返回内容相当于错误内容描述。当然对于一个应用系统来,这些有点简单了,所以一般我们都会做一套符合自己需求的错误描述机制,市面上比较常见的方法就是返回一个固定的数据结构来描述具体的错误内容,比如:

{
    "id": 40101,
    "message": "用户不存在"
}

如果有提交数据的,我们还可以指定具体的字段错误信息,这样客户可以很方便地知道具体是哪个字段出错:

{
    "id": 40001,
    "message": "提交的数据有误",
    "detail": {
        "username": "与已有账号相同",
        "password": "不能为空",
        "nickname": "不能为空"
    }
}

关于错误代码,我们可以直接扩展 HTTP 状态码来达到目的。比如我们可以将 401 的状态码进行细化: 401001 表示用户不存在,401002 表示密码错误,401003 表示 token 过期等。

安全

数据安全

我们一般都会用一个自增的 ID 作为资源的唯一 ID,但是如果把这一 ID 直接呈现给用户的话,就有可能不经意间泄露了你的业务信息。比如查看用户订单列表的接口:/users/100/orders,返回以下数据:

{
    "count": 2,
    "orders": [
        {"id": 100, name: "商品1", created: "2017-07-01"},
        {"id": 1000, name: "商品1", created: "2017-07-30"}
    ]
}

用语根据当前的订单 ID 就可以获取平台的订单量;用户只要在月初和月末各下一单,还能获取到平台的订单月增长量。对于这类的敏感数据,要使用具有唯一性的随机数据,比如 UUID。

上在的接口定义还有一个问题,即指定多余的用户 ID。100 为当前用户的 ID,有些聪明的人就会把 100 换成其它数据试试,这时候如果你的服务端没有做限定的话,无形就造成了用户数据的泄密。甚至是你做了限定,但是对该 ID 用户是否存在,返回了不同的状态提示,对方也可以根据这些试出用户数量。所以对于限定用户的一些接口,我们可以把其 ID 省去,由服务端根据登录的 token 作判断,接口可以改成:/orders。

服务器安全

HTTPS

全站启用 HTTPS 协议,不要在意那么点性能浪费,相对于安全,完全是值得的。

Strict-Transport-Security

HSTS 报头只对 HTTPS 启作用,一旦浏览器接收到这个报头,之后的一段时间之内(时间由 HSTS 报头指 的 max-age 指定)只会对服务器发送 HTTPS 请求,否则就会拒绝传输任何数据。

格式为:Strict-Transport-Security:max-age=17000604;includeSubDomains,其中 includeSubDomains 是一个可选值,指定了表示同时作用于子域名;max-age 指定了时间段,单位为秒,超过该时间段将不再启作用,不过每次获取带有该报头的响应时,都会刷新超始时间。

限定访问频率

限定用户的访问频率,一般是全站所有的接口,在一段时间内,用户有个访问的上限,超过这个上限,我们可以拒绝请求,返回 429 状态码。可以通过令牌桶算法实现。总共包含以下三个报头:

  1. X-Rate-Limit-Limit: 同一个时间段所允许的请求的最大数目;
  2. X-Rate-Limit-Remaining: 在当前时间段内剩余的请求的数量;
  3. X-Rate-Limit-Reset: 为了得到最大请求数所等待的秒数。

其它

  1. 将 API 部署到专门的域名下,比如:https://api.example.com。当然如果接口够简单,且不会有扩展的可能,也可以直接放在主域名下:https://example.com/api/
  2. 使用 JSON 作为数据交换的格式。JSON 是比 XML 更加轻便的数据交互格式,若没有特殊原因,建议使用 JSON 作为数据交换格式,当然如果你有条件,也可两种格式都提供;
  3. 输出格式化之后的数据;
  4. 启用内容压缩。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这