SPA和SSO(单点登录)中无状态身份验证的性能



如果我有一个SPA(单页应用程序-使用BackboneJS开发),并且希望为其数据提供一个无状态的RESTful后端API。我喜欢第三方单一登录如何让用户的事情变得如此简单,因此会喜欢它使用它。

但我知道,在这样的无状态环境中,身份验证是对每个请求进行的吗?如果是这样的话,如果我使用第三方SSO,例如GitHub,我不需要每次都转到GitHub来验证用户吗?对于这种情况,最好的做法是什么?我相信这是一个非常常见的用例?-我允许用户通过Google/GitHub或其他方式登录,然后从一些无状态REST API 获取数据

免责声明:)

在我的产品中实现了这样一件事,并分享了您的许多关注点和技术(尤其是使用100%无状态REST后端的Backbone SPA)后,我可以告诉您我对此有什么想法,明确表示这不想成为"答案",而是从由此产生的讨论中学习的对话起点,因为我认为我在这个主题上也有很多东西要学习。


首先,我认为你应该100%无国籍。100%,我的意思是100%:)不仅API层应该是无状态的,整个应用程序(当然,除了客户端)也应该是无国籍的。将会话转移到另一层(例如redis)只是稍微移动了一点问题,但并不能解决它。一切(尤其是扩展)都会变得容易得多,你稍后会为这个决定感谢自己。

因此,是的,您需要对每个请求进行身份验证。但这并不意味着每次都必须访问身份验证提供程序。我学到的一件事是,允许用户通过FB/GitHub/Whatever(从现在起,远程服务)进行身份验证只是为了减轻注册/登录的痛苦,而不是别的。你仍然需要扩大你的个人用户数据库。当然,每个用户都将与一个"远程"用户相关联,但在执行身份验证后不久,应用程序应该引用"您的"用户,而不是"远程"的用户(例如GitHub用户)。

实施

以下是我实现的:

  1. 我的API方法总是需要身份验证令牌。auth令牌是一个散列,表示我的系统的用户,因此当我调用POST /api/board?name=[a_name]&auth=[my_token]时,我知道谁在调用,可以检查权限,并可以将新创建的实体board与正确的用户关联。

  2. 所述令牌与远程服务的令牌无关。计算它们的逻辑特定于我的应用程序。但它映射了我的一个用户,也映射到了一个远程用户,所以在需要的情况下不会丢失任何信息。

  3. 以下是我如何通过远程服务对用户进行身份验证。我按照服务文档中的规定实现了远程身份验证。通常是OAuth或类似OAuth,这意味着最终我会得到一个代表远程用户的authToken。此代币有两个用途:

    • 我可以使用它在作为用户的远程服务上调用API方法
    • 我可以保证用户是它所说的那个人,至少通过远程服务
  4. 一旦用户通过远程服务进行了身份验证,就可以在系统中加载或创建匹配的用户。如果您的系统中不存在具有remote_id: GitHub_abc123的用户,则创建该用户,否则加载该用户。假设该用户具有id: MyApp_def456。您还可以使用自己的逻辑创建一个authToken,该逻辑将代表用户MyApp_def456并将其传递给客户端(cookie可以!!)

  5. 回到第1点:)


备注

身份验证是在每个请求时执行的,这意味着你要处理哈希和加密函数,根据定义,这些函数很慢。现在,如果您使用20次迭代的bcrypt,这将扼杀您的应用程序。我用它来存储用户登录时的密码,但随后对authToken使用了一个不那么重的算法(我个人使用的哈希与SHA-256相当)。这些代币的使用寿命可能很短(比如说比破解它们的平均时间更短),并且在服务器机器上计算起来相当容易。这里没有确切的答案。尝试不同的方法,衡量并做出决定。相反,我确信的是,我更喜欢这种问题,而不是会话问题。如果我需要计算更多的散列,或者更快,我会增加CPU能力。对于会话和集群环境,您会遇到内存问题、负载平衡和粘性会话问题,或者其他移动部件(redis)。

显然,HTTPS是绝对强制性的,因为authToken总是作为参数传递。

实现它的方法是在客户端(主干网)和RESTful Web服务器之间引入代理。代理与SSO一起管理用户的身份验证。因此,无需更改api和/或客户端/webserver。这里有一个快速演示:

var http = require('http'),
    httpProxy = require('http-proxy'),
    express = require('express');
var proxy = new httpProxy.RoutingProxy();
var app = express();
function ensureAuthenticated(req, res, next) {
  if (isLoggedIn) { return next(); }
  res.redirect('/');
}
// This should be your (RESTful) webserver
http.createServer(function (req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('request successfully proxied!' + 'n' + JSON.stringify(req.headers, true, 2));
  res.end();
}).listen(9000);
var isLoggedIn = false;
app.get('/', function(req, res){
  console.log(isLoggedIn)
  res.send('Logged in? ' + isLoggedIn);
});
app.get('/login', function(req, res){
  isLoggedIn = true;
  res.redirect('/');
});
app.get('/logout', function(req, res){
  isLoggedIn = false;
  res.redirect('/');
});
app.all('/api/*', ensureAuthenticated, function(req, res) {
  return proxy.proxyRequest(req, res, {
    host: 'localhost',
    port: 9000
  });
});
app.listen(8000);

第一次访问该页面时,您已注销,对/api/something的任何呼叫都会重定向到/。登录后(访问/login页面),所有对/api/*的请求都会通过代理路由到侦听端口9000的Web服务器。

特别是,当您设置app.all('/*', ...)时,对API服务器的所有调用都保持不变,但增加了身份验证层。使用oauth扩展这个概念是微不足道的(如果您使用node,请查看passportjs)。

您可以采用Facebook的JavaScript SDK中使用的方法。

此文档页面提供了登录Facebook for web的快速入门。不是很深入,但解释了如何使用他们的方法的基本原理。但是,没有提到签字的可能性。

为应用程序注册Facebook登录时,您会从Facebook上的应用程序仪表板中获得应用程序机密。

当用户通过Facebook登录到您的应用程序时,您的JavaScript将获得一个身份验证对象。此对象包含一个签名。(如果您在仪表板中设置正确。)

您可以将此身份验证对象与客户端调用一起提供给RESTful服务器,并在服务器上检查签名是否正确。通过这种方式,你就知道该用户是通过脸书验证的,就是这个用户,并且是为你的应用程序验证的。

此文档页介绍如何使用签名身份验证。不要被标题中的"游戏"吓倒,它适用于任何网络应用程序。

您可以使用其他OAUTH提供商,以与FB登录相同的精神实现一些东西,而不是只允许Facebook进行SSO。

使用daniepolencic建议的解决方案,但要对其进行修改,以便在同一个node.js实例中使用另一个服务器进行登录,而不是使用代理。该服务与提供者进行OAUTH检查,并维护与客户端的会话。它向客户端发送一个签名的令牌,生存时间很短。客户端必须在生存时间结束之前请求新的令牌。

然后,您可以实现一个客户端JavaScript,其功能类似于Facebook JavaScript SDK,用于应用程序使用的登录。此函数可以轮询新令牌,也可以根据请求检索新令牌,这对场景来说是最有效的。

客户端在每个请求中将此令牌提供给RESTful API,服务器检查签名。就像Facebook SSO一样。

仍然有一个会话,但它可以在完全不同的机器上进行维护。该服务可以使用RESTful api独立于服务器进行扩展。

然而,请记住,这种方法可能容易受到中间人攻击和重播攻击的影响。在没有https的情况下使用可能是不明智的。

最新更新