本文同步自个人公众号 “JSCON简时空”,欢迎关注: https://mp.weixin.qq.com/s/Vh...
本文长约 1w 字,阅读耗时约 20 min
本文要是讲 JWT
(JSON Web Token) ,我刚接触这个这个知识点的时候,心路历程是这样的:
- 啊?Token 是什么?
- 什么是
JWT
?为什么要去用JWT
? - 使用
JWT
繁琐不繁琐,怎么用? - 那说说看使用
JWT
的风险和收益分别是什么?
如果你有以上和我相似的疑惑句,那么本文将对你有所帮助。
本文将从 JWT 的概念、基本原理、如何用 Node.js 创建、适用范围 和 风险控制 方面来剖析 JWT ,让你清晰地了解哪些情况下适用 JWT,以及在使用时的注意事项。
1. 序
越来越多的开发者开始学习并在实际项目中运用 JWT
(JSON Web Token)技术来保护应用安全,很多公司的应用程序也开始使用 JWT
来管理用户会话信息。
任何技术框架都有 自身的局限性,不可能一劳永逸,JWT 也不例外。众所周知,如果账户信息(用户名和密码)泄露,存储在服务器上的隐私数据将受到毁灭性的打击,如果是管理员的账户信息泄露,系统还有被攻击的危险。那么,JWT
的信息发生泄露,会带来什么样的影响?该如何防范?
要想解答这些疑惑,我们需要稍微全面地了解 JWT
。
那么我们先从了解 Token 的概念开始吧。
2. 什么是 Token
当你去银行取钱的时候,肯定需要输入你的银行卡密码才能取到钱。在这里,你的 银行卡密码 就是一种 Token。
Token 的中文有人翻译成 “令牌”,我觉得挺贴切的,意思就是,你拿着这个令牌才能过一些关卡或者有特权做某些事情。想象一下古装剧里,钦差大臣带的 尚方宝剑 就是一个 Token...
那计算机领域的 Token 的概念是什么呢?为此去翻了一下 Wiki 百科,一般来讲,Token
(令牌) 通常是指 Security Token
(安全令牌),可分为:
-
Hardware Token
(硬件令牌):常见的比如银行给你发的 U 盾,每次你大额支付或转账的时候,你除了输入密码,还得在电脑上插入 U 盾才行。(吐槽一下,U 盾使用起来是真的麻烦...) -
Virtual Token(虚拟令牌)
:其实这个概念和上述的硬件令牌对应,概念比较广泛,凡是软件实现的都可以用这个概念来概括。 -
Authentication Token
(授权令牌):授权令牌用于决定你有访问哪些资源的权限,比如常见的就是你可以用微信登录第三方网站,第三方网站能根据微信的授权令牌来获取你的微信头像和昵称等个人信息。(常用的授权机制是采用 OAuth 2.0,本文就不展开了,网上很多教程) -
Cryptographic Token
(加密令牌):这个最为熟悉的一个例子就是 比特币 了, 加密数字货币就属于一种加密令牌~ 令牌的所有权通过某些加密机制(例如数字签名)来证明自己的某种数字资产 -
Key Fob
(钥匙卡):一种安全的小型终端:带有内置验证机制的小硬盘设备。常见一个例子的就是车子电子钥匙。
其他类型的 Token
,就不一一列举了。
上述我们可以看到 Token
的主要作用是验证 身份的合法性,以允许计算机系统的用户可以操作系统资源。
我们这里所讲的 Token
,主要目的是为计算机系统提供一个可以识别用户的任意数值,像 HelloWorld
的明文字符串,或像 xxxooo-aab-cc35r51sfa-sdf27
之类的加密字符。
Token
的知识就了解到这里。接下来将聊聊有关 JWT
(JSON Web Token) 的原理
3. 那什么是 JWT 呢?
JSON Web Token (JWT) 是一个开放标准(RFC 7519
),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
看不懂?没关系,上面那段话好像是机翻的...
翻译成人话,就是说 JWT
是一个加密标准,当用户拿到这个加密 Token 的话,相当于拿到了一份证明自己身份的数字证书,就可以出入加密系统。我们把计算机系统想成公司,JWT
就是公司发给你的工牌,你拿着公司工牌,就可以刷卡进入园区、食堂吃饭结算、进入会议室等等公司需要认证授权的地方。
RFC 7519 规定了 JWT 的格式,我们看一下 JWT 长啥样。总体上来看,JWT 以 .
分隔成 3 段,示例一下:
aaaaaa.bbbbbb.cccccc
这 3 段分别代表如下含义:
- header:译成 头部,对应上述
aaaaaa
部分, - payload:译成 载荷,对应上述
bbbbbb
部分 - signature:译成 签名,对应上述
cccccc
部分
如果不太好理解的话,以日常生活中 开货车 为例来对比:
-
header
相当于你货车的 车牌 -
payload
相当于你货车所拉的 货物 -
signature
就你驾驶员的 驾驶证/行驶证
这么一类比是否就清晰多了?
不仅 JWT 可以用货车来类比,计算机网络相关的概念(诸如 HTTPS、TCP 啊)等知识都可以这么类比方便自己理解。
接下来我们详细讲解生成这些 aaaaaa
、bbbbbb
、cccccc
字符串的具体过程。
3.1 Header - “车牌”
这 "车牌" 是一个 JSON 对象,描述 JWT 的元数据,包含两部分:
-
typ
:表示这个令牌(token
)的类型(type
),在 JWT 协议里没得选,只能是JWT
-
alg
:表示你后面你在Signature
部分(即上述ccccccc
部分)所使用的加密算法。
例子如下:
{
"alg": "HS256",
"typ": "JWT"
}
常用的算法有HMAC SHA256
或RSA
,完整的算法类型我从官方上截了个图:
最后将这个 JSON 对象使用 Base64URL
算法转成字符串,就变成了图示中的 aaaaaa
了。
3.2 Payload - "货物"
这 “货物” 部分也是一个 JSON 对象,用来存放实际需要传递的数据。
JWT
规定了 7
个官方字段(Registered claims)供选用:
-
iss
(issuer):签发人 -
exp
(expiration time):过期时间 -
sub
(subject):主题 -
aud
(audience):受众 -
nbf
(Not Before):生效时间 -
iat
(Issued At):签发时间 -
jti
(JWT ID):编号
这些官方字段记不住没关系,有个概念就好,大不了回来查一下就行。
除了官方字段,你还可以在这个部分定义私有字段(Private claims),下面就是一个例子:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,这部分默认是不加密的、是不加密的、是不加密的(重要的话多说几遍),任何人都可以读到,所以不要把你敏感信息明文放在这个部分(除非你把内容先自行加密过)。
这个 JSON 对象也要使用 Base64URL 算法转成字符串,就变成了图示中的 bbbbbbb
了。
3.3 Signature - "行驶证/驾驶证"
Signature
部分是对前两部分的签名,防止数据篡改。
- 首先,需要指定一个密钥(
secret
)。这个密钥只有服务器才知道,不能泄露给用户。 - 然后,使用
Header
里面指定的签名算法(默认是HMAC SHA256
),按照下面的公式产生签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这个算出的签名就是上面所述的 cccccc
字符串了。
算出签名以后,我们把 Header
、Payload
、Signature
三个部分拼成一个字符串,每个部分之间用"点"(.)分隔。
至此你就获得了一个 JWT
—— 是不是简单到令你窒息?!
贴一张从网上找来的图,如果你现在一眼看这张 JWT 图觉得非常直观,那说明你就已经掌握本节内容了。
3.4 插播知识点 - Base64URL
前面提到,Header
和 Payload
串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL
(比如 api.example.com/?token=xxx
)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。
这就是 Base64URL 算法。
JWT 使用 Base64 编码, 注意这不是加密,只是把 JWT 的 json 格式去除,变成更加紧凑的形式
如果觉得陌生的话,jwt.io 官网提供了实时的生成工具,可自行前往体验:https://jwt.io/#debugger-io
现在,我们已经了解了 JWT
的基本原理,接下来将使用 Node.js 来演示生成 JWT
的完整过程。
4. 用 Node.js 实现 JWT 的过程
同样官方还提供了现成的 Node.js 包 jsonwebtoken 用于 Node.js 环境。
用 Node.js 实现非常的简单,几行代码就完成了 JWT 的生成和校验。
首先安装依赖:
npm install jsonwebtoken
然后我们书写一个简单案例,给用户 “张三” 等登陆信息生成一个 JWT:
/** 引入依赖包 **/
const jwt = require('jsonwebtoken');
/** 张三用户登录信息 **/
let payload = {
id:"123",
name:"张三"
}
/** 加密秘钥,这个秘钥保存在服务端,不要给别人知道 **/
let seccret = "1024";
/** 调用工具生成 jwt **/
let token = jwt.sign(payload, seccret, {
/** 到期时间设置为 1 个小时 **/
/** 格式诸如 "7d"、"12h",默认单位是 ms,因此 “120” = 120ms **/
expiresIn: "1h" ,
/** 签发人 **/
issuer: "JSCON简时空"
});
/** 输出 token **/
console.log('token:', token);
// token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiLlvKDkuIkiLCJpYXQiOjE1ODY4MjkxMzIsImV4cCI6MTU4NjgzMjczMiwiaXNzIjoiSlNDT07nroDml7bnqboifQ.r0Yiiy80Fy_Sim1I1EB5BHKwYmpA0OH3_vAYBRim618
以上几行代码生成了 jwt,我们就可以下发给用户;等后续用户重新上传这个 token ,我们调用 jwt.verify
方法来校验是否合法:
/** 校验是否合法 **/
jwt.verify(token, seccret, (error, decoded)=>{
if(error){
console.log(error)
return error
}
/** 输出校验结果 **/
console.log("校验结果:", JSON.stringify(decoded, null ,4))
});
最终输出的校验结果就是一个 JSON
对象,包含了 JWT 的明文相关信息:
注意: 校验失败或 token 过期都会在执行 error。
讲到这里,原理也知道了,实现方法也清楚了,温饱问题解决后接下来就上升到 “精神” 层面的讨论:为什么我要用 JWT,它的优势体现在哪里?
5. 为什么需要 JWT 呢?
要想了解为什么,就需要先了解 JWT
的应用场景 —— 用于 Web 开发领域的身份验证。
5.1 HTTP 是无状态协议
我们知道 HTTP 是 无状态协议,所以我们如果想让服务器知道我们是谁,并且根据之前我的信息简化我本次的操作的话,那么就需要服务器和客户端进行配合来实现 “有状态”。
如果不太理解,我们做一个类比。
我们去餐厅吃饭,哪怕我们每天都去,那边的服务员都无法记住我们昨天吃了什么,如果你跟他说 “服务员上菜,和昨天的一样,记到我帐上”,他做不到。他对任何人的服务态度、服务方式都是一样的,他既不会记得你曾经吃过什么,也不会知道你的账单是哪个,更不会去找你要账。
这个场景用到我们 Web 开发领域就是 HTTP 协议他只负责传输,既没有历史记录(你昨天吃了什么)也没有账户密码(你的账单),只要你访问它就根据你的 URL 进行处理,处理完返回结果。你再次访问,他就再次返回。这就是无状态。
5.2 基于 Session 的身份验证方式
如果我们想让它更智能就需要做一些额外的事情。
由于 HTTP 无法记录我们的任何状态,那就必须由服务器来记录了。
还是刚刚那个例子,如果服务员记性不好,我们就要在餐厅 建立会员机制,餐厅给我们一个 会员编号 来区分不同的会员,餐厅根据这个编号记录每个会员卡的消费情况、账单情况。每次我们只需要给服务员会员编号他就可以获取到我们的消费信息了。
在 Web 开发领域,就是 Cookie
和 Session
的关系,在我首次访问站点的时候,我们的服务器发送给浏览器一个 Cookie
,浏览器记录了一个 Cookie
存储我们的 sessionID
,通过这个 sessionID
可以在服务器找到一个 Session
,里面可以记录各种自定义信息。
如图所示,Cookie
存储在浏览器,根据站点域名进行划分,不同域名的 Cookie
一般情况下是不会互相混用的(关于cookie的详细机制请自行百度)。
这种传统的 Session
方式就是用户保留会员编号,然后由餐厅记录个人信息的方式。
这里所言过程的就是经典的 Session
机制的身份验证。
5.3 基于 JWT 的身份验证方式
了解了上面的 Session
机制,我们再理解 JWT
就变的特别简单。
我们需要在服务端存储为登录的用户生成的 Session
,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们还需要在服务端定期的去清理过期的 Session
。
用户有很多,服务器对每个用户都记录的话,对服务器的压力会比较大。
而 JWT
机制的出现恰好就弥补了这个不足。
还是以刚才餐厅会员为例,这次餐厅不给我们会员编号,而是直接给了我们一张 会员卡 —— 卡中可以记录用户的一些信息,当我们拿卡去餐厅的时候,服务员一刷卡就可以获取我们的信息。
回到 Web开发领域,就是 Cookie
里面记录的内容的变化,Cookie
里面直接记录我们的具体消费信息,服务器拿到 Cookie
直接可以获得我们的相应信息,不再需要自行记录,也不需要查询,只需要“解码”和“验证”。
下图我们对比 Session
机制和 JWT
机制中 Cookie 存储内容的不同:
- 第一条记录是
Session
机制: Cookie 只记录了 session 的 id,服务器获取到 cookie 之后需要根据这个 cookie 获取到对应的 session,然后在 session 里面获取用户信息。 - 第二条记录是
JWT
机制:在 cookie 里面存储更多信息,直接记录我们的具体的消息,服务器获取到 Cookie 之后只要解码也就获取这些信息,而不需要去查询数据库。
回到 JWT
机制,服务器为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。服务器就不保存任何 Session
数据了。这样就简化了服务器端架构的设计:
- 此时服务器变成无状态了,从而比较容易实现扩展
- 将原本服务器的存储成本转移到客户端存储,从而缓解了数据存储、管理的压力
从整体来看,JWT 机制的引入,其实是 去中心化 的一种具体实现,将原本服务器的存储成本转移到客户端存储,从而简介了服务器的 Session
管理设计,也让处理效率变得高效。
5.4 常用的 JWT 的身份验证架构
通常基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录,常用身份验证的架构流程如下:
如图所示,存在3个角色:authentication server
(登录/授权服务器),user
(用户),app server
(应用服务器)。
- 用户通过授权服务器的登录系统去登录,授权服务器把
JWT
传给用户。 - 用户客户端收到
Token
以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里 - 用户访问应用服务器的
API
时,带上 JWT - 服务端收到请求,然后去验证客户端请求里面带着的
Token
,如果验证成功,就向客户端返回请求的数据
在这个过程中,只有身份验证服务器和应用服务器知道秘钥是什么。如果身份验证服务器和应用服务器完全独立,则应用服务器的 JWT 校验工作也可以交由认证服务器完成。(因此 JWT 也适合做单点登录功能)
可以看到,这是一套无状态的验证机制,不必在内存中保存用户状态。用户访问时自带 JWT
,无需像传统应用使用 Session
,应用可以做到更多的解耦和扩展。同时,JWT 还可以保存用户的数据,减少数据库访问。
6、JWT 的优势和劣势
通过上面的介绍,相信你已经掌握了 JWT
实现原理和相关知识点。不过当你开心地将某项技术应用到你的应用中,你必须充分地知道这项技术的优势,以及它所带来的局限性和风险。
6.1 使用 JWT 的优势
使用JWT保护应用安全,至少可以获得以下优势:
- 更少的数据库连接:因其基于算法来实现身份认证,在使用JWT时查询数据的次数更少(更少的数据连接不等于不连接数据库),降低服务器查询数据库的次数,可以获得更快的系统响应时间。
- 构建更简单:如果应用程序本身是无状态的,那么选择
JWT
可以加快系统构建过程。 - 跨服务调用:可以构建一个认证中心来处理用户身份认证和发放签名的工作,其他应用服务在后续的用户请求中不需要(理论上)在询问认证中心,可使用自有的公钥对用户签名进行验证。
- 无状态:不需要向传统的Web应用那样将用户状态保存于Session中。
6.2 使用 JWT 的劣势
任何技术框架都有 自身的局限性,不可能一劳永逸,JWT
也不例外。它存在以下劣势:
- 严重依赖于秘钥:
JWT
的生成与解析过程都需要依赖于秘钥(Secret
),且都以硬编码的方式存在于系统中(也有放在外部配置文件中的)。如果秘钥不小心泄露,系统的安全性将受到威胁。 - JWT 的最大缺点是无法作废已颁布的令牌:由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- 服务端无法主动推送消息:服务端由于是无状态的,将无法使用像Session那样的方式推送消息到客户端,例如过期时间将至,服务端无法主动为用户续约,需要客户端向服务端发起续约请求。
- 冗余的数据开销:一个
JWT
签名的大小要远比一个Session
ID 长很多,如果对有效载荷(payload
)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。如果放在 Local Storage,则可能受到XSS
攻击。
7. 安全风险控制
考虑这样一个问题:如果客户端的 JWT
令牌泄露或者被盗取,会发生什么严重的后果?有什么补救措施?
首先我们看一下使用 JWT 可能带来的风险
7.1 使用 JWT 带来的风险
- 如果单纯依靠 JWT 解决用户认证的所有问题,那么系统的安全性将是脆弱的。
- 由于
JWT
令牌存储于客户端中,一旦客户端存储的令牌发生泄露事件或者被攻击,攻击者就可以轻而易举的伪造用户身份去 修改/删除 系统资源。 - 虽然
JWT
自带过期时间,但在过期之前,攻击者可以肆无忌惮的操作系统数据。通过算法来校验用户身份合法性是JWT
的优势,也是最大的弊端 —— 太过于依赖算法。反观传统的用户认证措施,通常会包含多种组合,如手机验证码,人脸识别,语音识别,指纹锁等。 - 用户名和密码只做用户身份识别使用,当用户名和密码泄露后,在遇到敏感操作时(如新增,修改,删除,下载,上传),都会采用其他方式对用户的合法性进行验证(发送验证码,邮箱验证码,指纹信息等)以确保数据安全。
总而言之,与传统的身份验证方式相比,JWT
过多的依赖于算法,缺乏灵活性,而且服务端往往是被动执行用户身份验证操作,无法及时对异常用户进行隔离。(这是最为根本的特征,这是考试重点,可以做笔记了)
其实不管是基于 Sessions
还是基于 JWT
,一旦密令被盗取,都是一件棘手的事情。下面介绍 JWT
发生令牌泄露是该采取什么样的措施(包含但不局限于此)。
7.2 风险控制手段建议
为了防止用户 JWT
令牌泄露而威胁系统安全,可以在以下方面完善系统功能:
- 清除已泄露的令牌:最直接也容易实现。将
JWT
令牌在服务端也存储一份,若发现有异常的令牌存在,则从服务端将此异常令牌清除。当用户发起请求时,强制用户重新进行身份验证,直至验证成功。服务端令牌的存储,可以借助Redis
等缓存服务器进行管理,也可使用 Ehcache 将令牌信息存储在内存中。 - 敏感操作保护:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,定期(
30
分钟,15
分钟甚至更短)检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。如果身份验证不通过,则终止请求,并要求重新验证用户身份信息。 - 地域检查:通常用户会在一个相对固定的地理范围内访问应用程序,可以将地理位置信息作为辅助来甄别。如果发现用户A由经常所在的地区1变到了相对较远的地区2,或者频繁在多个地区间切换,不管用户有没有可能在短时间内在多个地域活动(一般不可能),都应当终止当前请求,强制用户重新进行验证身份,颁发新的 JWT 令牌,并提醒(或要求)用户重置密码。
- 监控请求频率:如果
JWT
密令被盗取,攻击者或通过某些工具伪造用户身份,高频次的对系统发送请求,以套取用户数据。针对这种情况,可以监控用户在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户密令是有问题的。例如 1 秒内连续超过 5 次请求,则视为用户身份非法,服务端终止请求并强制将该用户的JWT 密令清除,然后回跳到认证中心对用户身份进行验证。 - 客户端环境检查:对于一些移动端应用来说,可以将用户信息与设备(手机,平板)的机器码进行绑定,并存储于服务端中,当客户端发起请求时,可以先校验客户端的机器码与服务端的是否匹配,如果不匹配,则视为非法请求,并终止用户的后续请求。
8. 最佳实践
当你充分了解了 JWT 的技术细节、处理的场景,那么获得一套关于 JWT 使用的最佳实践,也就水到渠成:
- 在使用 JWT 的时候一定要注意别携带敏感信息,令牌别暴露了
- 在 Web 应用中,别把
JWT
当做Session
使用。如果想要 Session,绝大多数情况下,传统的Cookie-Session
机制工作得更好 -
JWT
适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存JWT
,真正实现无状态。 - 为了减少盗用,
JWT
不应该使用HTTP
协议明码传输,要使用 HTTPS 协议传输。 - 当你建立一套基于
JWT
的用户验证的时候,一定要同时建立一套相对应的风控机制,确保风险发生时风险可控 & 及时止损。
9. 小结
JWT
的出现,为解决 Web 应用安全性问题提供了一种新思路。但 JWT 并非全能,仍然需要做很多复杂的工作才能提升系统的安全性。
当然,世上没有完美的解决方案,系统的安全性需要开发者积极主动地去提升,其过程是漫长且复杂的。
本文的撰写时阅读参考了一些文章,感谢以下文章给予了很大帮助。
- JWT 介绍:非常流行的在线工具网站,这个在线工具使我们能够插入令牌以查看其有效载荷。一旦我们插入令牌后,jwt.io会对其进行解码并显示其有用数据
- JWT入坑爬坑指南:较为详细地描述了 JWT 的原理和利弊,本文的撰写参考了很多本文内容。
- 彻底理解JWT认证:言简意赅的总结
- node使用jwt来创建token和解析token:详细用本地 node.js 方法来演示
- Encode or Decode JWTs:在线工具网站,自动生成对应编程语言的代码
- 不要用JWT替代session管理(上):全面了解Token,JWT,OAuth,SAML,SSO:主要聊聊 JWT 在 session 管理方面的优势和劣势,同时尝试解决这些劣势,看看成本和代价有多少
- 了解常见的 authorize / authenticate 技术和相关概念:快速了解 JWT 的应用
- Json Web Token——JWT 笔记:对“登录”在无状态协议下的实现有了更加深入的理解
- Cryptocurrency vs Cryptographic Tokens: Benefits, Features & Rewards:Cryptographic Token 概念区别
- The Anatomy of a JSON Web Token:JWT 入门知识,通俗易懂
- A plain English introduction to JSON web tokens (JWT): what it is and what it isn’t:长文详解
- JWT使用总结:很中肯的 JWT 使用总结