您的位置:首页 > 其它

restful 风格API 实践

2017-07-07 17:05 351 查看
不要为了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 Methods

HTTP OperationDescription
GET获取,查找
POST新增创建
PUT更新
PATCH部分更新
DELETE删除

URL组成

网络协议(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": "cloud.mario@gmail.com",    
  "state": "Beijign",    
  "title": "一个开发者"
}


v2版本

GET http://~/v2/accounts/cloud.mario
Response Body:
{        
  "user_name": "cloud.mario",        
  "e_mail": "cloud.mario@gmail.com",        
  "profile": 
    {            
      "state": "Beijign",            
      "title": "一个开发者"
        
    }
}


示例2中的接口返回数据结构已经发生了变化。使用
$version
保证了
v1
v2
接口的共存。

URL定义限制

不使用大写字母

使用中线-代替下划线_

参数列表应该被encode过

接口分类

资源对象的CURD操作

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


示例1: 搜索

GET http://~/services/$version/search?q=filter&category=file


示例2: 任务队列操作

PUT http://~/services/$version/queued/jobs          往任务队列里面添加一个新的任务
DELETE http://~/services/$version/queued/jobs/:id   根据id删除任务


示例3: 更改界面语言环境

PUT http://~/settings/$version/gui/lang
{    "lang": "zh-CN"}


为什么需要区分?

Microservices

Microservices
是一个全新的概念,它主要的观点是将一个大型的服务系统分解成多个微型系统。每个微型系统都能独立工作,并且提供各种不同的服务。独立运行的特点使微型系统之间不会产生相互影响,其中的一个微型系统宕机并不会牵连到其他的微型系统。这种架构使分布式系统的节点数量大大提升。因为RESTful服务是无状态的,所以这种分解并不会带来状态共享的问题。

路由规则(逻辑)

当我们需要对不同属性的接口做路由规则的时候,按功能划分接口是一个很好的方案。例如:我们要对系统设置接口设置增加更严格的调用限制。

缓存

网络接口相对于堆栈接口来说数据传输极其不稳定,尽可能地减少数据传输不仅能控制这种风险还能减少流量。使用缓存还能有效地提高后台的吞吐量。

后台在响应请求时使用响应头
E-Tag
Last-Modified
来标记数据的版本,前台在发送请求时将数据版本通过请求头
If-Match
帮助后台判断缓存的使用。

Request Header

 If-Match: 2390239059405940


Response Header

E-Tag: 2390239059405940Last-Modified: 1403183502701


Bookmarker

在实际的环境中,有大量的查询需求是相同的。将这些搜索需求标签化能降低使用难度也可以达到重用的目的。

示例: 查找状态为关闭的订单

普通方式
GET http://~/$version/trades?status=closed&sorting=-created_at


Bookmarker
GET http://~/$version/trades#recently_closed




GET http://~/$version/trades/recently_closed


HATEOAS

HATEOAS通过Web Linking的方式来描述程序的状态信息

Link 主要包含以下属性:

PropertyDescription
rel关联内容
hrefURL
type媒体类型
methodHttp Method
title标题
arguments参数列表
value返回值
Rel
 可能为以下值:

ValueDescription
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": "cloud.mario@gmail.com",        
    "e_mail": "cloud.mario@gmail.com",        
    "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会带来怎样的改变? 使用它的意义在哪?

分页

Request

GET http://~/$version/trades?page=10&pre_page=100


Response

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
请求头的请求应该被拒绝访问。

Request Header

User-Agent: Data-Server-Client
Authorzation: Bearer 383w9JKJLJFw4ewpie2wefmjdlJLDJF


为什么建议使用Oauth2授权?

Oauth2的参与者为:客户端,资源所有者,授权服务器,资源服务器。客户端先从资源所有者得到授权码之后使用授权码从授权服务器得到
token
,再使用
token
调用资源服务器获取经过资源所有者授权使用的资源。这种授权方式的特点有:

资源所有者可以随时撤销授权许可

可以通过撤销
token
拒绝客户端的调用

资源服务器可以拒绝客户端的调用

通过这三种方式可以做到对资源的严格保护。资源的访问权限也把握在资源所有者的手中,而不是资源服务器。

当然,
Oauth2
授权框架也允许受信任的客户端直接使用token调用资源服务器获取资源。这种灵活性完全取决于客户端类型和对资源的保护程度。

为什么授权码要放在Http Header中?

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


状态码&错误处理

应用状态码

CodeHTTP OperationBody ContentsDescription
200GET,PUT资源操作成功
201POST资源,元数据对象创建成功
202POST,PUT,DELETE,PATCHN/A请求已经被接受
204DELETE,PUT,PATCHN/A操作已经执行成功,但是没有返回数据
301GETlink资源已被移除
303GETlink重定向
304GETN/A资源没有被修改
400GET,PSOT,PUT,DELETE,PATCH错误提示(消息)参数列表错误(缺少,格式不匹配)
401GET,PSOT,PUT,DELETE,PATCH错误提示(消息)未授权
403GET,PSOT,PUT,DELETE,PATCH错误提示(消息)访问受限,授权过期
404GET,PSOT,PUT,DELETE,PATCH错误提示(消息)资源,服务未找到
405GET,PSOT,PUT,DELETE,PATCH错误提示(消息)不允许的http方法
409GET,PSOT,PUT,DELETE,PATCH错误提示(消息)资源冲突,或者资源被锁定
415GET,PSOT,PUT,DELETE,PATCH错误提示(消息)不支持的数据(媒体)类型
429GET,PSOT,PUT,DELETE,PATCH错误提示(消息)请求过多被限制
500GET,PSOT,PUT,DELETE,PATCH错误提示(消息)系统内部错误
501GET,PSOT,PUT,DELETE,PATCH错误提示(消息)接口未实现

容器状态码

容器状态码是指http容器的状态码,应用不应该使用或限制使用

CodeHTTP OperationBody ContentsDescription
303GETlink静态资源被移除,应用限制使用
503GET,PSOT,PUT,DELETE,PATCHtext 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: cloud.mario@sina.com
X-Powered-By: cloud.mario;email=cloud.mario@gmail.com


资源福利

框架&工具

https://github.com/Respect/Rest

https://github.com/RestKit/RestKit

http://raml.org/

https://dropwizard.github.io/dropwizard/

参考资料

http://restfulobjects.org/

https://developer.github.com/v3/

http://tools.ietf.org/html/rfc5988

http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api

https://developer.yahoo.com/social/rest_api_guide/http-response-codes.html

http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  设计 api restful