Node.js无状态/无Cookie会话API身份验证

12

我正在构建一个API,并正在尝试在多种情况下确定身份验证。

会话和密码认证

API需要为我们创建和部署的客户端应用提供服务,并使用密码处理已认证的请求。将密码与每个请求一起发送不是一个好主意,因此更合理的方法是首先访问登录端点并获取会话ID。相关的Web应用程序是使用AngularJS编写的,并应在localStorage中跟踪自己的会话,以减轻会话劫持并删除对cookie的会话跟踪依赖。

Web应用程序需要在每个请求中发送会话标识符,目前将其放在请求正文中。这很容易导致分段,并且与API紧密耦合。我更喜欢通过一种方式传递所有身份验证信息,最好是通过头部,而不是通过请求体、URL和头部中分散的多个不同字段。

会话存储

Redis 当然很棒。会话存储简单明了,自动进行垃圾回收。不幸的是,Redis中的会话管理很困难:我无法轻松地撤销特定用户的所有会话。通过将会话存储在真正的redis数据结构中而不是全局键空间中添加该功能会删除添加键入TTL的能力。我目前的解决方案是在MongoDB用户集合中存储会话列表,并在会话活动(例如登录/注销)时清除过期的会话。

使用connect-redis 模块将会话存储在redis中,但由于它们与每个请求一起发送,因此不包含在cookie中。目前,我有一个小型中间件,将会话标识符从请求正文中取出并将其放入req.cookies对象中。

var express = require('express');
var RedisStore = require('connect-redis')(require('connect'));

var app = express();
app.use(function(req, res, next) {
  req.cookies = {session: req.body.session};
});
app.use(express.session({
  store: new RedisStore({
    client: redisClient,
    prefix: 'session:'
  }),
  key: 'session',
  secret: 'all mine'
});

这种方法很好用,但是express.session会在Express响应时设置cookie,这不是期望的行为。

在Express的环境下,我该如何正确设置这个?

API密钥

我们的API还应支持API密钥,以便第三方应用程序获得对我们系统的有限和受控访问。我知道实现这一点的最常见机制是向有兴趣的开发人员分发API密钥,并要求开发人员将API密钥作为请求的一部分传递。这遇到了与会话/密码身份验证遇到的相同困境:每个API都希望在请求的不同部分(从正文到URL到标头)中接收API密钥。

扩展性和开放标准

虽然我们不打算在初始发布时支持OpenAuth和OpenID等开放认证标准,但我们希望创建一个框架,使添加此类标准变得简单明了。其中一部分可以是统一授权凭据交给API的方式,就像会话/密码和API密钥支持的身份验证一样。

另一个问题询问是否自定义HTTP Authorization标头是一个好主意,还是使用自定义标头更好。

CRUD支持

为了支持RESTful API的CRUD范例,不应在正文中提供身份验证信息,因为这将限制所有API请求为POST请求,而CRUD建议使用各种HTTP方法。

总结

两件事:

  1. 如何在不使用基于cookie的会话的情况下使用像connect-redis这样的模块。
  2. 我们应该如何配置身份验证信息以实现最大的灵活性和互操作性?

在每个请求中发送密码并不是一个好主意,但并非完全不可行。只要所有请求都是https,您可以信任TLS与密码并使用HTTP基本身份验证。因此,请求将类似于 https://username:password@yourdomain.com/api/path?query=string;您的路由处理程序将测试凭据并根据需要加载用户数据和/或会话数据。 - Plato
我完全可以做到。而且你说得对,TLS/SSL 应该足够安全。话虽如此,我不想在任何地方保存密码的时间超过必要的时间,并且我需要在 Web 应用程序中存储密码才能使用该方法。我更愿意将用户名/密码转换为本地可存储的会话令牌。这样,无论发生什么情况,在用户输入密码和服务器接收登录请求之间,密码在服务器以外的唯一潜在漏洞点。 - skeggse
此外,我正在尝试统一凭据的提供方式,将所有凭据嵌入URL中似乎过于限制,并且可能与OAuth等不兼容。 - skeggse
没错,那是个很合理的逻辑;我在一个移动应用中使用了基本身份验证技术,在设备上将密码存储在幕后,以确保它不会被泄露。 - Plato
在一些情况下,"相当确定" 是不够的。对于一个小型应用程序可能还可以理解,但是在我们的情况中,密码授予了访问大量个人信息和使用已保存信用卡进行购物的能力。谢谢您的建议,尽管我之前没有考虑过这个选项。 - skeggse
显示剩余3条评论
2个回答

8
给定问题无法回答。会话本身就是状态。因此,您不能无状态地实现会话。
如果您确实想要无状态身份验证,则无法使用会话,应该使用HTTP认证机制。
如果您确实想要会话,但不想在Cookie头中传输状态令牌,则应使用OAuth机制,该机制允许使用Authorization头或请求参数来保存协商的状态令牌。
我们希望创建一个框架,使添加这些标准变得简单。
最简单和最好的方法是从它们开始使用。不要重新发明轮子。OAuth2旨在在多种用例中轻松实现,并具有其自己的扩展机制,以便在需要时进行扩展。

您可以拥有类似于无状态会话的东西,类似于Harun的答案。该解决方案有时被称为客户端会话,并使用HMAC将时间戳和服务器密钥嵌入不透明的“会话”中。此外,“OAuth2旨在易于实现”的说法对我来说感觉有点奇怪。我的意思是,如果您有一个库,它确实是设计成易于实现的。如果您尝试使用OAuth2允许通过几个外部服务进行授权,则不一致可能会产生破坏性影响。 - skeggse
那不是OAuth2,那是OpenID。 - OrangeDog
不,OAuth并没有关于身份验证的建立方式的规定。这就是为什么OpenID作为OAuth之上的一层存在的原因。 - OrangeDog
OAuth可能并非为此而设计,但它可以(并已经)用于身份验证。 - skeggse
其实我认为我们在说同一件事。OpenID Connect 的扩展非常小,因此我将其视为 OAuth2 的一种“用途”。抱歉造成困惑。 - skeggse

2
你可以考虑使用基于令牌的方法来避免使用会话。
你可以生成一个加密的令牌,其中包含至少用户ID、过期日期和服务器密钥的哈希值。一旦在登录请求中验证了用户,你甚至不需要包括密码。
令牌将在“Authorization”头部中传递,避免使用cookie或任何其他请求参数。
有了这个令牌,你可以识别用户并检查到期日期,而无需在服务器端存储状态,因此不需要会话状态。只有当需要按需过期已验证的令牌时,才需要存储。

如果我仍在这个项目上工作,我想我的问题会是“Authorization头应该采用什么格式?” - skeggse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接