声明 本文属于转载:原文
此文为实践总结,是自己在实践过程中积累的经验和"哲学"。部分内容参考相关资料,参考内容请看尾页。建议对RESTful有一定了解者阅读!
"哲学"
不要为了RESTful而RESTful
在能表达清楚的情况下,简单就是美
URI指向的是唯一的资源对象
示例: 指向ID为cloud.mario的Account对象
GET http://~/$version/accounts/cloud.mario
URI可以隐式指向唯一的集合列表
示例: 隐式地指向trades list 集合
GET http://~/$version/trades/(list)
等同于
GET http://~/$version/trades
聚合资源必须通过父级资源操作
示例: Profile
是User
的聚合资源,User
有一个唯一且私有的Profile
资源,只能通过User
操作Profile
。
更新user_id为123456的Profile资源 PUT http://~/$version/users/123456/profiles Request Body:{ "full_name": "cloud.mario", "state": "Beijing", "title": "一个开发者" }
HTTP Operation | Description |
---|---|
GET | 获取,查找 |
POST | 新增创建 |
PUT | 更新 |
PATCH | 部分更新 |
DELETE | 删除 |
网络协议(HTTP, HTTPS)
服务器地址
版本
接口名称
?参数列表
GET https://github.com/v1/trades
为什么需要版本?
当服务被更多其他系统使用的时候,服务的可用性和上下兼容变得至关重要。被外部系统依赖的服务在升级时是一个非常麻烦的事情,既要发布新的接口,又要保留旧的接口留出时间让调用者去升级。在URL中加入Version标示能很好地解决上下兼容(新老版本共存)问题。
示例1: URL中新增了Path parameter
v1版本
GET http://~/v1/trades?user_id=123456
v2版本
GET http://~/v2/:user_id/trades
示例1中的user_id
参数在v2版本被加入到path parameter中,使用$version
保证了v1
和v2
接口的共存。
示例2: 数据接口发生变化
v1版本
GET http://~/v1/accounts/cloud.mario Response Body: { "user_name": "cloud.mario", "e_mail": "[email protected]", "state": "Beijign", "title": "一个开发者" }
v2版本
GET http://~/v2/accounts/cloud.mario Response Body: { "user_name": "cloud.mario", "e_mail": "[email protected]", "profile": { "state": "Beijign", "title": "一个开发者" } }
示例2中的接口返回数据结构已经发生了变化。使用$version
保证了v1
和v2
接口的共存。
不使用大写字母
使用中线-代替下划线_
参数列表应该被encode过
GET http://~/$version/trades #获取trades列表 GET http://~/$version/trades/:id #根据id获取单个trade POST http://~/$version/trades #创建trade PUT http://~/$version/trades/:id #根据id更新trade PATCH http://~/$version/trades/:id #根据id部分更新trade DELETE http://~/$version/trades/:id #根据id删除trade
使用settings
标识,根据服务的属性选择http方法。
http://~/settings/$version/server-name
GET http://~/services/$version/search?q=filter&category=file
PUT http://~/services/$version/queued/jobs 往任务队列里面添加一个新的任务 DELETE http://~/services/$version/queued/jobs/:id 根据id删除任务
PUT http://~/settings/$version/gui/lang { "lang": "zh-CN"}
Microservices
Microservices
是一个全新的概念,它主要的观点是将一个大型的服务系统分解成多个微型系统。每个微型系统都能独立工作,并且提供各种不同的服务。独立运行的特点使微型系统之间不会产生相互影响,其中的一个微型系统宕机并不会牵连到其他的微型系统。这种架构使分布式系统的节点数量大大提升。因为RESTful服务是无状态的,所以这种分解并不会带来状态共享的问题。
路由规则(逻辑)
当我们需要对不同属性的接口做路由规则的时候,按功能划分接口是一个很好的方案。例如:我们要对系统设置接口设置增加更严格的调用限制。
网络接口相对于堆栈接口来说数据传输极其不稳定,尽可能地减少数据传输不仅能控制这种风险还能减少流量。使用缓存还能有效地提高后台的吞吐量。
后台在响应请求时使用响应头E-Tag
或Last-Modified
来标记数据的版本,前台在发送请求时将数据版本通过请求头If-Match
帮助后台判断缓存的使用。
If-Match: 2390239059405940
E-Tag: 2390239059405940Last-Modified: 1403183502701
在实际的环境中,有大量的查询需求是相同的。将这些搜索需求标签化能降低使用难度也可以达到重用的目的。
GET http://~/$version/trades?status=closed&sorting=-created_at
GET http://~/$version/trades#recently_closed
或
GET http://~/$version/trades/recently_closed
HATEOAS通过Web Linking的方式来描述程序的状态信息
Link 主要包含以下属性:
Property | Description |
---|---|
rel | 关联内容 |
href | URL |
type | 媒体类型 |
method | Http Method |
title | 标题 |
arguments | 参数列表 |
value | 返回值 |
Rel
可能为以下值:
Value | Description |
---|---|
next | 下一步 |
prev | 上一步 |
first | 第一步,最前 |
last | 最后一步,最后 |
source | 来源 |
self | 资源自身,相对于this |
Web Linking 可以通过两种方式传递至客户端:
Http Header
Link: <http://~/$version/trades?page_no=10>; rel="next", <http://~/$version/trades?page_no=19>; rel="last"
Http JSON Body
{ "links": [ { "rel": "next", "href": "http://~/$version/trades?page_no=1" }, { "rel": "last", "href": "http://~/$version/trades?page_no=19" } ] }
示例1: 用户注册业务
用户填写E-Mail与密码
完善用户资料
Register Request
POST http://~/$version/accountsHeaders: Accept: application/json Content-Type: application/json;charset=utf-8Body: { "username": "[email protected]", "e_mail": "[email protected]", "password": "balabala" }
Register Response
Headers: Content-Type: application/json;charset=utf-8 Status: 201 Created Body: { "uri": "http://~/$version/accounts/cloud.mario", "identity": "cloud.mario", "created_at": 1403535668653, "links": [ { "rel": "next", "href": "http://~/$version/accounts/cloud.mario/profiles", "method": "POST", "title": "Editing Profiles", "arguments": "status=editing" } ] }
Profile Request
POST http://~/$version/accounts/cloud.mario/profilesHeaders: Accept: application/json Content-Type: application/json;charset=utf-8Body: { "full_name": "cloud.mario", "state": "Beijing", "title": "一个开发者" }
Profile Response
Headers: Content-Type: application/json;charset=utf-8Status: 201 Created Body: { "uri": "http://~/$version/accounts/cloud.mario/profiles", "identity": "cloud.mario", "created_at": 1403535668653 }
示例2: 请看下节<分页>
HATEOAS在解决什么问题?
HATEOAS是Hypermedia as the Engine of Application State的缩写形式,中文意思为:超媒体应用状态引擎。它的核心思想是使用超媒体表达应用状态,与hypertext-driven思想是一致的。在此之前,我们大多数的程序业务控制在前台完成。例如:我们会在前台做注册流程,我们在前台判定下一步应该做什么,可以做什么。当使用HATEOAS时,这些状态流程控制都在应用程序的后台完成。我们使用超媒体来表达前台做完某一步骤之后可以做哪些? 这样一来,前台的任务就变得相当简单了,前台需要处理的是理解状态表述,数据收集和结果显示。
思考
HATEOAS会带来怎样的改变? 使用它的意义在哪?
GET http://~/$version/trades?page=10&pre_page=100
Link Header
Link: <http://~/$version/trades?page=11&pre_page=100>; rel="next", <http://~/$version/trades?page=19&pre_page=100>; rel="last"
JSON Body
{ "links": [ { "rel": "next", "href": "http://~/$version/trades?page=11&pre_page=100" }, { "rel": "last", "href": "http://~/$version/trades?page=19&pre_page=100" } ] }
为保证服务的可用性应对服务进行调用过载保护
Response Headers
X-RateLimit-Limit: 3000 调用量的最大限制 X-RateLimit-Reset: 1403162176516 调用限制重置时间 X-RateLimit-Remaining: 299 剩余的调用量
RESTful服务使用Oauth2的方式进行调用授权,使用http请求头Authorization
设置授权码; 必须使用User-Agent
设置客户端信息, 无User-Agent
请求头的请求应该被拒绝访问。
User-Agent: Data-Server-Client Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF
Oauth2的参与者为:客户端,资源所有者,授权服务器,资源服务器。客户端先从资源所有者得到授权码之后使用授权码从授权服务器得到token
,再使用token
调用资源服务器获取经过资源所有者授权使用的资源。这种授权方式的特点有:
资源所有者可以随时撤销授权许可
可以通过撤销token
拒绝客户端的调用
资源服务器可以拒绝客户端的调用
通过这三种方式可以做到对资源的严格保护。资源的访问权限也把握在资源所有者的手中,而不是资源服务器。
当然,Oauth2
授权框架也允许受信任的客户端直接使用token调用资源服务器获取资源。这种灵活性完全取决于客户端类型和对资源的保护程度。
WEB服务器对访问做记录已经成为了行业的一个标准,访问记录不仅可以用来做访问量统计还能用来做访问特征分析。互联网广告平台就是利用访问记录来做精准营销的。如果token
(授权码)包含在URL中就有很大的安全风险。
包含在URL中的token
串可能被进行重定向传递。通过这两种方式入侵者可以不通过授权而使用泄漏的授权码访问那些受保护的数据,会造成数据泄漏的风险。
以Apache为例,访问日志为:
127.0.0.1 - - [24/Jun/2014:14:38:04 +0800] "GET /v1/accounts/cloud.mario?token=dgdreLJLJLER798989erJKJK HTTPS/1.1" 200 343
通过对访问日志的提取,很容易得到token
信息。
查询,过滤条件使用query string。
用来描述数据或者请求的元数据放Header中,例如 X-Result-Fields
。
Content body 仅仅用来传输数据。
数据要做到拿来就可用的原则,不需要“拆箱”的过程。
使用JSON格式传输数据,在http请求头和响应头申明Content-Type
。返回的数据结构应该做到尽可能简单,不要过于包装。响应状态应该包含在响应头中!
Request
Accept: application/json Content-Type: application/json;charset=UTF-8
Response
Content-Type: application/json;charset=UTF-8
错误的做法
{ "status": 200, "data": { "trade_id": 1234, "trade_name": "Bala bala" } }
正确的做法
Response Headers: Status: 200 Response Body: { "trade_id": 1234, "trade_name": "Bala bala" }
示例1: 创建User对象
POST http://~/$version/users Request headers: Accept: application/json Content-Type: application/json;charset=UTF-8 body: { "user_name": "Cloud Mario" } Response status: 201 Created headers: Content-Type: application/json;charset=UTF-8 body: { "uri": "http://~/$version/users/1234", "identity": 1234, "created_on": "Date()", "links": [ { "rel": "next", "href": "http://~/gui/users/1234" } ] }
为什么是JSON?
JSON 是一种可以跨平台高扩展的轻量级的数据交换格式。易于人阅读和编写,同时也易于机器解析和生成。
不能使用大写(大小写友好)
使用下划线_命名(连接两个单词)
属性和字符串值必须使用双引号""
无状态服务器应该允许客户端对数据按需提取。在请求头使用X-Result-Fields
指定数据返回的字段集合。
例如:trade 有trade_id,
trade_name
, created_at
三个属性,客户端只需其中的trade_id
与trade_name
属性。
Request Header
X-Result-Fields: trade_id,trade_name
数据里面的子对象使用URI描述不应该被提取,除非用户指定需要提取子对象
示例: trade
里面的order
对象
错误的做法
{ "trade_id": "123456789", "full_path": null, "order": { "order_id": "987654321" } }
正确的做法
{ "trade_id": "123456789", "order": "http://~/$version/orders/987654321" }
应用指定提取子对象,需要在请求头声明X-Expansion-Fields
Request
X-Expansion-Fields: true
为什么要客户端指定提取子对象时才提取?
懒模式服务能够最大程度地节省运算资源。虽然与客户端交互的次数有所增加,但是能做到按需提取,按需响应,这也是响应式设计的一大特点。客户端的用户行为模式无法真实地模拟,也就无法确定哪些资源需要做到一次性推送,让客户端按需使用是一个不错的方式。
关于空字段
应该在返回结果里面剔除空字段,因为null值传输到客户端并没有实际的含义,反而增加了占用空间。
Tips
使用HTTP Header时,优先使用合适的标准头属性。用X-
作为前缀自定义一个头属性,例如: X-Result-Fields
。
Code | HTTP Operation | Body Contents | Description |
---|---|---|---|
200 | GET,PUT | 资源 | 操作成功 |
201 | POST | 资源,元数据 | 对象创建成功 |
202 | POST,PUT,DELETE,PATCH | N/A | 请求已经被接受 |
204 | DELETE,PUT,PATCH | N/A | 操作已经执行成功,但是没有返回数据 |
301 | GET | link | 资源已被移除 |
303 | GET | link | 重定向 |
304 | GET | N/A | 资源没有被修改 |
400 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 参数列表错误(缺少,格式不匹配) |
401 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 未授权 |
403 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 访问受限,授权过期 |
404 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 资源,服务未找到 |
405 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 不允许的http方法 |
409 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 资源冲突,或者资源被锁定 |
415 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 不支持的数据(媒体)类型 |
429 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 请求过多被限制 |
500 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 系统内部错误 |
501 | GET,PSOT,PUT,DELETE,PATCH | 错误提示(消息) | 接口未实现 |
容器状态码是指http容器的状态码,应用不应该使用或限制使用
Code | HTTP Operation | Body Contents | Description |
---|---|---|---|
303 | GET | link | 静态资源被移除,应用限制使用 |
503 | GET,PSOT,PUT,DELETE,PATCH | text body | 服务器宕机 |
Tips
4开头的错误用来表达来自于客户端的错误,例如: 未授权,参数缺失。5开头的错误用来表达服务端的错误,例如: 在连接外部系统(DB)发生的IO错误。
错误信息应该包含下列内容:
错误标题 message
, 必须
错误代码 error code
, 必须
错误信息 error message
, 必须
资源 resource
, 可选
属性 field
, 可选
文档地址 document
, 可选
Tips
Error Code
尽可能做到简洁明了,提取异常的关键字并且使用下划线_把它们连接起来。
示例: 调用频率超过限制,Response:
Headers: Content-Type: application/json;charset=UTF-8 X-RateLimit-Limit: 3000 X-RateLimit-Reset: 1403162176516 X-RateLimit-Remaining: 0 { "message": "Message title", "errors": [ { "code": "rate_limit_exceeded", "message": "Too Many Requests. API rate limit exceeded", "document": "https://developer.github.com/v3/gists/" } ] }
格式化(Pettyprint)JSON数据(返回结果)并且使用gzip压缩,Pettyprint易于阅读,多余的空格在经过gzip压缩之后占用空间比压缩之前更小。
重写Server
头
返回X-Powered-By
Response Headers
X-Pretty-Print: true Content-Encoding: gzip Server: [email protected] X-Powered-By: cloud.mario;[email protected]