原文链接:https://www.sitepoint.com/rest-api/[1]
作者:https://www.sitepoint.com/author/craig-buckler/[2]
前言
什么是REST API?REST是「Representational State Transfer(表现层状态转移)」的缩写 - 对最常用的网络服务技术几乎是毫无意义的描述。REST API是两个计算机系统在web浏览器和服务器中使用HTTP技术进行通信的一种方式。
在两个或多个系统之间共享数据一直是软件开发的一个基本要求。比如说,考虑购买汽车保险。你的保险公司必须获得关于你和你的车辆的信息,所以他们要求从汽车登记机构、信贷机构、银行和其他系统获得数据。所有这些都是实时透明地发生的,以确定保险公司是否能提供一个有竞争力的保单。
API(应用程序接口)通过为系统之间的对话提供接口来帮助这种类型的通信。REST只是一种被广泛采纳的API风格,我们用它来与内部和外部以一种一致的和可预测的方式进行沟通。它可以比作我们以前寄信时用邮票、地址和信封的方式,以确保信件被送达和阅读。
REST是人们在web系统中常用的交互方式。例如,在一个社交媒体应用中检索和更新账户信息。
REST API示例
在你的浏览器中打开以下链接,从Open Trivia Database[3]中请求一个随机的计算机问题:
https://opentdb.com/api.php?amount=1&category=18[4]
这是一个作为RESTful网络服务实现的公共API(它遵循REST公约)。你的浏览器将展示一个单独的JSON格式的问答问题,并附有答案。比如说:
代码语言:javascript复制{
"response_code": 0,
"results": [
{
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "What does GHz stand for?",
"correct_answer": "Gigahertz",
"incorrect_answers": [
"Gigahotz",
"Gigahetz",
"Gigahatz"
]
}
]
}
你可以使用任意HTTP客户端,来请求同样的URL并得到响应,比如使用curl[5]:
代码语言:javascript复制curl "https://opentdb.com/api.php?amount=1&category=18"
HTTP客户端库可以在所有流行的语言和运行时中使用,包括JavaScript、Node.js和Deno中的Fetch[6]以及PHP中的file_get_contents()[7]。JSON响应是机器可读的,因此可以在输出HTML或其他格式之前被进行解析和使用。
REST APIs和Rest
多年来,各种数据通信标准已经发展起来。你可能遇到过的选择包括CORBA[8],SOAP[9],或者 XML-RPC[10]。大多数都确定了严格的消息传递规则。
REST是由Roy Fielding在2000年定义[11]的,比其他的要简单得多。它不是一个标准,而是一套关于RESTful网络服务的建议和约束。其中包括:
- 「客户服务器分离模式」(Client-Server):系统A向系统B托管的URL发出HTTP请求,并返回一个响应。这与浏览器的工作方式相同。浏览器对一个特定的URL发出请求,该请求被转发到一个web服务器,该服务器通常返回一个HTML页面。该页面可能包含对图片、样式表和JavaScript的引用,从而产生进一步的请求和响应。
- 「无状态」(Stateless):REST是无状态的:客户端请求应该包含响应所需的所有信息。换句话说,应该可以按照任何顺序发出两个或更多的HTTP请求,并且会收到相同的响应(除非API被设计为返回随机响应)。
- 「可缓存」(Cacheable):响应应该被定义为可缓存或不可缓存。缓存可以提高性能,因为没有必要为同一个URL重新生成一个响应。在某个时间段特定于某个用户的私人数据通常不会被缓存。
- 「分层」(Layered):请求的客户端不需要知道它是否在与实际的服务器、代理或任何其他中间人进行通信。
创建RESTful网络服务
一个RESTful网络服务请求包括:
- 「端点URL」。实现RESTful API的应用程序将定义一个或多个带有域名、端口、路径、和/或查询字符串的URL端点,例如,
https://mydomain/user/123?format=json
。 - 「HTTP方法」。不同的HTTP方法可以在任何端点上使用,这些方法映射到应用程序的创建、读取、更新和删除(CRUD)操作:
HTTP方法CRUD行为GET读取返回请求数据POST创建创建一个新记录PUT 或者 PATCH更新更新已存在的记录DELETE删除删除已存在的记录
比如:
- 对
/user/
的GET请求返回系统中的注册用户列表。 - 对
/user/
的POST请求使用body
对象创建了一个ID为123
的用户。该响应会返回ID。 - 对
/user/123
的PUT请求使用body
对象更新用户123
。 - 对
/user/123
的GET请求返回用户123
的详情。 - 对
/user/123
的DELETE请求删除用户123
。
- 对
- 「HTTP头部」。认证令牌或
cookies
等信息可以包含在HTTP请求头中。 - 「Body对象」。数据通常在HTTP主体中传输,该方式与HTML
<form>
提交或者发送单独的JSON编码的数据字符串等方式相同。
API-Request.png
REST API响应
响应的有效负载可以是任何实用的东西:数据、HTML、图像、音频文件等等。数据响应通常是JSON编码,但也可以使用XML,CSV,简单字符串或任何其他格式。你可以允许在请求中指定返回格式。比如说,/user/123?format=json
或者 /user/123?format=xml
。
还应该在响应头中设置适当的HTTP状态码[12]。200 OK
用于成功的请求,尽管当记录被创建时也可以返回201 Created
。当发生错误时应该返回适当的状态码,比如说400 Bad Request
,404 Not Found
,401 Unauthorized
等等。
其他HTTP头部可以被设置包括Cache-Control
或Expires
,以指定响应在被视为过期之前可以缓存多长时间。
然而,并没有严格的规则。端点URL、HTTP方法、body对象和响应类型可以随心所欲地实现。例如,POST、PUT和PATCH通常可以互换使用,如有必要任何一个都可以用来创建或更新记录。
Hello World示例
下面的Node.js
代码使用Express
框架创建了一个RESTful网络服务。一个单一的/hello/
端点对HTTP GET请求作出响应。
确保已安装Node.js
,创建名为restapi
的新文件夹。在该文件夹中创建一个新的package.json
文件,内容如下:
{
"name": "restapi",
"version": "1.0.0",
"description": "REST test",
"scripts": {
"start": "node ./index.js"
},
"dependencies": {
"express": "4.18.1"
}
}
在命令行中运行npm install
来拉取依赖,然后创建index.js
文件,包括以下代码:
// simple Express.js RESTful API
'use strict';
// initialize
const
port = 8888,
express = require('express'),
app = express();
// /hello/ GET request
app.get('/hello/:name?', (req, res) =>
res.json(
{ message: `Hello ${req.params.name || 'world'}!` }
)
);
// start server
app.listen(port, () =>
console.log(`Server started on port ${port}`);
);
使用npm start
从命令行启动该应用程序,并在浏览器中打开http://localhost:8888/hello/
。响应GET请求时,会显示以下JSON:
{
"message": "Hello world!"
}
API也允许自定义名字,因此http://localhost:8888/hello/everyone/
会返回:
{
"message": "Hello everyone!"
}
客户端REST请求和CORS
考虑在浏览器中启动以下HTML页面,URL是http://localhost:8888/
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>REST test</title>
</head>
<body>
<script>
fetch('http://localhost:8888/hello/')
.then((response) => {
return response.json();
})
.then((json) => {
console.log(json);
});
</script>
</body>
</html>
fetch
调用发出同样的API请求,浏览器控制台显示{ message: "Hello world!" }
,正如你所期望的那样。
然而,假设你的RESTful网络服务现在被放在网络上,域名是http://mydomain.com/hello/
。页面的JavaScript fetch()
URL也相应地改变了,但在浏览器中打开http://localhost:8888/
,现在会返回控制台错误「Cross-Origin Request Blocked」。
为了安全起见,浏览器只允许客户端的XMLHttpRequest
和Fetch API
调用页面所在的同域请求。
幸运的是,跨源资源共享[13](CORS)使我们能够规避这一安全限制。设置Access-Control-Allow-Origin
HTTP响应头来告诉浏览器允许该请求。它可以设置为一个特定的域,或者设置为所有的域*
。
可以更改网络服务器API代码,以允许运行在任何域名的任何客户端脚本进行访问:
代码语言:javascript复制// /hello/ GET request
app.get('/hello/:name?', (req, res) =>
res
.append('Access-Control-Allow-Origin', '*')
.json(
{ message: `Hello ${req.params.name || 'world'}!` }
)
);
或者,Express.js
中间件函数可以将头部附加到每个端点请求:
// enable CORS
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', '*');
next();
});
// /hello/ GET request
// ...
注意,浏览器向REST API发出两个请求:
- 对同一URL的HTTP
OPTIONS
请求确定Access-Control-Allow-Origin
HTTP响应头是否有效。 - 实际的REST调用。
当你的服务器收到一个OPTIONS
请求方法时,它可以设置Access-Control-Allow-Origin
HTTP响应头返回一个假的空响应,以确保工作不被重复。
REST API挑战
REST的成功很大程度上归功于它的简单性。开发人员可以自由地实现RESTful API,但这可能会导致进一步的挑战。要深入了解实现策略,请看13个构建RESTful API的最佳实践[14]。
端点共识
考虑下面的端点:
/user/123
/user/id/123
/user/?id=123
所有这些都是为用户123
获取数据的有效选项。当你有更复杂的操作时,组合的数量会进一步增加。
归根结底,你如何格式化URL并不重要,但整个API的一致性很重要。这对有许多开发人员的大型代码库来说是个挑战。
REST API版本控制
API的变化是不可避免的,但端点的URL永远不应该失效,否则会破坏使用它们的应用程序。
为了避免兼容性问题,API通常是有版本的。例如,/2.0/user/123
取代了/user/123
。新的和旧的端点都可以保持活跃。不幸的是,这样就有必要维护多个历史API。旧的版本最终可以被废弃,但整个过程需要仔细规划。
REST API认证
上面显示的测试API是开放的:任何系统都可以在未经授权的情况下获取数据。这对于访问私有数据或允许更新和删除请求的API是不可行的。
与RESTful API处于同域的客户端应用程序将像其他HTTP请求一样发送和接收cookies
。(请注意,旧版浏览器中的Fetch()
需要设置credentials
初始选项)。因此,一个API请求可以被验证,以确保一个用户已经登录并拥有适当的权限。
第三方应用程序必须使用替代的授权方法。常见的认证选项[15]包括:
- HTTP基本身份验证[16]。在请求头中传递一个包含base64编码的
username:password
字符串的 HTTPAuthorization
头。因为base64很容易被解码,基本(Basic)认证应该只和其他安全机制一起使用,比如HTTPS/SSL。 - API密钥[17]。第三方应用程序通过发布一个密钥来获得使用API的许可,这个密钥可能有特定的权限或被限制在一个特定的域。密钥在每个请求中的HTTP头或查询字符串中被传递。
- OAuth[18]。在发出任何请求之前,通过向OAuth服务器发送一个客户ID和可能的客户秘密,获得一个令牌。然后,OAuth令牌会随每个API请求一起发送,直到过期。
- JSON Web Tokens (JWT)[19]。数字签名的认证令牌在请求和响应头中安全地传输。JWT允许服务器对访问权限进行编码,因此不需要调用数据库或其他授权系统。
API身份验证将根据使用上下文而有所不同:
- 在某些情况下,第三方应用程序被视为像任何其他具有特定权利和权限的登录用户。例如,一个地图API可以将两点之间的方向返回给调用的应用程序。它必须确认该应用程序是一个有效的客户端,但不需要检查用户凭证。
- 在其他情况下,第三方应用程序正在请求用户的私有数据,如电子邮件内容。REST API必须识别用户和他们的权利,但它可能不关心哪个应用程序在调用API。
REST API安全性
RESTful API提供了另一种访问和操作你的应用程序的途径。即使它不是一个引人注目的黑客目标,一个行为不良的客户端也可能每秒发送数以千计的请求,并使你的服务器崩溃。
安全性超出了本文的范围,但常见的最佳实践包括:
- 使用HTTPS。
- 使用健壮的身份验证方法。
- 使用CORS来限制客户端对特定域的调用。
- 提供最少的功能,也就是不要创建不需要的
DELETE
选项。 - 验证所有端点URL和body对象。
- 避免在客户端JavaScript中暴露API令牌。
- 阻止来自未知域名或IP地址的访问。
- 阻止意外的大型有效负载。
- 考虑速率限制,也就是使用同一API令牌或IP地址的请求被限制在每分钟N个以内。
- 以适当的HTTP状态代码和缓存头进行响应。
- 记录请求并调查失败情况。
多个请求和不必要的数据
RESTful APIs受到其实现的限制。响应可能包含比你需要的更多的数据,或者需要进一步的请求来访问所有数据。
考虑一个RESTful API,它提供对作者和书籍数据的访问。为了显示前10名畅销书的数据,客户端可以:
- 请求按销售量数量订购的前10
/book/
的详细信息(最畅销的在前)。响应包含有每个作者ID的书籍列表。 - 最多组成10个
/author/{id}
请求以获取每个作者的详细信息。
这被称为「N 1问题」;必须为父请求中的每个结果提出N个API请求。
为了避免不必要的大的响应,可以调整API使作者的细节是可选的。例如,?author_details=full
。API作者需要满足的选项的数量可能会变得令人困惑。
GraphQL是否更好?
REST的难题导致Facebook创建了GraphQL--一种网络服务查询语言。把它看作是网络服务的SQL:一个单一的请求定义了你所需要的数据以及你希望它如何返回。
GraphQL解决了RESTful APIs带来的一些挑战,尽管它引入了其他挑战。例如,很难对GraphQL响应进行缓存。
你的客户不太可能有与Facebook类似的问题,所以一旦RESTful API发展到超出其实际限制时,GraphQL可能值得考虑。
REST API链接和开发工具
有许多工具可以帮助所有语言的RESTful API开发。值得注意的选项包括:
- Swagger[20]:帮助设计、记录、模拟、测试和监控REST APIs的各种工具。
- Postman[21]:一个RESTful API测试应用程序。
- Hoppscotch[22]:一个开源的、基于web的Postman替代品。
还有大量的公共REST API,集合了笑话、货币转换、地理编码、政府数据以及你能想到的每一个主题。许多是免费的,尽管有些需要你注册一个API密钥或使用其他认证方法。分类列表包括:
- Any API[23]
- API list[24]
- Public APIs[25]
- Google APIs Explorer[26]
在实现你自己的网络服务之前,在你自己的项目中尝试使用一些RESTful API。或者考虑跟随Facebook、GitHub、Google和其他许多巨头的脚步,建立一个属于自己的RESTful API。
下一篇文章中,会翻译分享13个构建RESTful API的最佳实践[27],欢迎关注。
以上就是全部内容,如果对你有所帮助,欢迎点赞收藏转发~
参考资料
[1]
https://www.sitepoint.com/rest-api/: https://www.sitepoint.com/rest-api/
[2]
https://www.sitepoint.com/author/craig-buckler/: https://www.sitepoint.com/author/craig-buckler/
[3]
Open Trivia Database: https://opentdb.com/
[4]
https://opentdb.com/api.php?amount=1&category=18: https://opentdb.com/api.php?amount=1&category=18
[5]
curl: https://curl.se/
[6]
Fetch: https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API
[7]
file_get_contents(): https://www.php.net/manual/en/function.file-get-contents.php
[8]
CORBA: https://www.corba.org/
[9]
SOAP: https://en.wikipedia.org/wiki/SOAP
[10]
XML-RPC: http://xmlrpc.com/
[11]
REST是由Roy Fielding在2000年定义: https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style
[12]
HTTP状态码: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
[13]
跨源资源共享: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
[14]
13个构建RESTful API的最佳实践: https://www.sitepoint.com/build-restful-apis-best-practices/
[15]
认证选项: https://swagger.io/docs/specification/authentication/
[16]
HTTP基本身份验证: https://swagger.io/docs/specification/authentication/basic-authentication/
[17]
API密钥: https://swagger.io/docs/specification/authentication/api-keys/
[18]
OAuth: https://oauth.net/
[19]
JSON Web Tokens (JWT): https://jwt.io/
[20]
Swagger: https://swagger.io/
[21]
Postman: https://www.getpostman.com/downloads/
[22]
Hoppscotch: https://hoppscotch.io/
[23]
Any API: https://any-api.com/
[24]
API list: https://apilist.fun/
[25]
Public APIs: https://github.com/public-apis/public-apis
[26]
Google APIs Explorer: https://developers.google.com/apis-explorer/
[27]
13个构建RESTful API的最佳实践: https://www.sitepoint.com/build-restful-apis-best-practices/