RESTful Service API 常见问题解决方案

REST 风格的优秀设计应该像下面这些

- GET /users 获取所有用户
- GET /users/1234 获取ID为1234的用户
- POST /users 创建一个新用户
- PUT /users/1234 更新ID为1234的用户
- PATCH /users/1234 更新ID为1234的用户的部分内容
- DELETE /users/1234 删除ID为1234的用户

如果要设计一个资源拥有另外一个资源的情况的API,例如,设计一个包含用户(users)和用户的评论(comments)的 API 可以采用这样的形式:
- GET /users/1234/comments 获取用户ID为1234的所有评论
- GET /users/1234/comments/1 获取用户ID为1234的评论ID为1的单个评论
- DELETE /users/1234/messages/1 删除用户评论ID为1,属于用户1234的单个评论
 
实际工程实践中往往会遇到并不是对一个资源简单的 CRUD 的场景,设计此类 API 有这些手法可供参考
  • 将这些操作变成一个资源的属性,比如 disable 一个 user,可以在 user 里面加一个 disabled 的属性,可以设计一个 API 使用 PATCH /users/1234 将 disabled 设置成 true 即可。
  • 将这个操作看成某个资源的附属资源(就像上面例子中的 comments 一样)来设计,比如GitHub的Star a gist API ,就是这样的,它把star操作放在这个资源的后面,看上去好像是一个附属资源:- PUT /gists/:id/star   - DELETE /gists/:id/star
  • 在不得不使用其它例外形式设计 API 时,尽量用文档写清楚输入输出和返回值等其他必要信息,避免让习惯了使用资源名的调用者感到困惑
考虑到系统迭代和兼容性,需要在 API 中引入版本规则
  一种是将版本号放在 http header 内,另一种是直接放在 URL 中。而放在 URL 中是最常见的做法
    GET https://api.twitter.com/1.1/friends
    GET "https://graph.facebook.com/v2.8/me
如果一个 API 的版本过期了,任何把该请求重定向到最新版本上。比如 user API v1 版本过期了,当有调用/api/v1.0/users/1234的时候,应该被重定向(http 30x)到最新的 /api/v2.0/users/1234 上。
优雅的设计条件过滤,排序,搜索等传入参数形式
  RESTful API 经常有对返回数据过滤和排序的要求,这些输入参数推荐采用 HTTP Query Parameter 的方式实现。
  • 比如你要设计一个API,返回所有已经登录的用户,可以这样做:GET /users?login=true
  • 获取所有的用户,返回结果按照create_at降序排序可以这样设计:GET /users?sort=-create_at
  • 组合使用过滤条件和排序,GET /users?sort=-create_at,login_at&login=true 表示返回所有已登录用户,结果按照create_at降序, login_at升序
  • 单独为 API 设计一个 Query Parameter 专门用于搜索,从 API 中传递过来的 Query Parameter 可以直接设置成这些搜索框架的输入条件GET /users?q=key&&sort=-create_at,login_at&diabled=false
  • 映射到一个新的API(相当于快捷方式)比如设计一个用于返回最近登录用户的API:GET /users/recently_login这种设计可以简化客户端的调用,否则调用者每次都要根据时间合成 Query Parameter,增加了客户端使用复杂度
  • 查询数据的部分内容GET /user?fields=id,user_name,address&diabled=false&sort=-login_at   GET /facebook/v2.8/me?fields=id,name,birthday,cover,devices,email&access_token=xxX

API中都使用了下划线(user_name)的形式来命名这些参数,使用划线(user_name)还是使用驼峰(userName)的形式?下划线分割的形式比使用驼峰的形式更容易阅读(容易20%)

合理设计返回数据的形式,格式和考虑启用压缩(gzip)

  假如有个系统提供一个 API 用于上传一张图,这张图上传之后你可以调用另外一个 API 修改这个图片的描述。如果调用上传 API 后,返回数据中没有返回这张图的唯一性 ID,你就无法接着调用其它 API 引用到这个图的资源,从而无法进行修改描述的操作,除非之前额外再次调用查询操作拉取到这张图唯一性 ID。

  通常,POST 操作成功以后,我们一般也把新创建的资源的 URL 放在 HTTP header 的 location 字段中,方便客户的拉取。例如上上树图片上传的 API 返回的 header 中可以包含location: http://api.domain.name/photos/1234

  RESTful API 一般都是返回文本数据,启用 gzip 通常可以节省60%-80%以上的带宽(这个数据很好证明,随便使用几个个 json 文件 gzip下就可以看出来,我测试几个 json 文件一般300K左右都能被压缩成50K左右),尤其是在返回的数据比较大情况下,压缩比更高。不过启用gzip 不可避免会增加 CPU 的负担,实际工程项目中需要权衡考量。

  至于到底用什么用的格式来返回数据?XML?JSON?纯文本?但从统计数据来看 JSON 格式目前是使用做多的 REST API 的输入输出格式。

根据不同的 API 操作,设置合适的 HTTP 状态码和必要的出错信息

  • 200 OK 用于返回 GET, PUT, PATCH 或 DELETE 的操作。有使用也用来返回没有创建数据的 POST 操作;
  • ** 201 Created** 用来返回 POST 操作并且成功创建了数据的情况。新创建的数据资源的链接应该放在location中返回;
  • 204 No Content 用来返回一次成功的请求,但是该请求返回的 body 为空的情况,如 DELETE 请求;
  • 304 Not Modified 表示缓存没有失效,和上次的请求相比,没有新的内容;
  • 400 Bad Request 用于返回 API 参数不正确的情况,比如传入的 JSON 格式错误无法解析等;
  • 401 Unauthorized 用于表示请求等 API 缺少身份验证信息;
  • 403 Forbidden 用于表示该资源不允许特定用户访问;
  • 404 Not Found 请求一个不存在的资源;
  • 429 Too Many Requests 请求过于频繁,可以用在客户端调用过于频繁的情况。

使用 token 机制设计鉴权和验证系统(Authorization and Authentication)

  常见的场景就是用户系统-结合 OAuth2,参考腾讯云微视频MVS API,这里给出一个实用的解决方案:

  • 用户使用户名密码或者第三方登录,最终请求一个我们设计的登录 API(这个 API 接受用户名密码,或第三方登录验证结果);
  • 服务端认证成功以后,生成一个 token,并将这个 token 和用户信息关联在一起,同时返回这个 token 给调用客户端;
  • 客户端记录并保存下这个 token;
  • 下次客户端发起和用户相关请求 API 都要在 http header 中带上这个 token;
  • 服务端通过这个 token 去区分用户是谁,判断这个用户是否已经登录和有什么样的权限;
  • 服务端也要考虑 token 的失效时间;
  • 客户端在发现 token 失效的时候重新请求新的 token

扫描二维码关注公众号,回复: 1006081 查看本文章
为什么要多一个步骤使用 token 呢?为什么不直接把用户名和密码放在 http header 中直接做授权和验证?原因是调用 API 一般会被频繁调用,这样用户名和密码频繁在网络上传输,增加了泄漏的危险。如果使用token,即使泄漏了也不会暴露用户的密码,何况 token 也被经常被设计成有时间限制的,超时以后当前 token 就会失效,需要客户端重新做验证获得新的 token,暴露之后的影响很快就会过去。
其实获取 token,用 token 做授权和验证和 OAuth 2 如出一辙,手法完全相同的,只是 OAuth 2 有更复杂的标准步骤去换取这个token,并且这个 token 的用途不同。OAuth 2 的 token 用来授权给第三方使用,我们自己设计的系统 token 仅限在自己系统本身 API 使用。

如何实现数据的分页返回
 
一种是使用类似 Facebook Graph API 的方法,它把分页信息和数据一起返回,调用者只需要再次请求 next 中 URL 就可以获取下一页的数据,这种方式优点是灵活和直观,可以随意添加和分页相关的其他属性,例如总记录数,总页数等等。其中cursors用来解决“流”的问题(由于数据是动态增加的,基于旧数据的页数和页码会失效,于是引入 cursors 来标记数据位置,关于这个问题,Twitter 在介绍其timeline API时有

另一种符合WEB标准的做法是使用 link header,简单来说就是在 http header 使用 link字段,提供一个和超链接一样目的 URL 地址,来实现不同资源之间的转跳。如GitHub的Api文档是这样规定分页信息的,这种做法缺点是不太直观,优点是不会干扰数据,返回内容都是数据本身,无需在数据上嵌入额外的属性来说明分页信息,简单干净

如何处理有关联资源的返回数据

  对客户端来说,最直观和容易处理的返回形式如下:

  返回数据中 avatar 和 name 是每条数据都是重复的,所以你也可以这样设计返回数据:先返回该用户的所有评论 /comments?user=1234

  再通过请求该用户 API 的相关内容 /users/1234:{user_id: "1234", avatar: "a.jpg", nickName:"Jeffrey"...}这种情况下其实可以将依赖资源嵌入返回对象中,避免了客户端需要再一次发起请求来获取这个 user 的详细信息/comments?user=1234 直接返回类似这样的信息即可:

  

考虑启用 HTTP 缓存机制

HTTP协议本身支持两种缓存机制: ETagLast-Modified

  1. ETag:HTTP 请求中在 header 中包含一个内容的 hash,如果返回结果没有变化,该请求会直接返回304 Not Modified,而不是所有数据内容本身
  2. Last-Modified: 和 Etag 工作原理差不多,只是使用时间戳作为内容是否过期的标志。

限制 API 调用频次(Rate limiting)

 如果一个客户端请求 API 的频率太快,根据HTTP协议,可以返回429 Too Many Requests

如果要为客户端提供更加详细的调用频次和访问次数之类的信息,除了提供文档说明以外,还可以在 http header 用自定义字段的形式提供,比如 Twitter API 是这样做的:
X-Rate-Limit-Limit: 该请求的调用上限
X-Rate-Limit-Remaining: 15分钟内还可以调用多少次
X-Rate-Limit-Reset: 还有多少秒之后访问限制会被重置
 
尽可能的使用 HTTPS,涉及用户验证的 API 一定要强制启用 HTTPS
HTTPS 现在已经是各种网络服务的标配(比如 Xcode 默认不允许请求不安全的 HTTP 信息)
如果你的WEB Server 是 Nginx,在部署了 HTTPS 的情况下,下面两个选项务必仔细设置,因为这个两个简单的设置可以很大程度上避免一些安全问题:
  
  • ssl_prefer_server_ciphers: 表示服务端加密算法优先于客户端加密算法,主要是防止降级攻击 (downgrade attack)
  • Strict-Transport-Security(HSTS):告诉浏览器这个域名在指定的时间(max-age)内应该强制使用 HTTPS 访问。

猜你喜欢

转载自www.cnblogs.com/catherine9192/p/9081778.html