WinddSnow

Java面试题16JWT

字数统计: 2.6k阅读时长: 9 min
2022/10/22

JWT

JSON Web token 简称 JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他 session 数据。此特性便于可伸缩性, 同时保证应用程序的安全。

在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。

每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用 Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证 token。

组成

一个 JWT 实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

  • 头部(Header)
    头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个 JSON 对象。{“typ”:”JWT”,”alg”:”HS256”}-在 头 部 指 明 了 签 名 算 法 是 HS256 算 法 。 我 们 进 行 BASE64 编 码( http://base64.xpcha.com/ ) , 编 码 后 的 字 符 串 如 下 :eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

  • 载荷(playload)

    载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

    • 1标准中注册的声明(建议但不强制使用)

      • iss: jwt 签发者
      • sub: jwt 所面向的用户
      • aud: 接收 jwt 的一方
      • exp: jwt 的过期时间,这个过期时间必须要大于签发时间
      • nbf: 定义在什么时间之前,该 jwt 都是不可用的.
      • iat: jwt 的签发时间
      • jti: jwt 的唯一身份标识,主要用来作为一次性 token
    • 2公共的声明

      公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。

    • 3私有的声明

      • 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64 是对称解密的,意味着该部分信息可以归类为明文信息。
      • 这个指的就是自定义的 claim。比如前面那个结构举例中的 admin 和 name 都属于自定的 claim。这些 claim 跟 JWT 标准规定的 claim 区别在于:JWT 规定的 claim
      • JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证(还不知道是否能够验证);而 private claims 不会验证,除非明确告诉接收方要对这些 claim进行验证以及规则才行。
  • 签证(signature)

    jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:

    • header (base64 后的)
    • payload (base64 后的)
    • secret

    这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用.连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了jwt 的第三部分。

  • 注意

    secret 是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行jwt 的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt 了。

使用场景

  1. 一次性验证

    比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其它可能的账户……这种场景就和 jwt 的特性非常贴近,jwt 的payload 中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。

  2. restful api 的无状态认证

    使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改……springsecurity oauth jwt 提供了一套完整的 jwt 认证体系,个人的经验来看:使用 oauth2或 jwt 来做 restful api 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后者实现简单。

  3. 使用 jwt 做单点登录+会话管理(不推荐)

JWT token 泄露了怎么办?(常问)

使用 https 加密你的应用,返回 jwt 给客户端时设置 httpOnly=true 并且使用cookie 而不是 LocalStorage 存储 jwt,这样可以防止 XSS 攻击和 CSRF 攻击。

Secret 如何设计?

jwt 唯一存储在服务端的只有一个 secret,个人认为这个 secret 应该设计成和用户相关的属性,而不是一个所有用户公用的统一值。这样可以有效的避免一些注销和修改密码时遇到的窘境。

注销和修改密码?

传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验有效性。没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。分析下痛点:注销变得复杂的原因在于 jwt 的无状态。提供几个方案,视具体的业务来决定能不能接受:

  • 仅仅清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用户需要重新登录。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应的 jwt 依旧可以访问系统。
  • 清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 本身不变,但是由于 secret 不存在或改变,则无法完成校验。这也是为什么将 secret 设计成和用户相关的原因。
  • 借助第三方存储自己管理 jwt 的状态,可以以 jwt 为 key,实现去 Redis 一类的缓存中间件中去校验存在性。方案设计并不难,但是引入 Redis 之后,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt 的初衷。实际上这个方案和 session 都差不多了。
  • 修改密码则略微有些不同,假设号被盗了,修改密码(是用户密码,不是 jwt 的 secret)之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,所以仅仅清空 cookie 自然是不够的,这时,需要强制性的修改 secret。

如何解决续签问题

传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点身不由己了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签。

解决方案

  1. 每次请求刷新 jwt

    jwt 修改 payload 中的 exp 后整个 jwt 串就会发生改变,那就让它变好了,每次请求都返回一个新的 jwt 给客户端。只是这种方案太暴力了,会带来的性能问题。

  2. 只要快要过期的时候刷新 jwt

    此方案是基于上个方案的改造版,只在前一个 jwt 的最后几分钟返回给客户端一个新的jwt。这样做,触发刷新 jwt 基本就要看运气了,如果用户恰巧在最后几分钟访问了服务器,触发了刷新,万事大吉。如果用户连续操作了 27 分钟,只有最后的 3 分钟没有操作,导致未刷新 jwt,无疑会令用户抓狂。

  3. 完善 refreshToken

    借鉴 oauth2 的设计,返回给客户端一个 refreshToken,允许客户端主动刷新 jwt。一般而言,jwt 的过期时间可以设置为数小时,而 refreshToken 的过期时间设置为数天。

  4. 使用 Redis 记录独立的过期时间

    在 Redis 中单独为每个 jwt 设置了过期时间,每次访问时刷新 jwt 的过期时间,若jwt 不存在于 Redis 中则认为过期。

token 续签;2. token 的安全性; 3. 如何统一处理 token

  • 在解决 token 续签的问题上,我们这里采取的是在请求头获取 token 时判断 token 是否存在,
  • 如果不存在就创建 token并保存到Redis中,如果存在我们就重新从Redis中获取 token并 进 行 续 签 (Duration.ofHours(1)) 。
  • 在 安 全 问 题 上 , 我 们 这 里 采 用 加 密 加 盐(SignatureAlgorithm.HS256,secret)的方式来解决。
  • 为了实现 token 统一校验,我们这里采用的是 springmvc 拦截机制+Threadlocal 局部线程的方式来解决。
  • 这里我们自定义了一个 TokenInterceptor 拦截器,实现 preHandle() 方法,
  • 这样的话就可以在用户请求进入controller 层之前进行拦截,通过 token 获取了用户对象,并将用户对象存储到 Threadlocal中,
  • 这样就实现了 token 统一校验的功能。

随着 Restful API、微服务的兴起,基于 Token 的认证现在已经越来越普遍。基于token 的用户认证是一种服务端无状态的认证方式,所谓服务端无状态指的 token 本身包含登录用户所有的相关数据,而客户端在认证后的每次请求都会携带 token,因此服务器端无需存放 token 数据。

当用户认证后,服务端生成一个 token 发给客户端,客户端可以放到 cookie 或localStorage 等存储中,每次请求时带上 token,服务端收到 token 通过验证后即可确认用户身份。

CATALOG
  1. 1. JWT
  2. 2. 组成
  3. 3. 使用场景
  4. 4. JWT token 泄露了怎么办?(常问)
  5. 5. Secret 如何设计?
  6. 6. 注销和修改密码?
  7. 7. 如何解决续签问题
  8. 8. token 续签;2. token 的安全性; 3. 如何统一处理 token