前后端JWT令牌策略

44

我正在使用Ember.js编写应用程序,后端/服务器端是基于Node.js服务器的。 我已经配置了Ember.js,以便用户可以使用第三方OAuth(Google、Twitter、Facebook)进行登录/注册。 我编写了一个基于Express的Node.js后端服务器来托管RESTful API。

由于Ember.js严格限制只能在客户端代码中运行,因此我没有将数据库连接到Ember.js上,并且我认为我不应该这样做。 我计划使用JWT来在客户端和服务器端之间通信。当用户使用他们的OAuth凭据登录时,我会从提供者那里收到一个JSON对象,其中包含uid、姓名、登录名、access_token和其他详细信息。

我正在考虑如何处理用户注册流程。由于OAuth不存在注册过程,所以如果用户不存在我的数据库中,就需要创建一个新用户。 我不支持电子邮件/密码认证。 用户第一次使用OAuth提供程序登录时应采取什么流程?Ember.js是否应该在每次登录时将所有详细信息发送到后端,以便后端可以将新用户添加到数据库中?

我的JWT载荷应包含哪些部分?我正在考虑使用UID和提供商提供的访问令牌。这里可能存在一个问题,即特定于提供程序的访问令牌可能会更改。用户可以从提供者的站点中撤销令牌,然后再次使用Ember.js进行注册。

如果使用其他JavaScript客户端框架编写前端可以更容易实现,则我愿意尝试更改。


你是想要一个如何实现的描述还是所有的代码?请注意,Ember和Node都可以胜任此任务,客户端和后端的技术栈对解决方案没有实质性影响。 - Harry
我正在寻找一个流程图,以了解不同组件在哪个阶段应该如何通信。我不需要代码。 - ed1t
3个回答

51

如果我们不仅谈论工作,而且还要考虑安全的无状态身份验证,您需要考虑使用访问刷新令牌的正确策略。

  1. 访问令牌是提供对受保护资源访问的令牌。 过期时间 可以设置为大约1小时(取决于您的考虑)。

  2. 刷新令牌是一种特殊的令牌,应该用于生成额外的访问令牌,以防它已过期或用户会话已更新。显然,您需要使其寿命更长(与访问令牌相比),并尽可能安全。 过期时间 可以设置为大约10天甚至更长时间(也取决于您的考虑)。

注意:由于刷新令牌的生命周期很长,为了使它们真正安全,您可能希望将它们存储在数据库中(刷新令牌请求很少)。这样,假设您的刷新令牌被某种方式黑客攻击并重新生成了访问/刷新令牌,当然您将失去权限,但是您仍然可以登录系统,因为您知道登录/密码(如果您稍后要使用它们)或通过任何社交网络进行登录。


这些 tokens 应该存储在哪里?

基本上有两个常见的地方:

  1. HTML5 Web 存储 (localStorage/sessionStorage)

虽然方便,但同时也存在风险。该存储是通过同一域名下的 javascript 代码访问的。这意味着如果你遇到XSS,你的 tokens 可能会被黑客攻击。因此选择此方法时,您必须小心并对所有不受信任的数据进行编码/转义。即使您这样做了,我敢肯定您使用了一些第三方客户端模块,并且没有保证其中一个模块没有恶意代码。

此外,Web 存储 在传输期间不强制执行任何安全标准。因此,您需要确保 JWT 是通过 HTTPS 发送而不是 HTTP

  1. Cookies

使用特定的HttpOnly选项,可以使cookie无法通过javascript访问,并且免疫XSS。您还可以设置Secure cookie标志,以确保cookie仅通过HTTPS发送。 但是,cookie容易受到不同类型的攻击:跨站请求伪造(CSRF)。 在这种情况下,可以通过使用某种同步令牌模式来防止CSRF。在Security Considerations部分中,AngularJS有很好的实现。

您可能想要关注的文章

为了说明它的一般工作原理:

enter image description here


关于JWT的简介:
为了让它更清晰,Auth0团队有一个非常酷的JWT调试器。 通常有2(有时3)种常见的声明类型:publicprivate(和保留)。 JWT主体的示例(有效载荷,可以是任何你想要的内容):
{     
  name: "Dave Doe",
  isAdmin: true,
  providerToken: '...' // should be verified then separately
}

关于JWT结构的更多信息,您可以在这里找到。


7

回答你提出的两个具体问题:

当用户第一次使用OAuth提供程序登录时,流程是什么?Emberjs是否应该在每次登录时将所有详细信息发送到后端,以便后端可以将新用户添加到数据库中?

每当用户通过oauth注册或登录并且客户端接收到新的访问令牌时,建议将其更新或插入到用户表(或集合)中,并包括从oauth提供程序API检索到的任何新的或更新的用户信息。我建议将其直接存储在每个用户记录上,以确保访问令牌和相关配置文件信息原子更改。通常,我会将其组合成某种中间件,当存在新标记时自动执行这些步骤。

我的JWT正文应该包括什么?我在想uid和提供程序提供的访问令牌。我能想到的一个问题是,提供程序特定的访问令牌可能会更改。用户可以从提供程序的站点撤销令牌,然后再次使用emberjs进行注册。

JWT主体通常由用户声明组成。我个人认为,在JWT令牌的正文中存储提供程序访问令牌没有太多好处,因为对于您的客户端应用程序而言它的好处较少(除非您从客户端直接向其API进行大量调用,我更喜欢在服务器端执行这些调用,并向我的应用客户端发送符合自己接口的规范化声明集)。通过编写自己的声明接口,您将不必在客户端应用程序中处理来自多个提供程序存在的各种差异。例如,将Twitter和Facebook具体字段命名不同的问题合并为存储在用户配置文件表中的常见字段,然后将本地配置文件字段作为JWT正文中的声明嵌入到客户端应用程序中进行解释。这样做的另一个好处是,您不会在未加密的JWT令牌中保存任何可能在未来泄漏的数据。

无论您是否在JWT令牌正文中存储oauth提供程序提供的访问令牌,每当配置文件数据更改时都需要授予新的JWT令牌(如果没有配置文件更新且先前的令牌仍然有效,则可以设置绕过发放新JWT令牌的机制)。

除了在JWT令牌正文中存储为声明的任何配置文件字段外,我始终会定义标准的JWT令牌正文字段

{
    iss: "https://YOUR_NAMESPACE",
    sub: "{connection}|{user_id}",
    aud: "YOUR_CLIENT_ID",
    exp: 1372674336,
    iat: 1372638336
}

4
对于任何OAuth工作流程,您一定要使用passportjs库。您还应该阅读完整的文档。它很容易理解,但我第一次没有阅读整个文档,所以遇到了困难。它包含了超过300个提供商的OAuth身份验证和发行令牌。
尽管如此,如果您想手动执行或想要基本了解,这里是我将使用的流程:
  1. 前端拥有一个登录页面,在此页面中列出了使用Google / Facebook等进行登录的选项,OAuth已经实现。

  2. 成功的OAuth将产生uid,login,access_token等(JSON对象)

  3. 您将JSON对象POST到Node.js应用程序中的/login/路由。 (是的,您发送整个响应,无论它是新用户还是现有用户。在此处发送额外数据比执行两个请求更好)

  4. 后端应用程序读取uidaccess_token。通过遵循(https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#checktoken)或使用访问令牌从提供程序请求用户数据来确保access_token有效。(对于无效的访问令牌,这将失败,因为OAuth访问令牌是基于每个应用程序/开发人员生成的)。现在,在您的后端DB中搜索。

  5. 如果uid存在于数据库中,则更新用户的access_tokenexpiresIn。 (access_token允许您获得该特定用户的更多Facebook信息,并且通常提供几个小时的访问权限。)

  6. 否则,创建具有uid,login等信息的新用户。

  7. 在更新access_token或创建新用户后,您发送包含uid的JWT令牌。 (使用密钥编码jwt,这将确保它是由您发送并且尚未被篡改。请查看https://github.com/auth0/express-jwt

  8. 在前端用户从/login接收到jwt之后,通过sessionStorage.setItem('jwt', token);将其保存到sessionStorage

  9. 在前端,还需添加以下内容:

如果存在jwt令牌,则以下代码将确保在每个请求中发送该令牌:

if ($window.sessionStorage.token) { xhr.setRequestHeader("Authorization", $window.sessionStorage.token); }

  1. 在您的Node.js app.js文件中添加:

app.use(jwt({ secret: 'shhhhhhared-secret'}).unless({path: ['/login']}));

这将验证路径中的任何内容的jwt,确保用户已登录,否则不允许访问并重定向到登录页面。这里的例外情况是/login,因为那是您为新用户或未经身份验证的用户提供JWT的地方。

您可以在Github URL上找到有关如何获取令牌以及查找当前正在服务的哪个用户请求的更多信息。


你如何保护后端的/login端点,以便只允许客户端进行发布,而不是任何人? - ed1t
您正在使用CORS。在后端添加Access-Control-Allow-Origin: http://www.foo.com,以便只有Origin:http://www.foo.com可以向/login发送POST数据。 - Rahat Mahbub
然而,请记住这可以被欺骗。我已经更新了第四步的相关信息。 - Rahat Mahbub
@RahatMahbub 你认为这个怎么样:https://github.com/gkatsanos/boilerplate-server。我还做了邮件验证。(只使用用户/密码=> JWT,没有Facebook/Google oAuth)(没有前端代码,只有API/服务器) - George Katsanos
PassportJS有时候设置起来太麻烦了。通常它在前端生成正确的错误输出方面不够灵活。PassportJS远非任何问题的完美解决方案。 - Nikita Vlasenko

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