关于前后端分离鉴权的思考

前后端分离项目的 Token 存储问题由来已久,有的人存 Cookie 有的人存 LocalStorage 或 SessionStorage。在之前的个人项目中,曾经花时间去研究这个问题,以下分享下我的看法。

如何安全的传输用户 token

这是最传统也是最简单的方式了,前端登录,后端根据用户信息生成一个 token,并保存这个 token 和对应的用户 id,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个 cookie,后端根据这个 cookie 来标识用户。

flow-cookie-session.jpg

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。

尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。另外,后端每次都需要根据 token 查出用户 id,这就增加了数据库的查询和存储开销。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。

secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

CSRF.jpg

看起来我们不能兼顾。确实,光依靠这一个 token 我们没办法兼顾这两点。既然一个不够,那就两个。于是有了 XSRF-TOKEN,它和作为用户令牌的 token 类似,也是服务器生成的一个散列值。我们把 token 通过 httpOnly 发回去,把 XSRF-TOKEN 直接发回去。我们可以无视 httpOnly 的 cookie 因为我们没法操纵它,但对于这个 XSRF-TOKEN,我们就可以在我们网站的每个请求中都加入到 header 里面去。而服务端就需要检查这个 header 的 XSRF-TOKEN 是否真实有效。

由于 XSRF-TOKEN 以非 httpOnly 的形式存储在 cookie 中,正常情况下只有我们自己的网站可以获取到该 XSRF-TOKEN。这样 XSRF 攻击就变得不太可能了。另外由于用户 token 是通过 httpOnly 形式存储,JS 不可获取,这样也保证了用户 token 的安全。XSS 注入最多只能获取到 XSRF-TOKEN。

但还是有一种可能,XSS 注入取得 XSRF-TOKEN 后在当前页面发送请求出去。本文并不打算讨论 XSRF 和 XSS,明白这两个真正危害的地方就可以知道,这种 XSS 注入取得 XSRF-TOKEN 后发送请求其实并没有带来什么危害。不过呢,还是要看具体情况吧,如果我们的网站有一个投票 XXX 的接口,这个接口的链接被 XSS 注入中,那么当所有人打开这个页面的时候,都会自动的朝 XXX 投了一票。

不同于 XSRF, XSRF 可以从其他网站执行该段脚本,而这里只能注入到我们的网站中来执行。因为我们的 JS 也是这样子做的,取出 XSRF-TOKEN 放入请求头部然后发送请求出去,所以这就无法避免了。事实上,由于我们的前端代码都是公开的,无论 JS 层面绕多少个弯,XSS 注入还是可以照着做过来。但好在这种方式其实造成的影响相当有限,并不会比我们常说的 XSS 注入和 XSRF 攻击的危害大,要知道 XSS 注入危害的是 cookie 的泄露,但因为真正鉴定身份的 Token 被我们通过 httpOnly 存在 cookie 中,因此避免了 XSS 注入产生的影响。

我们再讨论另一个问题,前面也说了,服务器要经常去查询这个 token 对应的是哪一个用户。其实可不可以不要服务器去查询呢?如果我们生成 token 遵循一定的规律,比如我们使用对称加密算法来加密用户 id 形成 token,那么服务端以后其实只要解密该 token 就可以知道用户的 id 是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,由公钥加密生成 token,私钥来解密 token,这样做就安全多了。其实现在有个规范是可以这样做的,就是我们接下来要介绍的 JWT。

Json Web Token

接下来我们就简单介绍 JWT 这个东西,全称叫 Json Web Token。

JWT 简介

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

  • 简洁(Compact)

    可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

  • 自包含(Self-contained)

    负载中包含了所有用户所需要的信息,避免了多次查询数据库

JWT 组成

JWT 由 Header, Payload, Signature 三部分组成,即头部,负载,签名,长这样:

jwt.jpeg

  • Header 头部

    头部包含了两部分,token 类型和采用的加密算法

    {
    "alg": "HS256",
    "typ": "JWT"
    }

    它会使用 Base64Url 编码组成 JWT 结构的第一部分

  • Payload

    这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。这些我们应该规范的使用,因为他们在校验中会用到。

    例如:

    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }

    同样的,它会使用 Base64Url 编码组成 JWT 结构的第二部分

  • Signature

    前面两部分都是使用 Base64Url 进行编码的,即客户端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过 . 连接在一起就是我们的 JWT 了,它可能长这个样子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

你也可以把 XSRF-TOKEN 也签发进去, 然后前端传来 XSRF-TOKEN 的时候与 JWT 中的 XSRF-TOKEN 进行比对, 这样服务端也不需要存储 XSRF-TOKEN 了。

JWT 的长度貌似和你的加密算法和私钥有关系。其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP/2,HTTP/2 对头部进行了压缩,相信也解决了这个问题。

JWT 使用

JWT 生成了,怎么使用就看你了,不过还是有一点要求的。当访问需要 JWT 验证的 API 时,需要把该 JWT 放入头部的 Authorization 中。

Authorization: Bearer <token>

注意 Bearer 是必须的,中间有一个空格,后面跟着 JWT,这样服务端就可以从 Authorization 取出来用了。当然了,你也可以完全爱怎么来就怎么来。但是按照规范你可以省很多事情。

等等, 可是我们前面说的是存到 cookie 中的? 关于这一点, 使用 koa-jwt 你可以通过设置把 JWT 存储到 cookie 中。 或者我们手动的把 cookie 取出来加到 header 也可以。

简单的 JWT 流程是这样的,不带 XSRF 的,没有找到带 XSRF 的图 =.=

tokens-new.png

总结

其实关于 JWT 存放到哪里一直由很多讨论,有人说存放到本地存储,有人说存 cookie。但我觉得上面我说的这种方式是挺稳妥的,如果你有什么意见和看法欢迎提出。参考资料也附出了比较热门的关于 jwt 存储位置的讨论文章,可以看下。


参考资料:

标签: JWT, JavaScript, Koa, Angular

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

已有 2 条评论

  1. [...]参考:1 2 3[...]

  2. monster1935 monster1935

    赞一个!

添加新评论

This page loaded in 0.001468 seconds