初识JWT
最近一直在整理过去的项目经历,其中有一个使用vue-cli和Element-UI搭建的后台管理单页应用,其中的管理员权限认证用到了JSON Web Token
。
之前在做博客的后台管理系统的时候(虽然现在基本没用过了),也采用和同样的认证方式,当时只是简单对着文档进行实现,对其原理并没有很深刻的认识,因此决定稍作整理。
参考:
- JSON Web Token - 在Web应用间安全地传递信息,强烈推荐
- 官方文档
- Token 认证的来龙去脉
- 讲真,别再使用JWT了!,这里有个扩展阅读哈哈
业务场景
在单页应用中,通过Token实现用户的身份权限认证,大致流程如下
- 用户首次登陆输入账户密码,后台接口验证通过,则返回一个token值
- 用户接受到服务器传回的token后,然后保存在本地,
- 在之后的接口请求中都附带该token值,用于服务器进行用户身份权限校验
- 如果校验通过,则返回正常数据;如果不通过,返回401错误,跳转到登录界面
其中由几个关键点,下面一一道来
token
token包含一些关键信息,也可以转码成原始数据,这样后台才能解析token相关的信息。
token生成
需要防止token被修改过伪造,否则token进行的身份认证将毫无意义。
一般会通过密钥对token进行加密,密钥由后台控制,不会传递给前端。
token有效期
token包含过期校验,可以通过保存一个过期时间expries字段,也可以通过token的保存机制(比如cookie,下面会提到)来实现。
一旦token过期,则需要让旧的token失效,然后判断用户是不是仍在使用应用;如果是,则需要返回一个新的token,或者更新旧token的有效期。
保存token
获取到token后,需要将其保存在本地,前端本地保存常见的有三种形式(这个好像是一个常见的面试题~)
localStorage
不能跨子域名共享,可以持久化保存,需要手动实现认证过期机制
sessionStorage
不能在子域名共享,也不能跨窗口共享,关闭窗口后就会被清除,如果业务比较敏感可以采用
document.cookie
可以跨子域名共享(将所有子域名的domain都修改成顶级域名即可),可以通过设置过期时间控制token的有效期。
需要注意的是这里只是把cookie作为前端存储机制,对应的cookie字段本身并不参与后台的权限认证逻辑中,这样可以避免CSRF攻击
流程实现
通过上面的流程,我们需要实现的主要有三个部分
- token的生成机制,如果保存基础信息,以及token的加密机制
- 前端在每次的请求中都需要附带对应的token,这可以通过请求拦截器处理。在项目中使用的是
axios.interceptors.request
,为请求头的添加一个自定义头部来实现的。 - 后台需要在每次请求中对token进行解析和判断,这个可以通过使用中间件实现(比如express中的
server.use
或者Laravel中的middleware),在中间件中对请求头中的token进行解码
总之,这里的认证需要需要前后端的配合来实现
- 前端负责保存token,并为每次请求附带上对应的token
- 后端负责生成和解析token,获取token内部信息,判断是否合法和过期,并决定对应操作
JSON Web Token
OK,上面大致了解了Token的作用和使用流程,接下来看一看一种实现Token的方式:JSON Web Token
(简称JWT)。
组成
为了方便理解,从代码实现开始入手,这里用到的jwt-simple这个库。
let jwt = require("jwt-simple");
// 服务端密钥
let jwtSecret = "my_base_secret";
let Util = {
encode(params) {
return jwt.encode(params, jwtSecret);
},
verify(token) {
let data = jwt.decode(token, jwtSecret);
let { expires } = data;
return expires > Date.now();
}
};
// 载荷
let data = {
uid: 100,
name: 'shymean',
expires: new Data().getTime() + 24*60*60*1000
}
let token = Util.encode(data)
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwMCwibmFtZSI6InNoeW1lYW4ifQ.sbxKmBPjaQcXrfwBTxDkppXUa7ZJVvQ9ppw0Apnd_Jk
console.log(token)
先不用管上面代码的具体含义,打印输出的token,通过两个.
连接的三个子字符串组成的一个token。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
载荷
载荷指的是token保存的原始数据,即上面代码中的data
变量,其中保存了一些信息,服务器通过解析JWT,然后可以获取到对应的信息。通过对data进行base64转码,就可以得到对应的子字符串。
let base64url = require('base64url')
console.log(base64url(JSON.stringify(data)))
得到的结果为
eyJ1aWQiOjEwMCwibmFtZSI6InNoeW1lYW4ifQ
发现了什么?没错,就是上面的token中第二部分。base64可以通过解码还原成原始的数据,因此没有加密之说。
头部
头部用于描述jwt的基本信息,比如签名所使用的算法等。头部甚至可以是某种固定的形式,查看jwt-simple
的源码可以发现,在其encode
方法中
if (!algorithm) {
algorithm = 'HS256';
}
var header = { typ: 'JWT', alg: algorithm };
然后同样对header进行base64编码,得到的结果为
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
就是上面的token中第一部分。同理,对header编码进行还原,可以获取JWT的基本信息。
签名
将头部和载荷通过.
连接在一起,然后通过指定的HS256
算法进行加密(关于HS256算法,可以移步这里),就可以得到加密后的内容,这个内容被称为签名。
在加密过程中,需要一个密钥,通过密钥和HS256算法
,就可以得到加密后的签名。对应的加密可以通过crypto
这个包来实现。
let crypto = require('crypto');
let msg = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwMCwibmFtZSI6InNoeW1lYW4ifQ'
let base64str = crypto.createHmac('sha256', jwtSecret).update(msg).digest('base64');
console.log(base64str)
// sbxKmBPjaQcXrfwBTxDkppXUa7ZJVvQ9ppw0Apnd/Jk=
最后将头部、载荷和签名通过.
连接起来,就得到了上面的JWT。OK,现在我们理解了JWT的生成过程。
理解
可以看见,上面的载荷和头部都是简单的base64转码,只有签名是经过加密的,其中密钥保存在服务器上,不会泄露到客户端。
当服务器接收到JWT后,会根据头部的信息(加密类型和算法),再次对头部和载荷拼接的字符串进行加密,然后比较传回的签名和计算得到的签名,如果不一致,则表示数据已发生改变,token不可信任,否则,解析载荷的内容,获取token保存的信息。这就是为什么需要将头部、载荷和签名一起保存到JWT中的原因。
由于载荷采用的是base64编码,因此不能在载荷中放入敏感信息,比如用户密码等。但是由于签名的存在,在用户认证和权限判定等应用场景下还是可以使用的。
注意事项
这里我一直有一个疑问,如果恶意用户不修改头部和载荷,而是直接盗用了整个token,那么就可以完整地模拟权限用户了。这跟我们如何保存token有很大关系
- 如果将token保存在本地(localStorage、sessionStorage),则需要注意XSS攻击,这样恶意脚本就可能获取到对应的token值。
- 如果将JWT保存在cookie中,然后设置为HttpOnly,则需要注意CSRF攻击,但是这样就需要后台去解析每次请求的Cookie字段了,一旦发生了CSRF攻击,则会绕过认证权限。
因此这里就回到了前端的Web安全问题上了:XSS和CSRF。另外为在token中保存过期时间也是一个很有必要的事情,这样可以避免万一发生泄漏导致用户持久被攻击的问题。
过期问题随之而来的另外一个问题是在用户访问过程中token失效的处理措施,即后台更新过期时间策略。
关于JWT的具体使用场景,除了单页面身份认证,目前在项目中接触的并不是很多,有待进一步学习和整理。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。