本文长约 1w 字,阅读耗时约 25 min
本文要是讲 JWT
(JSON Web Token) ,我刚接触这个这个知识点的时候,心路历程是这样的:
JWT
?为什么要去用 JWT
?JWT
繁琐不繁琐,怎么用?JWT
的风险和收益分别是什么?如果你有以上和我相似的疑惑句,那么本文将对你有所帮助。
本文将从 JWT 的概念、基本原理、如何用 Node.js 创建、适用范围 和 风险控制 方面来剖析 JWT ,让你清晰地了解哪些情况下适用 JWT,以及在使用时的注意事项。
越来越多的开发者开始学习并在实际项目中运用 JWT
(JSON Web Token)技术来保护应用安全,很多公司的应用程序也开始使用 JWT
来管理用户会话信息。
任何技术框架都有 自身的局限性,不可能一劳永逸,JWT 也不例外。众所周知,如果账户信息(用户名和密码)泄露,存储在服务器上的隐私数据将受到毁灭性的打击,如果是管理员的账户信息泄露,系统还有被攻击的危险。那么,JWT
的信息发生泄露,会带来什么样的影响?该如何防范?
要想解答这些疑惑,我们需要稍微全面地了解 JWT
。
那么我们先从了解 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) 的原理
JSON Web Token (JWT) 是一个开放标准(RFC 7519
),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
看不懂?没关系,上面那段话好像是机翻的...
翻译成人话,就是说 JWT
是一个加密标准,当用户拿到这个加密 Token 的话,相当于拿到了一份证明自己身份的数字证书,就可以出入加密系统。我们把计算机系统想成公司,JWT
就是公司发给你的工牌,你拿着公司工牌,就可以刷卡进入园区、食堂吃饭结算、进入会议室等等公司需要认证授权的地方。
RFC 7519 规定了 JWT 的格式,我们看一下 JWT 长啥样。总体上来看,JWT 以 .
分隔成 3 段,示例一下:
aaaaaa.bbbbbb.cccccc
这 3 段分别代表如下含义:
aaaaaa
部分,bbbbbb
部分cccccc
部分json-web-token-overview
如果不太好理解的话,以日常生活中 开货车 为例来对比:
header
相当于你货车的 车牌payload
相当于你货车所拉的 货物signature
就你驾驶员的 驾驶证/行驶证这么一类比是否就清晰多了?
不仅 JWT 可以用货车来类比,计算机网络相关的概念(诸如 HTTPS、TCP 啊)等知识都可以这么类比方便自己理解。
接下来我们详细讲解生成这些 aaaaaa
、bbbbbb
、cccccc
字符串的具体过程。
这 "车牌" 是一个 JSON 对象,描述 JWT 的元数据,包含两部分:
typ
:表示这个令牌(token
)的类型(type
),在 JWT 协议里没得选,只能是 JWT
alg
:表示你后面你在 Signature
部分(即上述 ccccccc
部分)所使用的加密算法。例子如下:
{
"alg": "HS256",
"typ": "JWT"
}
常用的算法有
HMAC SHA256
或RSA
,完整的算法类型我从官方上截了个图:
最后将这个 JSON 对象使用 Base64URL
算法转成字符串,就变成了图示中的 aaaaaa
了。
这 “货物” 部分也是一个 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
了。
Signature
部分是对前两部分的签名,防止数据篡改。
secret
)。这个密钥只有服务器才知道,不能泄露给用户。Header
里面指定的签名算法(默认是 HMAC SHA256
),按照下面的公式产生签名。HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这个算出的签名就是上面所述的 cccccc
字符串了。
算出签名以后,我们把 Header
、Payload
、Signature
三个部分拼成一个字符串,每个部分之间用"点"(.)分隔。
至此你就获得了一个 JWT
—— 是不是简单到令你窒息?!
贴一张从网上找来的图,如果你现在一眼看这张 JWT 图觉得非常直观,那说明你就已经掌握本节内容了。
JWT 图示
前面提到,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.io 官网提供实时预览功能
现在,我们已经了解了 JWT
的基本原理,接下来将使用 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,它的优势体现在哪里?
要想了解为什么,就需要先了解 JWT
的应用场景 —— 用于 Web 开发领域的身份验证。
我们知道 HTTP 是 无状态协议,所以我们如果想让服务器知道我们是谁,并且根据之前我的信息简化我本次的操作的话,那么就需要服务器和客户端进行配合来实现 “有状态”。
如果不太理解,我们做一个类比。
我们去餐厅吃饭,哪怕我们每天都去,那边的服务员都无法记住我们昨天吃了什么,如果你跟他说 “服务员上菜,和昨天的一样,记到我帐上”,他做不到。他对任何人的服务态度、服务方式都是一样的,他既不会记得你曾经吃过什么,也不会知道你的账单是哪个,更不会去找你要账。
这个场景用到我们 Web 开发领域就是 HTTP 协议他只负责传输,既没有历史记录(你昨天吃了什么)也没有账户密码(你的账单),只要你访问它就根据你的 URL 进行处理,处理完返回结果。你再次访问,他就再次返回。这就是无状态。
如果我们想让它更智能就需要做一些额外的事情。
由于 HTTP 无法记录我们的任何状态,那就必须由服务器来记录了。
还是刚刚那个例子,如果服务员记性不好,我们就要在餐厅 建立会员机制,餐厅给我们一个 会员编号 来区分不同的会员,餐厅根据这个编号记录每个会员卡的消费情况、账单情况。每次我们只需要给服务员会员编号他就可以获取到我们的消费信息了。
在 Web 开发领域,就是 Cookie
和 Session
的关系,在我首次访问站点的时候,我们的服务器发送给浏览器一个 Cookie
,浏览器记录了一个 Cookie
存储我们的 sessionID
,通过这个 sessionID
可以在服务器找到一个 Session
,里面可以记录各种自定义信息。
Session-Cookie 关系示意
如图所示,Cookie
存储在浏览器,根据站点域名进行划分,不同域名的 Cookie
一般情况下是不会互相混用的(关于cookie的详细机制请自行百度)。
这种传统的 Session
方式就是用户保留会员编号,然后由餐厅记录个人信息的方式。
这里所言过程的就是经典的 Session
机制的身份验证。
了解了上面的 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
管理设计,也让处理效率变得高效。
通常基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录,常用身份验证的架构流程如下:
如图所示,存在3个角色:authentication server
(登录/授权服务器),user
(用户),app server
(应用服务器)。
JWT
传给用户。Token
以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里API
时,带上 JWTToken
,如果验证成功,就向客户端返回请求的数据在这个过程中,只有身份验证服务器和应用服务器知道秘钥是什么。如果身份验证服务器和应用服务器完全独立,则应用服务器的 JWT 校验工作也可以交由认证服务器完成。(因此 JWT 也适合做单点登录功能)
可以看到,这是一套无状态的验证机制,不必在内存中保存用户状态。用户访问时自带 JWT
,无需像传统应用使用 Session
,应用可以做到更多的解耦和扩展。同时,JWT 还可以保存用户的数据,减少数据库访问。
通过上面的介绍,相信你已经掌握了 JWT
实现原理和相关知识点。不过当你开心地将某项技术应用到你的应用中,你必须充分地知道这项技术的优势,以及它所带来的局限性和风险。
使用JWT保护应用安全,至少可以获得以下优势:
JWT
可以加快系统构建过程。任何技术框架都有 自身的局限性,不可能一劳永逸,JWT
也不例外。它存在以下劣势:
JWT
的生成与解析过程都需要依赖于秘钥(Secret
),且都以硬编码的方式存在于系统中(也有放在外部配置文件中的)。如果秘钥不小心泄露,系统的安全性将受到威胁。JWT
签名的大小要远比一个 Session
ID 长很多,如果对有效载荷(payload
)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。如果放在 Local Storage,则可能受到 XSS
攻击。考虑这样一个问题:如果客户端的 JWT
令牌泄露或者被盗取,会发生什么严重的后果?有什么补救措施?
首先我们看一下使用 JWT 可能带来的风险
JWT
令牌存储于客户端中,一旦客户端存储的令牌发生泄露事件或者被攻击,攻击者就可以轻而易举的伪造用户身份去 修改/删除 系统资源。JWT
自带过期时间,但在过期之前,攻击者可以肆无忌惮的操作系统数据。通过算法来校验用户身份合法性是 JWT
的优势,也是最大的弊端 —— 太过于依赖算法。反观传统的用户认证措施,通常会包含多种组合,如手机验证码,人脸识别,语音识别,指纹锁等。总而言之,与传统的身份验证方式相比,JWT
过多的依赖于算法,缺乏灵活性,而且服务端往往是被动执行用户身份验证操作,无法及时对异常用户进行隔离。(这是最为根本的特征,这是考试重点,可以做笔记了)
其实不管是基于 Sessions
还是基于 JWT
,一旦密令被盗取,都是一件棘手的事情。下面介绍 JWT
发生令牌泄露是该采取什么样的措施(包含但不局限于此)。
为了防止用户 JWT
令牌泄露而威胁系统安全,可以在以下方面完善系统功能:
JWT
令牌在服务端也存储一份,若发现有异常的令牌存在,则从服务端将此异常令牌清除。当用户发起请求时,强制用户重新进行身份验证,直至验证成功。服务端令牌的存储,可以借助 Redis
等缓存服务器进行管理,也可使用 Ehcache 将令牌信息存储在内存中。30
分钟,15
分钟甚至更短)检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。如果身份验证不通过,则终止请求,并要求重新验证用户身份信息。JWT
密令被盗取,攻击者或通过某些工具伪造用户身份,高频次的对系统发送请求,以套取用户数据。针对这种情况,可以监控用户在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户密令是有问题的。例如 1 秒内连续超过 5 次请求,则视为用户身份非法,服务端终止请求并强制将该用户的JWT 密令清除,然后回跳到认证中心对用户身份进行验证。当你充分了解了 JWT 的技术细节、处理的场景,那么获得一套关于 JWT 使用的最佳实践,也就水到渠成:
JWT
当做 Session
使用。如果想要 Session,绝大多数情况下,传统的 Cookie-Session
机制工作得更好JWT
适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存 JWT
,真正实现无状态。JWT
不应该使用 HTTP
协议明码传输,要使用 HTTPS 协议传输。JWT
的用户验证的时候,一定要同时建立一套相对应的风控机制,确保风险发生时风险可控 & 及时止损。JWT
的出现,为解决 Web 应用安全性问题提供了一种新思路。但 JWT 并非全能,仍然需要做很多复杂的工作才能提升系统的安全性。
当然,世上没有完美的解决方案,系统的安全性需要开发者积极主动地去提升,其过程是漫长且复杂的。
本文的撰写时阅读参考了一些文章,感谢以下文章给予了很大帮助。