自定义 REST 服务的 API 身份验证最佳实践
Posted
技术标签:
【中文标题】自定义 REST 服务的 API 身份验证最佳实践【英文标题】:API Authentication best practices for custom REST service 【发布时间】:2021-05-24 17:46:55 【问题描述】:我会尽量保持它与框架无关,但我会参考我个人使用的框架来提供一些上下文。另外,很抱歉,这是一个很长的问题。
上下文:我正在开发一个在线平台。它由两个不同的服务器组成,一个为公共 REST(ish) API 提供服务的“后端”(比如说 api.example.com),一个使用 Sapper (Svelte) 和 s-s-r (example.com) 的“前端” .后者基本上服务于一个 API 客户端。
显然,某些端点与特定帐户相关,因此需要身份验证。据我所知,有两种不同的方法,它们具有不同的漏洞:
会话身份验证 (cookie),易受 CSRF 攻击 token认证(header),易受XSS攻击(一般token保存在localStorage
)
直接在后端服务器上使用会话身份验证非常困难,因为该页面由另一台服务器提供服务,并且生成/验证令牌不是那么容易,如果不是不可能的话。
常见解决方案:
人们似乎对使用localStorage
感到非常焦虑,我明白了,因为攻击者可以使用 XSS 窃取令牌并冒充用户。因此the preferred solution seems to be如下:
问题是,由于现在使用 cookie 进行身份验证(至少在“前端”级别),CSRF 攻击又是一个问题。当然,现在处理起来更容易了,因为这都是前端服务器的责任,并且已经有existing CSRF middlewares表示express和polka。
但是,我的观点是,CSRF 令牌和 CSRF 保护通常在存在 XSS 漏洞的情况下是无用的,因此,即使 JS 无法直接访问令牌,该平台仍然存在漏洞。 XSS 保护仍然很重要,我们这样做没有任何收获。另一方面,现在前端服务器对每个后端请求都“忙碌”,这破坏了拥有不同服务器的优势之一。
我想做的基本上是坚持localStorage
的方法,但没有实际使用localStorage
,以利用s-s-r。这个想法是我可以使用 Sapper 中间件来读取 cookie 并将令牌存储在 Sapper 会话中:
sapper.middleware(
session: (req, res) => (
authorization: getCookie(req, 'supersecret') // get supersecret cookie from request
)
)
对于那些不了解 Sapper 的人来说,session
在这种情况下基本上是一块由服务器初始化并与客户端共享的内存。制作 cookie httpOnly 并没有真正发挥作用,因为 JS 仍然可以使用会话存储访问该值。但是由于服务器现在可以在页面重新加载期间访问令牌,我们现在可以利用 s-s-r(在服务器上呈现从 API 获得的私有信息有多大用处是值得商榷的,但为什么不呢?)。
问题是:这显然不是标准方法,因此我不明白它可能导致(或已经存在)的问题,因为我似乎找不到任何缺点对此。我在想什么/做错了什么?这真的可以吗?
PS:如果我想使用 API 密钥并限制 API 使用,“前端代理”是强制性的,但我认为我真的不需要它(如果我错了,请纠正我)
【问题讨论】:
有什么好处?也许只有我一个人,但不清楚你为什么要这样做。我最初的想法是安全方面的,从客户端的角度来看,这与将其存储在类似 localstorage 的东西中是相同的,但是具有额外的依赖项和更大的攻击面,更多潜在的配置错误和更复杂的一般性,这对安全性不利。 @GaborLengyel 使用 cookie 允许我将授权令牌传递给服务器以从 API 检索信息并使用服务器端呈现 这种方法比将令牌存储在 localStorage 中并将其作为标头添加到每个请求中如何更好? 仅供参考,看看 Auth0 如何在浏览器中存储令牌 github.com/auth0/auth0-spa-js/blob/master/src/Auth0Client.ts @pianka 如果在这里失败,请尝试在security.stackexchange.com 上询问。 【参考方案1】:所以你真正想做的是:
进行无状态身份验证和授权以提高可扩展性
您需要将会话数据存储在客户端上。您需要随每个请求发送会话数据的相关部分。您需要在会话数据中添加一个难以猜测的秘密,以用于验证用户身份。您需要在服务器上为每个请求检查此密钥。
避免通过 XSS 窃取会话
您至少需要将您的秘密移动到仅 HTTP 的 cookie 中,这样没有人可以使用客户端上运行的 javascript 代码窃取它。
避免通过 CSRF 使用会话
您需要另一个难以猜测的秘密,可用于验证用户身份或对于实际会话是唯一的。这个秘密可以被客户端上运行的 javascript 代码知道,并且必须以非 cookie 的方式随每个请求一起发送。您需要在服务器上为每个请求检查此密钥,但如果运行客户端 JavaScript 代码是使用您的应用程序的要求,则无需在服务器上生成它。
实现这些的一种可能方法是将整个会话移动到由服务器创建和修改的加密签名 JWT,并以仅 HTTP cookie 的形式存储在客户端上。您可以将用户 ID、会话 ID 和到期日期添加到此会话。您验证每个请求的签名,并检查会话 ID 是否不在黑名单上或会话是否未过期。您定期更新用于加密和签署 JWT 的密钥,例如每一个月。您也在 HTTP 标头中发送加密的会话 ID,并将其与您在 JWT 中为每个请求获取的 ID 进行比较。
一个不太安全且从服务器角度来看要求不高的实现是仅将用户 ID、到期日期和会话 ID 存储在仅签名但未加密的令牌中。会话的其余部分可以存储在本地存储或会话存储中,并且可以由客户端读取和修改,而不仅仅是服务器。
一种更不安全的方法是在每个请求中只发送电子邮件和密码,而不是签名。
请注意,这只是我的意见,而不是某些行业标准,我建议使用现有的经过良好测试的产品,而不是使用您的自定义安全解决方案。
【讨论】:
这是非常标准的方法,基本上你可以用 php 做的,但是用令牌而不是会话 cookie。让我失望的是后端是单独服务的,我想将 s-s-r 与前端服务器一起使用......要么我想要太多东西,要么我错过了一些东西。 (电子邮件和密码显然是不可能的) @pianka 我认为电子邮件和密码也可以,这实际上取决于您的应用程序需要什么样的安全性,被盗帐户会造成多大的损害。人们通常没有意识到,但网络安全与编程是不同的工作。如果这是一个严肃的应用程序,那么您最好请专家从安全角度帮助设计它。以良好的安全性设计它比稍后尝试修复它要容易得多。例如,通用标准可以帮助设计安全的应用程序,但需要时间来学习。 en.wikipedia.org/wiki/Common_Criteria【参考方案2】:LocalStorage(或 MemoryStorage,如果您愿意的话)从来都不是用于生产目的的好解决方案。如果您只是在做演示或展示更改,那么您应该坚持使用 LocalStorage 内存缓存。将 Redis 用于专用缓存服务。
您是否希望使用基于令牌的系统或基于会话的系统来确保服务器-客户端之间的一致性取决于您。您可以在请求中采用特定参数以确保更好的身份(IP 位置、更短的到期时间等)
设置 https 证书以降低 XSS 攻击的效力。利用服务器生成的令牌确保基于会话的 CSRF 攻击更难执行(生成的令牌可以是非对称或对称类型)。
【讨论】:
我已经在考虑跟踪 IP 地址以尝试检测奇怪的行为,并希望发现被盗的令牌以撤销它们。以我目前的方法,CSRF 预防是不必要的,不是吗? 旧版浏览器甚至允许您捕获笔记本电脑的 MAC 地址和电池 ID。继续寻找可用于良好身份分类的不可变信息。 (加密您存储为 cookie 的任何令牌。使用至少 sha512 级别的加密)【参考方案3】:如有疑问,请查看专家如何处理问题(除非您的方法很新颖)。例如,银行具有严格的安全性和较短的会话寿命,因此用户必须每 x 秒重新登录一次,因此即使攻击者窃取了第一个会话密钥,在您重新验证后他也不会拥有新会话(假设他没有没有一种有争议的方法来窃取新的会话密钥)它增加了一个障碍,但不是最终的防御,因为它不存在。 也许您还可以查看使用私钥和公钥对或唯一代码生成器的非对称加密,前端必须始终根据前端和后端都拥有的加密密钥生成新的密码短语,并且您必须发送它每个请求的每一个进化密码,攻击者将无法通过这种安全措施,除非他可以访问前端代码,这同样不是最终的防御,而是攻击者的额外障碍。一种更好的方法是使用机器学习来识别典型的用户行为模式并标记异常活动,并在当前活动会话中的置信水平低时发送警报/重新验证。
【讨论】:
我真的不明白这如何适用于我的问题...:\以上是关于自定义 REST 服务的 API 身份验证最佳实践的主要内容,如果未能解决你的问题,请参考以下文章