作者:Bryan Manuele
原文:Sessionless Authentication using JWTs (with Node + Express + Passport JS)
——学习基于JWT的无用户会话(sessionless)验证的理论和最佳实践
使用有状态的用户session和储存在cookie中的session id进行验证的策略已经有几十年历史了。但随着面向服务架构和网络服务的出现,促进了使用sessionless原则设计应用的思想。
JWT提供了一种无状态的验证解决方案,不需要再在服务器跟踪session数据。相反,JWT允许我们安全的在客户端直接以JWT的形式储存session数据。
JWT受到了很多批评和怀疑,但事实上session验证和JWT验证都有很多产品应用,这两种方式用于处理用户验证都是安全和稳健的。如果你认为在你的系统架构中实现无状态是很重要的实践,那么JWT就是为你准备的。在本文,我们将讨论JWT是什么,选择使用JWT所要做的权衡,以及如何在你的架构中安全的实现它们。
验证是如何工作的
在我们开始之前,我们需要确定验证的流程看起来是什么样的:
- 用户使用POST(通过HTTS)传给服务器验证的细节:{ username, password }
- 服务器确定该用户是否是用户自己声称的身份
- 如果用户的验证尝试成功通过,接下来服务器发送某种形式的数据(一般是token或者session id),在接下来的每次请求中可以附加上这种数据,这样可以识别用户是否经过验证。
使用sessionless验证,客户端接收到的数据负载是JWT。JWT应该包含编码过的用户标识,它是后端服务器签名过的JSON格式。我们把JWT放到cookie中,因此不必在local-storage中储存它以免受到XSS攻击。以下是名为TheLegend27的用户使用JWT验证的流程:
cookie是一种通过验证后随着每次请求一起发送的特殊头部。它还可以方便的跨过用户session实现持久化。这意味着TheLegend27登录成功后,他在之后的每一次请求中都将他的JWT一起发送。我们所要做的是验证他的身份,检查请求中的cookie并验证JWT。
需要注意的重要事项:
- 我们不在服务器跟踪用户的session!这是JWT验证和session验证的明显不同之处。使用sessionless验证我们就少了一个需要担心的数据源。
- 我们的验证流程超级简单!如果你只是想尽可能直观,快速的在Web应用中实现身份验证,那么JWT是不错的方案。
这就是使用JWT实现sessionless验证的原理。如果你已经熟练使用session验证,这一流程看起来有些熟悉。JWT看起来与储存在cookie中HS256加密的session_id非常相似。事实上,JWT默认就是使用HS256签名的。两者之间的区别是JWT在负载中编码了所有session数据,而session_id从session表中查询session。
JWT去除了在后端跟踪session的需求。而是将session数据编码在JWT负载中。这里需要做权衡的是JWT的大小和它的负载大小成正比。幸运的是,携带形如{user_id, expiration_date }的负载对大多数情况都是足够的了。
所以,理论就是这样。我们来开始实战!
在讨论具体实现之前,让我们先来讨论在验证过程中保护自己免疫最常见的攻击/漏洞的最佳实践。
- XSS和SQL / noSQL注入攻击
- 用户证书暴力破解攻击
- 攻击者获取用户JWT/cookie
- 攻击者获取数据库的副本或读取凭证
JWT和最佳实践可以保护我们免疫所有这些攻击。让我们来看怎么做到的:
XSS攻击
这种攻击是我们最容易预防的。一种试图保护我们免受XSS和代码注入的简单做法是清洁用户输入,如使用_.escape(userInput)。但这一方法的问题是,对于私有数据,盲目信任库函数能恰当清洁用户输入以免受sql xss 攻击是很天真的。输入清洁是是第一层重要的防御,只用它还不够。
一种保护用户敏感数据的更牢靠的方式是使用ORM/ODM,这会强制使用参数化查询。如果我们用的是SQL,也可以使用它的存储过程语句,这样查询过程是在数据库层定义的,而不是代码层。
如果你对XSS和查询清洁的最佳实践感兴趣,我强烈推荐一个由开放网络应用安全项目基金会提供的防御XSS注入攻击最佳实践的资料
暴力攻击用户证书
攻击者暴力破解用户证书有两种方法:
A)他们会攻击单个用户,尝试排列组合密码直到匹配为止。
B)他们会攻击多个用户,使用一个常用密码表进行浅暴力破解,直到匹配为止。这里有一个很好的例子:https://hackernoon.com/picking-the-low-hanging-passwords-b64684fe2c7
对于B类攻击我们能做的不多。如果用户把密码设为p@ssw0rd,那么我们除了强制使用更严格的密码策略,所能做的不多。
事实证明我们可以很好的保护自己免受A类攻击。关键的地方在于让处理登录请求的时间需要花不少时间。如果需要
JWT受损
这是我们能遇到的最坏可能的攻击,因为这最难解决。幸运的是,JWT很少受损,因为我们在cookie中储存JWT,并使用HTTPS来进行网络传输。由于我们从不在LocalStorage中储存JWT,只在cookie中储存,恶意的攻击者不能使用XSS偷走用户的JWT。
如果攻击者以某种未公开方法去偷用户的JWT,那么很不幸没有很多办法可以预防。为了减少破坏,你应该设计让你的应用要求在高级别档案传输之前执行重新验证,例如在购买和更改密码之前。你的JWT应该有过期日期。这样受损的JWT只能生效这么长时间。
数据库受损
事实证明,即使黑客拿到了我们的数据库读取凭据,我们仍可以通过数据混淆处理保护用户数据。
如果我们有密码,我们不希望将其以纯文本形式存储在我们的数据库中。相反,我们可以对密码进行salt和hash,只存储salt和hash而不是我们的纯文本密码。
想象我们有一个密码p@ssw0rd。salt是一个随机的字符串,我们把它附在密码上('p@ssw0rd' + 'asdf253$n5'),然后将它传给hash算法:SHA256('p@ssw0rd' + 'asdf253$n5')。随后我们将salt和hash的结果储存在用户表里。
如果我们的数据库受到损害,这也可以很好的保护我们的用户密码。你仍然需要采取行动,例如请求所有用户重置他们的密码,但这将为你带来足够的时间。
事实证明有一种标准的方法来salt和hash你的用户密码,这样就不需要在用户表中储存salt和hash字段。这一解决方案称为Bcrypt。
Bcrypt
Bcrypt是一种在1999年诞生的密码hash算法,它对salt和hash密码做了标准化。它也可以保护密码被暴力破解,因为它让验证过程需要高强度计算,慢到爆。我们将在我们的验证实现中使用该方法。
这就是经过Bcrypt处理后的密码的样子。它包含了hash算法的版本,hash成本,salt值,和hash后的密码值。
现在我们已经拥有了所需的知识,让我们开始实现验证
我将使用MongoDB,你可以选择自己熟悉的数据库。下面是一个普通的mongoose User Schema
const
接下来,我们将注册两个Passport策略。分别是Local Strategy和JWT Strategy。在后面,当我们调用passport.authenticate('local')或passport.authenticate('jwt')时,就会调用各自的中间件。
const
Local strategy从req.body中提取username和password,并查询用户表以验证该用户。
JWT strategy从请求中的cookie中提取JWT,使用应用中的secret验证JWT的签名。
最后,很重要的是,我们定义/login和/register路由
const
/register 路由很直观,我们创建一个新的用户并将它存入用户表中,如果成功就返回状态码200。
/login路由更复杂一些。我们来分解一下:首先使用local strategy策略验证。如果验证成功,我们给JWT编写一个负载。随后调用req.login,这可以将负载放在req.user上。然后我们调用jwt.sign对JWT签名。最后在cookie中设置JWT。
现在我们来建一个需要验证的路由,可以使用JWT中间件来验证用户是否合法。
router
总结
就是这样!这里有整个兔子洞(注:爱丽丝梦游仙境中通向迷失世界)等待着那些对安全和认证感兴趣的人。我个人被困在这个兔子洞里的时间比我承认的要长得多。
本文是我对JWT最佳实践的研究成果,这应该足够让你尝试一下sessionless认证。JWT是一种优雅的认证方案,我希望本文阐明了它的工作原理,并让你有所启发,自己安全的实现它。