JWT基础概念

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由哪些部分组成?

此图片来源于:https://supertokens.com/blog/oauth-vs-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 分为三种类型:

  1. 标准声明(Registered Claims):预定义的一些声明,建议使用,但不是强制性的。
  2. 公共声明(Public Claims):这些声明不是JWT规范中定义的标准声明,但是它们是用于自定义和共享的一组常见声明。公共声明是由应用程序开发人员约定的,用于在不同组织之间共享信息。在使用公共声明时,需要确保相关方都理解并遵守了它们的含义。
  3. 私有声明(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 里面,以后客户端发出的所有请求都会携带这个令牌。


JWT身份验证示意图

简化后的步骤如下:

  1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。
  2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
  3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
  4. 服务端检查 JWT 并从中获取用户相关信息。

两点建议:

  1. 建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF(Cross-Site Request Forgery,跨站请求伪造) 风险。
  2. 请求服务端并携带 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的安全性?

可以通过以下措施来增强其安全性:

  1. 使用HTTPS:始终通过HTTPS协议传输JWT和其他敏感信息,这样可以加密通信,防止中间人攻击和数据篡改。
  2. 使用安全的签名算法:选择安全的签名算法,例如HMAC SHA256或RSA等,以确保签名的强度和不可伪造性。
  3. 使用密钥保护密钥:确保JWT的签名密钥是安全存储和管理的,不要将密钥暴露在不安全的环境中。
  4. 限制JWT的生命周期:在Payload中设置合理的过期时间(exp),以确保JWT在一定时间后失效,减少JWT被滥用的可能性。
  5. 签名Payload:在生成JWT时,确保将Header和Payload进行签名,以保证JWT的完整性。
  6. 校验JWT签名:在验证JWT时,对JWT的Header和Payload重新计算签名,并与JWT中的Signature部分进行比较,确保JWT没有被篡改过。
  7. 不要存储敏感信息:避免在JWT的Payload中存储敏感信息,特别是未加密的情况下。敏感信息应该存储在安全的服务器端。
  8. 使用CSRF Token:在包含JWT的请求中,可以使用CSRF Token来防止跨站请求伪造攻击,确保请求是合法的。
  9. 验证JWT的接收者:在验证JWT时,校验JWT的"Audience"("aud")字段,确保JWT只发送给合法的接收者。
  10. JWT加密:如果有必要,可以使用JWT加密(JWE)来对JWT进行加密,保护其中的内容。

6. JWT 身份认证优缺点分析

JWT的优缺点

相比于 Session 认证的方式来说,使用 JWT 进行身份认证主要有下面 4 个优势。

优点:

  1. 无状态性:JWT是无状态的,服务器不需要在持久化存储中保留会话信息,降低了服务器的负担,适合分布式和高扩展性的应用
  2. 安全性:JWT使用数字签名或加密算法,确保令牌在传输过程中不被篡改,防止信息被恶意修改或伪造,提高了认证的安全性。
  3. 跨平台:JWT是基于JSON格式的标准,因此可以在不同的平台和语言之间轻松传递和解析,方便在不同系统中共享认证信息。
  4. 可扩展性:JWT允许添加自定义的声明,使其适用于各种场景,可以根据需要存储额外的用户信息或其他数据。

缺点:

  1. 无法撤销令牌:一旦JWT被签发,其有效期内令牌将一直有效,无法撤销,除非等待其过期或通过其他方式强制使其失效。
  2. 信息存储:由于JWT将所有信息都存储在令牌中,所以令牌会变得比较大,可能导致网络传输和存储成本增加。
  3. 安全性依赖密钥管理:JWT的安全性依赖于密钥的管理,如果密钥不当泄露或遭到攻击,可能导致令牌被篡改或伪造,造成安全漏洞。
  4. 无法处理会话管理:JWT是无状态的,无法在服务器端主动使令牌失效或处理会话管理,这些功能需要在应用层自行实现。

总体而言,JWT作为一种轻量级的身份认证机制,在无状态场景下具有一定优势,但需要开发者在使用时注意安全性和合理规划令牌的有效期。同时,对于某些需要即时撤销令牌或具有复杂会话管理需求的应用,可能需要考虑其他身份认证方案。

JWT 身份认证常见问题及解决办法

注销登录等场景下 JWT 还有效

与之类似的具体相关场景有:

  • 退出登录;
  • 修改密码;
  • 服务端修改了某个用户具有的权限或者角色;
  • 用户的帐户被封禁/删除;
  • 用户被服务端强制注销;
  • 用户被踢下线;
  • ......

这个问题不存在于 Session 认证方式中,因为在 Session 认证方式中,遇到这种情况的话服务端删除对应的 Session 记录即可。但是,使用 JWT 认证的方式就不好解决了。我们也说过了,JWT 一旦派发出去,如果后端不增加其他逻辑的话,它在失效之前都是有效的。

那我们如何解决这个问题呢?查阅了很多资料,简单总结了下面 4 种方案:

  1. 将 JWT 存入内存数据库
    将 JWT 存入 DB 中,Redis 内存数据库在这里是不错的选择。如果需要让某个 JWT 失效就直接从 Redis 中删除这个 JWT 即可。但是,这样会导致每次使用 JWT 发送请求都要先从 DB 中查询 JWT 是否存在的步骤,而且违背了 JWT 的无状态原则。

  2. 黑名单机制
    和上面的方式类似,使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。前两种方案的核心在于将有效的 JWT 存储起来或者将指定的 JWT 拉入黑名单。虽然这两种方案都违背了 JWT 的无状态原则,但是一般实际项目中我们通常还是会使用这两种方案。

  3. 修改密钥 (Secret) :
    我们为每个用户都创建一个专属密钥,如果我们想让某个 JWT 失效,我们直接修改对应用户的密钥即可。但是,这样相比于前两种引入内存数据库带来了危害更大:如果服务是分布式的,则每次发出新的 JWT 时都必须在多台机器同步密钥。为此,你需要将密钥存储在数据库或其他外部服务中,这样和 Session 认证就没太大区别了。如果用户同时在两个浏览器打开系统,或者在手机端也打开了系统,如果它从一个地方将账号退出,那么其他地方都要重新进行登录,这是不可取的。

  4. 保持令牌的有效期限短并经常轮换
    很简单的一种方式。但是,会导致用户登录状态不会被持久记录,而且需要用户经常登录。

另外,对于修改密码后 JWT 还有效问题的解决还是比较容易的。另一种比较简单的方式:使用用户的密码的哈希值对 JWT 进行签名。因此,如果密码更改,则任何先前的令牌将自动无法验证。

JWT 的续签问题

JWT 有效期一般都建议设置的不太长,那么 JWT 过期后如何认证,如何实现动态刷新 JWT,避免用户经常需要重新登录?

我们先来看看在 Session 认证中一般的做法:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。

JWT 认证的话,我们应该如何解决续签问题呢?简单总结了下面 4 种方案:

  1. 类似于 Session 认证中的做法
    这种方案满足于大部分场景。假设服务端给的 JWT 有效期设置为 30 分钟,服务端每次进行校验时,如果发现 JWT 的有效期马上快过期了,服务端就重新生成 JWT 给客户端。客户端每次请求都检查新旧 JWT,如果不一致,则更新本地的 JWT。这种做法的问题是仅仅在快过期的时候请求才会更新 JWT ,对客户端不是很友好。

  2. 每次请求都返回新 JWT
    这种方案的的思路很简单,但是,开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。

  3. JWT 有效期设置到半夜
    这种方案是一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。

  4. 用户登录返回两个 JWT
    第一个是 accessJWT ,它的过期时间 JWT 本身的过期时间比如半个小时,另外一个是 refreshJWT 它的过期时间更长一点比如为 1 天。客户端登录后,将 accessJWT 和 refreshJWT 保存在本地,每次访问将 accessJWT 传给服务端。服务端校验 accessJWT 的有效性,如果过期的话,就将 refreshJWT 传给服务端。如果有效,服务端就生成新的 accessJWT 给客户端。否则,客户端就重新登录即可。

这种方案的不足是:

  • 需要客户端来配合;
  • 用户注销的时候需要同时保证两个 JWT 都无效;
  • 重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况(可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT);
  • 存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容