1. 什么是JWT?
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
可以看出,JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则 。
并且, 使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。
2. JWT由哪些部分组成?
JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:
Header: 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。Payload: 用来存放实际需要传递的数据Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
JWT 通常是这样的:xxxxx.yyyyy.zzzzz。
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
你可以在 jwt.io 这个网站上对其 JWT 进行解码,解码之后得到的就是 Header、Payload、Signature 这三部分。
Header 和 Payload 都是 JSON 格式的数据,Signature 由 Payload、Header 和 Secret(密钥)通过特定的计算公式和加密算法得到。
Header(头部)
Header通常由两部分组成:
alg(Algorithm) :签名算法,比如HS256。typ(Type) :令牌类型,也就是JWT。
示例:
{
"alg": "HS256",
"typ": "JWT"
}
JSON 形式的 Header 被转换成 Base64 编码,成为 JWT 的第一部分。
Payload(负载)
Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。 Claims 分为三种类型:
- 标准声明(Registered Claims):预定义的一些声明,建议使用,但不是强制性的。
- 公共声明(Public Claims):这些声明不是JWT规范中定义的标准声明,但是它们是用于自定义和共享的一组常见声明。公共声明是由应用程序开发人员约定的,用于在不同组织之间共享信息。在使用公共声明时,需要确保相关方都理解并遵守了它们的含义。
- 私有声明(Private Claims):私有声明是由应用程序开发人员自定义的声明,用于满足特定的业务需求。它们是JWT规范之外的声明,只有发送方和接收方之间知道如何解释和使用这些声明。私有声明在不同的应用程序之间可能会有不同的含义。
下面是一些常见的标准声明:
+ "iss"(Issuer): 表示JWT的签发者,即标识谁创建了该JWT。
"sub"(Subject): 表示JWT的主题,即标识该JWT所代表的用户或实体。"aud"(Audience): 表示JWT的受众,即标识预期的接收者。"exp"(Expiration Time): 表示JWT的过期时间,即标识JWT的有效期限。在该时间之后,该JWT将不再被接受。"nbf"(Not Before): 表示JWT的生效时间,即在该时间之前,该JWT将不会被接受。"iat"(Issued At): 表示JWT的签发时间,即标识JWT的创建时间。"jti"(JWT ID): 表示JWT的唯一标识符,用于防止JWT重放攻击。
示例:
{
"uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
"sub": "1234567890",
"name": "John Doe",
"exp": 15323232,
"iat": 1516239022,
"scope": ["admin", "user"]
}
Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!
JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。
Signature
Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。
这个签名的生成需要用到:
- Header + Payload。
- 存放在服务端的密钥(一定不要泄露出去)。
- 签名算法。
签名的计算公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,这个字符串就是 JWT 。
3.如何基于JWT进行身份验证?
在基于 JWT 进行身份验证的的应用程序中,服务器通过 Payload、Header 和 Secret(密钥)创建 JWT 并将 JWT 发送给客户端。客户端接收到 JWT 之后,会将其保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。
简化后的步骤如下:
- 用户向服务器发送用户名、密码以及验证码用于登陆系统。
- 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
- 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
- 服务端检查 JWT 并从中获取用户相关信息。
两点建议:
- 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF(Cross-Site Request Forgery,跨站请求伪造) 风险。
- 请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 Authorization 字段中(
Authorization: Bearer Token)。
4. 如何防止JWT被篡改?
有了签名之后,即使 JWT 被泄露或者截获,黑客也没办法同时篡改 Signature、Header、Payload。
这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。
不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature、Header、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。
密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
5. 如何加强JWT的安全性?
可以通过以下措施来增强其安全性:
- 使用HTTPS:始终通过HTTPS协议传输JWT和其他敏感信息,这样可以加密通信,防止中间人攻击和数据篡改。
- 使用安全的签名算法:选择安全的签名算法,例如HMAC SHA256或RSA等,以确保签名的强度和不可伪造性。
- 使用密钥保护密钥:确保JWT的签名密钥是安全存储和管理的,不要将密钥暴露在不安全的环境中。
- 限制JWT的生命周期:在Payload中设置合理的过期时间(
exp),以确保JWT在一定时间后失效,减少JWT被滥用的可能性。 - 签名Payload:在生成JWT时,确保将Header和Payload进行签名,以保证JWT的完整性。
- 校验JWT签名:在验证JWT时,对JWT的Header和Payload重新计算签名,并与JWT中的Signature部分进行比较,确保JWT没有被篡改过。
- 不要存储敏感信息:避免在JWT的Payload中存储敏感信息,特别是未加密的情况下。敏感信息应该存储在安全的服务器端。
- 使用CSRF Token:在包含JWT的请求中,可以使用CSRF Token来防止跨站请求伪造攻击,确保请求是合法的。
- 验证JWT的接收者:在验证JWT时,校验JWT的"Audience"("aud")字段,确保JWT只发送给合法的接收者。
- JWT加密:如果有必要,可以使用JWT加密(JWE)来对JWT进行加密,保护其中的内容。
6. JWT 身份认证优缺点分析
JWT的优缺点
相比于 Session 认证的方式来说,使用 JWT 进行身份认证主要有下面 4 个优势。
优点:
- 无状态性:JWT是无状态的,服务器不需要在持久化存储中保留会话信息,降低了服务器的负担,适合分布式和高扩展性的应用
- 安全性:JWT使用数字签名或加密算法,确保令牌在传输过程中不被篡改,防止信息被恶意修改或伪造,提高了认证的安全性。
- 跨平台:JWT是基于JSON格式的标准,因此可以在不同的平台和语言之间轻松传递和解析,方便在不同系统中共享认证信息。
- 可扩展性:JWT允许添加自定义的声明,使其适用于各种场景,可以根据需要存储额外的用户信息或其他数据。
缺点:
- 无法撤销令牌:一旦JWT被签发,其有效期内令牌将一直有效,无法撤销,除非等待其过期或通过其他方式强制使其失效。
- 信息存储:由于JWT将所有信息都存储在令牌中,所以令牌会变得比较大,可能导致网络传输和存储成本增加。
- 安全性依赖密钥管理:JWT的安全性依赖于密钥的管理,如果密钥不当泄露或遭到攻击,可能导致令牌被篡改或伪造,造成安全漏洞。
- 无法处理会话管理:JWT是无状态的,无法在服务器端主动使令牌失效或处理会话管理,这些功能需要在应用层自行实现。
总体而言,JWT作为一种轻量级的身份认证机制,在无状态场景下具有一定优势,但需要开发者在使用时注意安全性和合理规划令牌的有效期。同时,对于某些需要即时撤销令牌或具有复杂会话管理需求的应用,可能需要考虑其他身份认证方案。
JWT 身份认证常见问题及解决办法
注销登录等场景下 JWT 还有效
与之类似的具体相关场景有:
- 退出登录;
- 修改密码;
- 服务端修改了某个用户具有的权限或者角色;
- 用户的帐户被封禁/删除;
- 用户被服务端强制注销;
- 用户被踢下线;
- ......
这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 JWT 认证的方式就不好解决了。我们也说过了,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。
那我们如何解决这个问题呢?查阅了很多资料,简单总结了下面 4 种方案:
-
将 JWT 存入内存数据库 : 将 JWT 存入 DB 中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 发送请求都要先从 DB 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。
-
黑名单机制 : 和上面的方式类似,使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。前两种方案的核心在于将有效的 JWT 存储起来或者将指定的 JWT 拉入黑名单。虽然这两种方案都违背了 JWT 的无状态原则,但是一般实际项目中我们通常还是会使用这两种方案。
-
修改密钥 (Secret) : 我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大:如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。
-
保持令牌的有效期限短并经常轮换 : 很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。
另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。另一种比较简单的方式:使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。
JWT 的续签问题
JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录?
我们先来看看在 Session 认证中一般的做法:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。
JWT 认证的话,我们应该如何解决续签问题呢?简单总结了下面 4 种方案:
-
类似于 Session 认证中的做法 : 这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。
-
每次请求都返回新 JWT : 这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
-
JWT 有效期设置到半夜 : 这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
-
用户登录返回两个 JWT : 第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。
这种方案的不足是:
- 需要客户端来配合;
- 用户注销的时候需要同时保证两个 JWT 都无效;
- 重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT);
- 存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。