使用 JWT + Session 对 Web App 身份认证的分析与实现

Web 前端全栈设计后端开发

发表时间:
作者:Ivan
热度:0

前言

本文的实现全靠 Node.js,本文讨论的 session 技术一般特指具体的用户数据保存在服务器上的 Session 技术(server side session),而不是通过加密或签名验证等形式保存在客户端上的 client side session

用于实现前端的技术栈

  • Vue —— 前端框架
  • Nuxt —— 用于实现 Vue SSR (Server-Side Rendering)
  • Express —— Nuxt 使用到的 Web 服务框架

用于实现后端的技术栈

  • Typescript —— 我爱强类型
  • Nest —— ts 实现的后端服务框架(基于 Express

分析:什么理想的身份认证技术

发布产品都要讲故事的年代,发布文章也得讲讲故事了。

笔者的博客又双叒叕更新了一番,这次后端从 Laravel 换到了 Nest,重新实现用户鉴权时,选择用 Session 还是 JWT 来实现身份认证时,犯了难。面对选择,那就从理想入手,找到适合的才是最好的。

身份认证的目的是为了数据安全,那么我们就先从安全入手,分析下什么样的身份认证技术是安全的。

身份认证信息安全

当用户登录后,服务器下发的身份凭据就是每次操作时用来确定唯一用户的标识。只有保证这个标识不被篡改,不被捏造,服务器才能确定谁是谁,权限如何。

防范 CSRF 攻击

cookie 传递身份凭据的时代,总是要防范 CSRF 攻击。CSRF 的全称是跨站请求伪造(Cross-site request forgery),在服务器没有对 CSRF 攻击做防范的话,凭借 cookie 中的身份凭据可以直接对目标站点发起合法请求。防御 CSRF 攻击,业界目前有三种策略:验证 HTTP Referer 字段;请求中添加 token 并验证;在请求头中添加自定义属性并验证。后两者都不得在 cookie 中保存 token,其中,前者需要是服务器必须对跨域请求进行限制,并单独为每个用户提供临时 token;后者则需要全站 Ajax 才得以实现。

防范 XSS 攻击

这里不讨论什么类型的 XSS 攻击,只关注用户凭据的安全性。javascript 代码可以读取非 Http-Onlycookie,可以读取 LocalStorageSessionStorageApplication Cache,可以读取 DOM 树。如果我们将凭据我们可以有两个策略来防范,一是将凭据存储在 javascript 变量中,二是使用双重验证。双重验证可以将用户凭据保存在以 Http-Only形式存储的 cookie 中,再在其他区域存储一个 token,当发起请求时附带上这两个凭据并验证。一但 XSS 攻击时获取到 token,但没有用户凭据,也无法完成攻击。

性能

写程序除了实现功能,剩下的就是降低资源占用率了。网站服务器,辛苦的都是 IO 操作啊,后台的每个请求都是需要鉴权的,REST API 的设计更增加了请求数量。如果每个请求都附上鉴权所需的用户信息,而不是一个用于查询用户信息的 key 那磁盘 IO 开销就少了。并且花费更多的网络 IO 主要是花在服务器的上行带宽上,影响甚小,十分值得。

持久性

对于带着服务器渲染的单页面 Web App 来说,刷新后还需要等待脚本加载完毕后再获取数据,甚至要重新登录才能获取数据,这个是不可忍受的。想要刷新后第一个响应就能给取到足够的数据,这依然需要仰仗 cookie 的帮助。如果只是需要记住用户,那倒不需要使用到 cookie,将长期的凭存储到 LocalStorage 等地方都是可以的,但这似乎给 XSS 攻击带来更多的可能性。

小结

作为正在改造个人网站(在境外的低配的VPS哇)的笔者来说,需要的是安全、高效并且持久的保存和读取用户凭据,jwt 加上 server side session 似乎是折中的解决方案。JWT 用来实现短期、不可注销、带有签名验证的用户信息的用户认证。session 用来实现长期、获取新 JWT时的用户认证。

分析:如何实现 JWT + Session 的身份认证系统

正确使用 JWT

如果你将 JWT 存储在 cookie 中,那么恭喜你,重新发明了 client side session。在 RESTful 的架构中,JWT 应当在 HTTP HeaderAuthorization 字段中传输。因为 JWT 一旦签发就不可提前作废,所以有效期应尽可能的短。由于 JWT 负载的信息都是未经加密的,所以不应在负载中出现用户密码等敏感信息。

如何续期 JWT

假设我们给用户登录后授予 30 分钟的在线时间,但 token 只有 2 分钟的有效期,这时应该在 token 过期前,例如客户端拿到 token 后的 90 秒时重新凭借着旧 token 获取新的 token。这样我们就完成了一次续期。当然,旧的 token 依然可以在他的有效期内使用。

而 30 分钟到达时,我们继续拿着即将过期的 token 向服务器索要新的 token,服务器首先验证了请求的 token 是否有效,再验证签发时间是否超过了 30 分钟。因为超过了 30 分钟,该用户已经无法凭借旧的 token 获取新的 token ,用户登录失效,再过不到 30 秒,用户会因为 token 过期而被迫下线。

从用户上线的三十分钟内,服务器只凭借 JWT 便能了解用户信息,避免鉴权带来的更多查询,这对于用户量大的、频繁需要鉴权的站点来说十分高效。

利用 Session 来续期 JWT

因为跨域的限制,CSRF 攻击是无法使用 POST、PUT、PATCH、DELETE 等请求来获取到其他站点的响应内容。所以我们在用户登录时可以通过响应头的 Set-Cookie 字段来设置一个 Http-Onlysession id,这样我们可以通过 session 获取新的 JWT

利用 session 来获取 token,虽然在重新获取 JWT 时需要服务器通过 session id 认证用户,但好处是更加安全,在 token 过期后,我们如果想要获取新的 token,可以验证用户是否已经登出,在不添加 token 黑名单的前提下,避免 JWT 无法注销的问题。

由于我们的 token 只存储在 javascript 变量中,所以一旦刷新网页,token 就会丢失。但是我们可以利用 session 来重新获取 token,这样一来,避免了用户重新登录带来的影响。

综合以上两种方法

在过期前,我们可以直接凭借 token 来进行除了续期以外的身份认证,这样同样可以减少查询操作。但如果我们一个三个阶段的验证模式,能带来更好的平衡点。

  • token 的有效期,一旦过期将自动登出。
  • token 更新截止时间,一旦超过这一时间将不能通过有效的 token 获取新的 token
  • session 有效期,根据需要设置,在有效期内,用户可凭借 session id 获取新的 token

首先,用户登录时我们签发一个短期有效的 token,再根据用户是否记住密码来创建一个 session,并将 session id 保存到客户端的 cookie 中。

服务器通过中间件对除了登录、通过 session 获取新 token 外的所有请求进行 token 验证,以达到鉴权的目的。

token 在有效期内,且未超过更新截止时间,用户可凭借有效的 token 来获取新的 token;否则,服务器通过 session id 进行认证,若通过,则用新的更新截止时间来签发新的 token,若仍未通过,则返回 401 错误码。

我们的 JWT 串大概长这样:

{
  "alg": "HS256",
  "typ": "JWT"
}
.
{
  "email": "a@ivanli.cc",
  "name": "Ivan",
  "group": "admin",
  "refreshExpiresIn": 1518518323,
  "iat": 1518517722,
  "exp": 1518517872
}
.
fkT3Sx7R3di827cngSvA345wT0SSS3BKkoxj4sjZByE

中间是负载,带三个时间:

  • refreshExpiresIn: 更新截止时间
  • iat:签发时间
  • exp:过期时间

通过上述的方法,可以以最低的成本,避免重放攻击,如果将 exp 设得更小,则可以在这段时间外避免重放攻击。

实现与代码

有空补充。。。。

登录 后发言

评论列表

暂时没有评论,快快抢个沙发吧!

共 0 条