REST API 安全最佳实践:鉴权和授权
原文标题:Best practices for REST API security: Authentication and authorization
原文链接:https://stackoverflow.blog/2021/10/06/best-practices-for-authentication-and-authorization-for-rest-apis/
作者:Sam Scott and Graham Neray
大多数使用现代 Web 框架的应用程序都会有一个或多个 REST API 。 REST 是构建 Web API 的 一种简单而灵活的方法。它不是标准或协议,而是一组架构上的约束。
REST API 的三个使用场景:
1. 为客户端(例如浏览器中的单页应用或手机上的 App)提供服务端数据访问权限。
2. 让最终用户(不论人还是程序)以代码的方式访问由应用程序管理的数据。
3. 让许多在应用程序内作为基础设施的服务彼此之间能够互相通信。
但是出于以上目的而构建的 API 却有可能被心怀恶意或者粗心鲁莽的人滥用。因此你需要给你的应用程序制定一个访问策略——谁可以查看或修改你服务器上的数据?例如,只有博客文章的作者能够编辑它,读者应该只能查看它。设想如果任何人都可以编辑你现在正在读的这个帖子,那么这篇文章就会被破坏,增加垃圾索引,或者被随意更改和删除内容。
为你的应用程序定义访问策略的过程称为授权。 在本文中,我们将向你展示在 REST API 中实现授权的最佳实践。
始终使用 TLS
Web API 应该使用 TLS(Transport Layer Security)来保护 API 请求和响应中包含的信息。TLS 可在传输过程中对信息进行加密。TLS 的前身是 SSL,你可以通过 SSL 来了解 TLS。如果网站启用了 TLS,那么网站的 URL 会以 https:// 而不是http://开头。
如果没有采用 TLS,那么其他人就可以拦截和读取传输中的敏感信息,例如 API 凭据和私密数据。这样一来,你采用的任何身份验证措施效果都会减弱。
TLS 需要证书授权中心颁发证书才能使用,这也可以让你的用户知道你的API 是合法并且受保护的。大多数云提供商和托管服务将管理你的证书并为你启用 TLS。 如果你在 Heroku 上托管网站,只需单击一个按钮即可启用 TLS。 如果你在 AWS 上托管,AWS Certificate Manager 结合 AWS Cloudfront 将为你服务。 如果可以的话,你的主机也可以管理你的TLS 证书,这样一来,不仅不麻烦,每次调用API 还会自动受到保护。
如果你在没有任何第三方服务的情况下运行自己的 Web 服务器,则必须管理自己的证书。 最简单的方法是使用自动证书颁发机构 Let's Encrypt。
使用 OAuth2 通过 OpenID Connect 进行单点登录 (SSO)
几乎所有的应用程序都需要将用户和私密数据链接起来。换句话说就是登录和退出用户账号。以前你可能会自己编写登录代码,但现在有一种更加简单的方法:将 OAuth2 和现有的单点登录(SSO)结合起来。
SSO 允许你的用户通过令牌交换向受信任的第三方(如 Google、Microsoft Azure 或 AWS)验证自己以获得对资源的访问权限。 例如,用户将登录到他(她)们的 Google 帐户,并被授予访问你的应用程序的权限。
使用 SSO 意味着:
- 你不必自己管理密码! 这减少了你存储的用户数据,因此在发生数据泄露时暴露的数据也更少。
- 你不仅不必实现登录和注销,而且还不必采用多因素认证。
- 你的用户不需要新帐户和新密码——在 Google 等 SSO 提供商那,用户已经拥有了一个帐户。 减少注册时的摩擦意味着留住更多用户。
OAuth2 是描述第三方应用程序如何代表用户从应用程序访问数据的标准。 OAuth2 不直接处理身份验证,而是一个主要为授权而构建的更通用的框架。 例如,用户可能会授予应用程序访问权限以查看日历以便为你安排会议。 这将涉及用户、日历提供者和日程安排应用程序之间的 OAuth2 交互。
在上面的例子中,OAuth2 提供了协调三方的机制。 日程应用想要获取 access token 以便从日历应用中获取日历数据。它将用户信息通过特定的 URL 发送出去,这个 URL 的请求参数已编码。日历程序申请用户同意这次访问,接着携带着授权码重定向回日程应用。这个授权码可以交换 access token 。
你可以通过获取唯一标识用户的信息(如电子邮件地址)在 OAuth2 的基础上实施身份验证。不过,你应该更偏向于使用 OpenID Connect 。OpenID Connect 规范建立在 OAuth2 之上,提供协议用于验证用户身份。遗憾的是,并非每个第三方登录授权厂商都支持 OpenID Connect。 例如,GitHub 不会让你使用 OpenID Connect。 在这种情况下,你必须自己处理 OAuth2。 但好消息是,有一个适合你选择的编程语言的 OAuth2 库和大量好的文档!
OAuth 技巧
你可以在无状态或有状态模式下使用 OAuth2。
简单的来说:正确实现有状态后端来处理 OAuth 流程通常更容易 ,因为你可以在服务器上处理更多的敏感数据并避免泄露凭据的风险。 但是,REST API 应该是无状态的。 因此,如果你想以这种方式保留后端,则需要使用无状态方法或添加额外的有状态服务器来处理身份验证。
如果你选择实施无状态方法,请确保使用其代码交换模式的证明密钥,以防止跨站点请求伪造和代码注入攻击。
你需要存储用户的 OAuth 凭据。 不要将它们放在本地存储中 —— 页面上运行的任何 JavaScript 都可以访问它们! 相反,将令牌存储为安全 cookie,可防止跨站点脚本 (XSS) 攻击。 但是,cookie 可能容易受到跨站点请求伪造 (CSRF) 的攻击,因此你应该确保你的 cookie 使用 SameSite =Strict .
使用 API 密钥为现有用户提供编程访问权限
虽然你的 REST 端点可以为你自己的网站提供服务,但 REST 的一大优势是它为其他程序提供了一种与你的服务交互的标准方式。 为了简单起见,不要让你的用户在本地执行 OAuth2 或提供用户名/密码组合 —— 这首先会破坏使用 OAuth2 进行身份验证的意义。 相反,为你自己和你的用户保持简单,发布并使用 API 密钥。
API 密钥使用步骤:
- 当用户注册访问你的 API 时,生成一个 API 密钥:
var token = crypto.randomBytes(32).toString('hex');
- 将其存储在与你的用户关联的数据库中。
- 谨慎和用户分享这些数据,确保尽可能隐藏它。 例如,在重新生成之前,你可能只想显示一次。
- 让你的用户提供 API 密钥作为 header,例如
curl -H "Authorization: apikey MY_APP_API_KEY" https://myapp.example.com
- 要验证用户的 API 请求,请在数据库中查找对应的 API 密钥。当用户生成一个 API 密钥时,让他(她)们给该密钥一个标签或名称以供他(她)们自己记录。以便以后能删除或重新生成这些密钥,借此可以从受损的凭据中恢复数据。
鼓励有效保管 API 密钥
保护自己的秘密是用户的责任,但你也可以提供帮助! 通过编写良好的示例代码来鼓励你的用户遵循最佳实践。 展示 API 示例时,请使用环境变量展示你的示例,例如 ENV["MY_APP_API_KEY"]
.
from requests.auth import HTTPBasicAuth
import requests
import os
api_key = os.environ.get("MY_APP_API_KEY")
auth = HTTPBasicAuth('apikey', api_key)
req = requests.get("<https://myapp.example.com>,
headers={'Accept': 'application/json'},
auth=auth)
(如果你像 Stripe 一样编写包含某人 API 密钥的交互式教程,请确保它是测试环境的密钥,而不是生产环境的密钥。)
使用请求级授权来按需授权
我们一直在谈论 API 授权,好像它适用于每个请求,其实不一定都需要。 您可能想要添加请求级授权:查看传入请求以确定用户是否可以访问您的资源。 这样,您可以让每个人都看到 /public/ 中的资源,或者选择用户需要经过身份验证才能发出的某些类型的请求。最好的方法是使用请求中间件。
为不同的 API 密钥配置不同的权限
出于各种原因,你将授予用户程序化的 API 访问权限。 一些 API 端点可能用于脚本访问,一些用于仪表盘等等。 由于并非每个端点都需要用户的完整帐户访问权限,因此可以考虑使用多个具有不同权限级别的 API 密钥。为此,将权限与 API 密钥一起作为字符串列表存储在数据库中。 首先保持简单:从限制“读”和“写”权限开始 ,然后添加一个请求中间件,用于获取用户和用户提交的密钥的权限,并根据 API 检查令牌权限。
将其余授权留给应用程序/业务逻辑
现在你已经开始向你的 API 添加授权模块,可能想添加越来越多的逻辑来处理更多的检查。 你最终可能会针对每个资源和权限级别使用嵌套的 if 语句。 这样做会导致你最终可能会复制应用程序逻辑。 你会发现自己在中间件中获取数据库记录,这并不理想的处理方式。
相反,让应用程序代码来处理这个层级的授权逻辑。任何对资源访问的授权检查都应该在应用程序中处理,而不是在中间件。
如果你需要在应用程序中处理复杂的授权逻辑,可以使用像 Oso 这样的工具,它们会把你的授权策略简化到只剩几条简单的规则。
授权和鉴权还有很多可以讨论的,但这些内容对入门来说已经足够了!我们希望这些 tips 能帮助你设计有用而安全的 API 端点。
总结:使用好的库
我们已经给了很多具体的建议,但所有这些都归于一个点 —— 尽可能多把处理工作抛给信任的库来处理。授权是一件棘手的事情,我们需要尽量减少可能出错的地方。 其实你有很多很棒的工具可以帮助你进行授权,因此请尽可能充分利用它们!