JWT全称JSON Web Token,是一种为网络应用环境间传递声明而执行的基于JSON的开发标准(RFC 7519)。
- 实现页面刷新时判断JWT是否存在或过期
跨域认证
- JWT是目前最流行的跨域认证解决方案
用户认证的普通流程
- 客户端向服务器发送账户和密码
- 服务端验证后在当前会话中保存相关数据,比如角色、登录时间等。
- 服务器向客户端返回一个
session_id
并写入客户端Cookie - 客户端后续每次请求前先从Cookie中获取
session_id
,然后将其传回服务器。 - 服务器接收到
session_id
后查找对应的用户数据以判断用户身份
普通用户认证模式的缺陷在于扩展性差,单机使用问题不大,若服务器集群或是跨域的服务导向架构,就要求Session数据共享才能读取到会话信息。
比如不同主机的站点是同一家公司的关联服务,现在要求用户只要在其中一个站点登录,再访问另一个则会自动登录,如何实现呢?解决方案可分为两种:
- Session数据持久化,写入数据库或其他持久层。服务收到请求后向持久层请求数据。此种方式的好处在于架构清晰,但工程量大。另外一旦持久层挂掉则单点失败。
- 服务器不保存Session数据,所有数据都保存在客户端,每次请求都发回给服务器。JWT就是这种方案的一个代表。
原理分析
JWT原理是服务器认证通过后会生成一个JSON对象,将JSON对象经过编码和签名生成Token,在发回给客户端,客户端接收到服务器返回的Token后可存储在Cookie或本地的localStorage中。之后客户端和服务端每次通信时,都需要发回Token。虽然可以将其添加到Cookie中自动发送,但无法实现跨域。因此最佳做法是将其放到HTTP请求头的Authorization字段中,并添加Bearer标注,当然也可将其放到POST请求的数据中。服务器完全靠这个Token中的数据来认定用户的身份。
为了防止用户在客户端篡改这个JSON对象,服务器在生成对象时需要对其进行签名。这样服务器就不再保存任何Session数据,也就是说服务器变成无状态的了,从而比较容易实现扩展。
注意事项
- JWT令牌可直接被用于认证,也可用于交换信息。有效地使用JWT可降低服务器查询数据库的次数。
- JWT默认并未加密,任何人都可以读取,因此不要存储秘密信息。也可以再使用密钥进行加密。
- JWT最大缺点是由于服务器不保存Session状态,因此无法在使用过程中中止某个Token或更改其权限。也就使说一旦JWT签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT本身包含了认证信息,一旦泄露任何人都可以获得该令牌的所有权限。因此为了减少盗用,JWT的有效期应该设置的尽量短。另外对于比较重要或敏感的权限,应在此对用户进行认证。
- 为减少JWT被盗用,应使用HTTPS传输。
HTTP Authorization
由于HTTP是无状态的,在浏览器和HTTP服务器之间通信时可通过Cookie来识别身份,但对于桌面应用程序,则无法实现。因此诞生了HTTP基本认证(Basic Authorization)。桌面应用通常会将“用户名:密码”使用Base64算法编码后的字符串放到HTTP请求头的Authorization字段中发送给服务端。
Authorization: Basic base64encode(username+":"+password)
当浏览器访问使用HTTP基本认证的站点时,浏览器会提示你输入用户名和密码。若账户或密码输入错误则返回返回一个401 Unauthorization的响应,同时会在响应头的WWW-Authenticate
中添加相关信息。
Bearer Token
Bearer本意为持票人或送信人,Bearer Token的设计概念类似于支票兑换的场景。类似银行汇票手持支票坐等兑付的受款人,银行作为承兑人,会以票面上出票人的签名作为凭证来行使保障受款人收款的义务。在Bearer Token概念中,“票”也就时Token,Token作为一种凭证,服务端必须承认持有Token的用户拥有的对应权利,因此此时持有Token的用户就时一个Bearer。
Authorization: Bearer <token>
数据结构
JWT令牌设计的紧凑且安全,特别适用于分布式站点的单点登录(SSO)。JWT的声明一般被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,以便从资源服务器获取资源,同时也增加了一些额外的业务逻辑所必须的声明信息。
例如:典型的JWT令牌字符串,注意JWT内部是没有换行的。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U
JWT实际上由三段文本通过点号.
连接构成,即JWT实际是由三部分组成的。
组成 | 描述 | 示例 |
---|---|---|
Header | 头部 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 |
Payload | 载荷 | eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9 |
Signature | 签名 | Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U |
头部
- Header头部是一个JSON对象,用于描述JWT的元数据。
- Header头部字符串是将JSON对象经过Base64URL算法编码串行化后形成的字符串
- Header头部承载着两块:声明类型和声明加密的算法,算法常采用HMAC SHA256。
{"type":"JWT", "alg":"HS256"}
组成 | 全称 | 描述 |
---|---|---|
typ | type | 令牌类型,JWT令牌统一写为JWT。 |
alg | algorithm | 签名算法,默认为HMAC SHA256,简写为HS256。 |
载荷
- Payload载荷又称为负载,存储JWT的有效信息。
- Payload部分也是一个JSON对象,用来存放实际需要传递的数据。
- Payload载荷部分也是由JSON对象经Base64URL算法编码串行化后形成的字符串
JWT有效信息包括标准中注册声明、公共的声明、私有的声明三部分。JWT规定了7个官方字段供选用,可自定义私有字段。
字段 | 标准 | 描述 |
---|---|---|
jti | JWT ID | 编号 |
sub | subject | 主题 |
aud | audiene | 受众 |
iss | issuer | 签发人 |
iat | Issued At | 签发时间 |
nbf | Not Before | 生效时间 |
exp | expiration time | 过期时间 |
签名
Signature签名的作用是对前两部分执行签名以防止篡改,签名前首先需要指定一个密钥(secret)。密钥只有服务器才知道,不能泄露给用户。使用Header头部中指定的签名算法执行签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + secret)
当计算出签名后,将Header、Payload、Signature三部分使用点号.
拼接即可形成令牌,返回给客户端。
Base64URL
Header和Payload串行化算法使用的是Base64URL,此算法和Base64算法基本类似,但也有微小不同。
JWT作为令牌会放到URL作为GET参数传递。因此会采用Base64URL算法而非普通的Base64算法,因为Base64URL算法会将URL中具有特殊含义的字符+
、/
、=
替换或省略。
Base64 | Base64URL |
---|---|
= | 省略掉 |
+ | 替换为- |
/ | 替换为_ |
jsonwebtoken
$ npm i -D jsonwebtoken@latest
-
jwt.sign(data, secret, options)
生成签名 -
jwt.decode(token)
解码令牌
const jwt = require("jsonwebtoken")
const params = {id:1, username:"admin"}
const secret = "x1fd34alml"
const options = {expiresIn:"2h", algorithm:"HS256"}
const token = jwt.sign(params, secret, options)
const {id, username} = jwt.decode(token)
jwt-decode
-
jwt-decode
包可用于解析Base64URL编码格式的JWT令牌,不提供验证,适用于浏览器应用程序。
$ npm i -D jwt-decode
解码令牌
const jwtdecode = require("jwt-decode)
console.log(jwtdecode(token))