Web API 的设计和开发
- 什么是 Web API
- 软件组件的外部接口;为了从外部调用软件的功能,需要指定调用该功能应该遵守的规则,这些规则,即是 API;
- API 的重要性:通过使用API,避免大包大揽,将一些非核心功能开放给第三方来开发,通过分工建立生态,建立更加强大的生命力;
- 面向外部大量开发者群体的 API 和面向内部少量开发群体的 API,在设计思想上会有所区别,前者侧重普适通用性,后者侧重上手,二者对于“优美”的定义不同;
- 响应数据的格式
- JSONP
- 目的:规避同源策略的限制
- 用法:将响应数据作为 script 进行引用;
- 原理:因为 Script 的加载不受同源策略约束;
- 建议:能不支持尽量不支持,原因:安全问题,可以会被攻击;
- 数据内部结构
- 原则:尽量减少 API 请求次数,一次性返回所需要的数据;
- 让用户选择响应的内容;指定单个字段太麻烦的话,可以考虑使用小组及组合或嵌套的策略,将所需要的字段根据场景做进一步的抽象;
- 封装可以让响应格式统一,但有点浪费 HTTP 协议本身自带的封装效果;
- 数据尽量扁平化,除非层级有绝对优势才考虑使用层级;
- 列表建议做 { } 封装,原因:封装后不符 js 语法,可以避免攻击;
- 列表的响应,可以添加 hasNext 字段,来表示是否还有后续内容;
- 数据的格式
- JSON:驼峰命名法
- 性别:GENDER 是社会意义的,SEX 是生物意义的;
- 日期的格式: RFC3339,2015-10-12T11:30:22+09:00
- 大数据:由于JS处理64bit长度的局限性,可以转成字符串传输;
- 响应数据的格式
- 出错信息用状态码表示,遵循HTTP的规范,即2开头表示成功,4开头表示客户端请求有误,5开头表示服务器内部处理有误(原因:更加易懂);
- 向客户端返回详细的错误信息,以便其了解原因,方便调试
- 可以考虑使用 devMsg 和 userMsg 来区分显示给用户和开发人员的消息内容;
- 另外还可以加上 info 字段放上文档链接,方便人员查询错误代码的具体意义;
- 有时需要返回意义不明确的信息,例如登录错误(涉及安全),好友屏蔽(涉及隐私)等;开发环境和生产环境也可以做区分,开发环境返回真实的信息,生产环境返回模糊的信息;
- JSONP
- 尽量利用 HTTP 协议
- 尽量使用通用规范的好处:让他人的学习成本更低,易于理解;
- 状态码:2、3、4、5,每个数字下面,都对应有几十种细分,非常丰富,所以应尽量使用它们来表示想要的状态;
- 202:表示服务端已经开始处理,但还没有弄好;此时可以在响应消息的 Retry-after 字段里面,放上时间(目测这个功能可以用来在客户端显示进度?)
- Etag:一种缓存校验机制;缓存的目的是提高通信效率,但可能存在的问题是如何获取最新版本,此时通过给数据的版本加上唯一标识符,即可以知悉资源是否更新;如果标识符发生变化,则表示需要获取最新版本;
- 3开头,其中的强制重定向,可以用来避免客户端在原页面重复进行操作,当请求处理成功后,返回给客户端一个新的页面,这在付款、下单、下载资源等场景非常有用;
- 4开头:认证错,授权错,资源不存在,资源冲突,方法错,格式不支持,曾经存在,超时,太长太大,超次数
- 5开头:内部错,存在不可用;
- 缓存与HTTP规范
- 涉及方:客户端缓存、客户端代理服务器缓存、服务端代理服务器缓存;
- 过期模型:通过在首部指定过期时间(绝对时间,相对时间等),可以有效减少请求次数;
- 校验模型:通过判断资源是否更新(虽然没有减少请求次数,但适用于大数据交互的场景);通过 Entity Tag 来实现,即 ETAG,在首部加上这个信息用于判断;根据场景可分为强验证和弱验证两种;
- 启发式过期:服务器端没有指定具体的过期时间,客户端(浏览器)自己判断;比如首部中有 last modified 字段,假设为1年前,则客户端可以猜测缓存有效期可以设置为1年左右;如果 last modified 是昨天,则缓存有效期可以设置为半天;
- 不希望缓存: no-cache(其实有缓存,只是每次都需要验证),no-store(真的没有缓存)
- vary 字段:用来增加资源的唯一识别,除了 URI 外,再加上 vary 批定的字段,只有这些字段的值全部一致时,才读取缓存数据,如果不一致,则重新到服务器进行请求;
- cache-control 支持多种参数,其中有一种 stale-while-revalidate,假设该字段值设置为1000,则表示1000到期后,缓存仍然有效,并且,此时会异步去服务器请求最新的数据;这样的好处是即提高了响应速度,也能保证数据及时更新;
- 媒体类型:由于有时客户端采用的第三方库,会根据返回的媒体类型进行不同的操作,因为返回正确的媒体类型很重要,避免客户端处理出错;
- x- 开头的媒体类型,表示该类型尚未在官方组织正式注册;但新的 RFC 规范已经废止通过这种方式来自定义媒体类型,而采用了“注册树”的办法;
- x- 也常用于表示私有首部;
- API 的设计要方便更改
- 常用做法:在 URI 中嵌入主版本号
- 最大程度的减少 API 的更新;如果必须更新,注意向下兼容;
- 发布新版 API 后,如果需要停止对老版本的支持,则建议正常先保留老版本半年;
- 安全性与可靠性
- 安全问题
- 劫持服务端与客户端之间的通讯信息;
- 应对措施:使用 HTTPS 进行加密;缺点:会增加一些访问的时间,性能有所下降;
- 攻击服务器获取服务端的数据;
- 劫持服务端与客户端之间的通讯信息;
- 攻击类型
- XSS:在传输的数据中,注入恶意的代码(例如 js),然后触发它被执行;例如先伪造一个 URL,吸引用户点击,之后传输包含恶意代码的网页给用户,用户端的浏览器显示该网页时,触发恶意代码被执行;该攻击有3种类型,其中一些只危害用户端,另2种会危害服务器端
- 防范:对用户的输入进行检查,确保其中不包含恶意的代码
- XSRF:跨站点请求伪造;站点 B 伪造成用户的身份登录站点 A 进行操作;
- 前提:用户登录了 站点 A,同时,用户打开了站点 B
- 原理:当用户点击 B 时,执行恶意代码,获取了站点 A 给用户的 Cookie,凭借这个 Cookie,站点 B 假装自己是用户,向站点 A 发起了非法的请求;
- 防范:除了 Cookie 外,增加其他验证用户身份的方式;比如:
- 只能使用 POST 方法进行数据更新的操作(原因:因为 GET 方法缺少验证)
- 使用 TOKEN 验证;
- 使用 一些只能人工识别的验证方式,例如识别图片上面的字符
- 来自怀有恶意的用户的攻击
- 修改参数达到有利自己的目的(防范:仔细检查参数的有效性)
- 多次发送(防范:增加状态的判断,例如通过增加虚拟的订单和收据,然后根据单据的状态变化,来判断是否允许再次操作)
- XSS:在传输的数据中,注入恶意的代码(例如 js),然后触发它被执行;例如先伪造一个 URL,吸引用户点击,之后传输包含恶意代码的网页给用户,用户端的浏览器显示该网页时,触发恶意代码被执行;该攻击有3种类型,其中一些只危害用户端,另2种会危害服务器端
- 同安全相关的 HTTP 首部
- X-XSS-Protection: 1
- 开户后,浏览器可以检测和防御 XSS 攻击(Chrome 和 Safari 此功能是默认开启的,且无法禁用)
- X-Frame-Options: Deny
- 攻击者在用户的页面上添加透明的 iFrame 元素,当用户点击时,执行了恶意代码(即点击劫持)
- Content-Security-Policy: default-src ‘none’
- 用于指定读取的HTML内元素指向的资源范围(例如:IMG, SCRIPT, LINK 等元素),用于降低 XSS 攻击的风险;
- Strict-Transport-Security: max-age=15768000
- 预先告知浏览器只能通过 HTTPS 进行访问
- max-age 值,即是告知浏览器,在这个时间期限内,只能使用 HTTPS 访问;
- 该首部只有通过 HTTPS 发放时才会生效,通过 HTTP 发放是不生效的;
- 缺点:第一次使用 HTTPS 访问前,如果已经通过 HTTP 访问过,则该首部不会产生效果
- Set-Cookie: session=e827ea…..3i1689jp; Path=/; Secure; HttpOnly
- Secure 表示 session 值只能在 HTTPS 下发送,HTTP 下不发送(避免泄露)
- HttpOnly 表示仅在 HTTP 通信方式下,才能读取该 session 值,不能通过其他方式读取,避免被恶意脚本获取 session 值;
- X-XSS-Protection: 1
- 应对大规模访问
- 限制每个用户的访问:
- 前提:需要能够识别用户
- 方法:给每个用户设置 Token 或 key 来识别;
- 设置访问次数的上限:时间单位需要考虑用户如果超出限制,需要等待多久;
- 当超出次数限制时,返回 429 状态码,Too many requests
- 告知用户详细的限制信息,包括:单位时间允许次数,剩余次数,重置时间;
- 方式一:通过专门的API,让用户主动查询
- 方式二:在返回的消息的首部注明,包括:
- X-RateLimit-Limit,单位时间的访问上限;
- X-RateLimit-Remaining,剩余的访问次数;
- X-RateLimit-Reset,访问次数重置的时间;
- 服务器端可使用 Redis 来记录限制信息;
- 限制每个用户的访问:
- 安全问题
- 附录
- 公开 Web API 的准备工作
- 提供文档,可参考 https://apiblueprint.org 倡导的规范;
- 提供沙盒,让用户可以在这个环境中随意测试;
- 提供 console,让用户可以通过浏览器对 API 进行操作测试;例如:https://developers.facebook.com/tools/explorer
- 提供 SDK,让开发人员能够快速上手使用,避免自己写代码进行连接的工作;缺点:如果 API 更新频率很高,则维护 SDK 的工作量也很大;
- 确认清单
- URI 是否短小且容易输入
- URI 是否能够让人一眼看懂;
- URI 是否只由小写字母组成;
- URI 是否容易修改;
- URI 是否反映了服务端的架构;
- URI 规则是否统一;
- 是否使用了合适的 HTTP 方法;
- URI 用到的单词是否和业界相同;
- URI 用到的名词是否使用了复数形式;
- URI 里面有没有空格及需要编码的字符;
- URI 单词之间是否使用了连字符;
- 分布的设计是否恰当;
- 登录是否使用了 OAuth 2.0
- 响应数据格式是否默认使用 JSON
- 是否支持通过查询参数指定数据格式;
- 是否支持不必要的 JSONP
- 响应数据的内容是否支持客户端指定;
- 响应数据是否存在不必要的封装;
- 响应数据的结构是否尽量扁平化;
- 响应数据是否使用对象描述,而非数组
- 响应数据使用的单词名称是否和业界一致;
- 响应数据的名称是否尽可能短小;
- 响应数据的多个单词是否使用了连字符;
- 响应数据名称是否使用了奇怪的缩写;
- 响应数据单词的单复数形式是否和内容一致;
- 出错时的响应数据是否包含有助于客户分析原因的信息;
- 出错时有没有返回 HTML 数据;
- 有没有返回合适的状态码;
- 服务器在维护时有没有返回 503 状态码;
- 有没有返回合适的媒体类型;
- 必要时是否支持 CORS
- 有没有返回 Cache-Control,ETag,Last-Modified, Vary 等首部信息,以便客户端制定合适的缓存策略;
- 不想缓存的数据是否指定 no-cache 或 no-store
- 有没有对 API 进行版本管理
- API 版本的命名是否遵守版本控制规范;
- 有没有在 URI 里嵌入主版本编号,并且能够让人一目了然;
- 有没有考虑 API 终止提供时的相关事项;
- 有没有在文档里注明 API 的最低提供期限;
- 有没有使用 HTTPS 提供 API
- 有没有执行 JSON 转义;
- 能不能识别 X-Requested-With 首部,让浏览器无法通过 Scrip 元素读取 JSON 数据;
- 通过浏览器访问的API 有没有使用 XSRF Token
- API 在接收参数时,有没有检查非法参数;
- 能否避免重复发送,导致数据多次更新;
- 有没有在响应消息里添加各种增强安全的首部;
- 有没有实施访问限速;
- 对预想的用例来说,限制的次数是否可能设置得过少;
- 公开 Web API 的准备工作
Web API 的设计和开发
https://ccw1078.github.io/2018/07/11/Web API 的设计和开发/