优化单点登录流程的好东西:JWT 介绍

作者: judasn 分类: Java Web 开发,SSO 发布时间: 2018-03-15 18:53

JWT 概念

JSON Web Token (JWT)是一种开放标准(RFC 7519)(译者注:该RFC标准比较通俗易懂,建议进一步阅读),其中定义了一种紧凑 (compact) 且自包含(self-contained)(译者注:指的是在payload里包含更多的信息)的方式用于以JSON对象的形式在多方之间传递信息。信息可以被核实和信任,因为它经过了数字签名。JWT既可以使用密钥(采用HMAC(Hash-based Message Authentication Code)算法),也可以使用公私钥(采用RSA算法)进行签名。

  • 说人话就是:JWT是一个定义一种体积小、自包含的并且提供防篡改机制的传递数据的方式的标准协议。
  • JWT 特点:
    • 体积小意味着传输更快。
    • 传输方式多样,可以通过 GET / POST 参数化、HTTP头部参数化等方式传输给后端
    • 自包含,字符串里面是有有用的信息
    • 可扩展,可以扩展里面的信息
    • 密签,是有签名的,别人无法篡改,但是因为是通用标准生成的,别人是可以看到的,所以不能放敏感信息。
    • 支持跨域验证,可以应用于单点登录。
  • JWT 好处:
    • 前提:网站最好使用 https,防止中间人攻击,不然被嗅探到就可以直接看到内容了,虽然最多只能冒充下身份,但是还是安全点好
    • 后端服务不需要在管 session,压力小了
    • 后端服务方便扩展,很适用于分布式系统

JWT 效果演示

  • JWT 简单理解可以就是一个 Token 值,只是这个 Token 值比较特殊,有两个 . 号分隔,比如下面这个:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRw
Oi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiw
KICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIi
wKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ
1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP9
9Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccM
g4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKP
XfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvR
YLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0
nx7RkKU8NXNHq-rvKMzqg
  • 点号第一部分称为 header,第二部分称为 playload,第三部分称为 signature。
  • 其中 header 和 playload 是两个 base64 编码的 json 字符串,前端直接用 base64 解码即可,这里就是前端的要点之一。
  • signature 是签名信息,JWT 是否有效取决于对签名信息的验证是否通过,当然这是后端的事情了,这是后端的要点之一。
  • 当我们拿到这个 Token 值,我们一般有两种存放方式,一种是 Cookie,则后端后面只要拿 Cookie 这个值即可。另外一种是放在 localStorage,然后前端有请求的时候在拿出来放在请求头,比如这样:
  • 在请求头里加入 Authorization,并加上 Bearer 头(至于为什么要这样格式,可以看 这个):
fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token值
  }
})
  • 后端拿到 Token 校验是否有效、是否过期等情况。

主要组件

  • header + algorithm(头部 + 算法)
    • 描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等,比如:
    • typ 是格式,alg 是加密方式。更多加密方式可以看:这里
{
  'typ': 'JWT',
  'alg': 'HS256'
}
  • payload(载荷)
    • 包含 JWT 的一些标准数据,外加我们自己定义的一些数据
    • JWT 标准数据中有(不是每个都需要),这种也叫做:Claims,更多 Claim
      • iss(Issuer): 该JWT的签发者
      • sub(Subject): 主题
      • aud(Audience): 接收该JWT的一方
      • exp(Expiration time): 什么时候过期,这里是一个 Unix 时间戳
      • iat(issued at): 签发的时间
      • jti(JWT ID): JWT的唯一标识
  • signature + algorithm(签名 + 算法)
    • 签名的过程一般是我们用 HS256 算法对 Header 和 Payload 在 base64 之后的字符串进行加密,这个密钥 secret 存储在服务端,前端不可见,所以这件事是纯后端的事情。
    • signature 是用下面的逻辑代码计算出来的:
data = base64Encode(header内容) + "." + base64Encode(payload内容);
signature = Hash(data,secret);
signature = base64Encode(signature);

JWT 在线工具

Java 工具包

  • jjwt(优先):https://github.com/jwtk/jjwt
    • 依赖:Jackson 2.8.x (or later)
  • java-jwt:https://github.com/auth0/java-jwt
  • 需要注意的:
    • (优先)可以放在 localStorage,前端请求的时候把 token 放在 Request Header 中发给后台。
    • 也可以吧生成的 token 如果存入 cookie,则必须使用 HttpOnly 属性防止Cookie被JavaScript读取,从而避免跨站脚本攻击(XSS攻击)。
  • 后端一般要做的校验:
    • 签名是否正确
    • token 是否过期(根据 exp 字段)
    • 接收方(aud)是否是我

JWT 应用细节注意(感谢茶饮兄交流)

JWT 有效时长控制

  • 我们需要设置一个阀值,比如业务上我们规定有效期是 24 小时,阀值是 12 小时。
  • 当请求过来,判断 token 里面的创建时间是否已经超过 24 小时,超过直接 401。
  • 未超过 24 但是超过 12 小时,则放行请求,并下发新 token(放在响应头里面)。
  • 如果未超过 12 小时则放行请求。

JWT 吊销 token

  • 引入一个全局 Map 作为黑名单 token 使用。Map 的 key 是 token 值,value 是对象:包含用户信息,token 创建时间。(引入全局 Map 主要是避免引入 redis,减少一个网络请求,但是数据量大的情况下还是得考虑中间件)
  • 当用户修改密码、重置密码,则把该用户当前 token 存入 map。
  • 每次请求过来,先解析 JWT,如果解码通过,再去全局 Map 判断是否存在该 token,如果不存在,放行请求。如果存在就表示该用户最近修改过密码或是重置密码,返回 401 给前端。
  • 同时有一个定时任务,固定时间遍历 map 中的信息,如果创建时间超过 24 小时,清除该 key。
  • 但是以上的前提是,所有服务都是经过网关的,全局 map 在网关中。如果你的系统有服务是在网关之外的,则必须引入 redis 等中间件,其他服务系统才能判断 token 是否在黑名单中。

动态密钥替换

  • 为了安全,所以需要不定期更换密钥。
  • 系统配置里面有:
    • 秘钥A值
    • 密钥B值
    • 一个表示是否进入更换密钥开关
    • 开启更换密钥时间。
  • 在没有启动更换密钥的时候,所以请求都是根据密钥A来进行解码,流程如上 JWT 有效时长控制
  • 如果此时进入更换密钥状态:那当前是密钥A是正在使用的,密钥B是即将使用的。
  • 因为我们 JWT 从业务角度有效期是 24 小时,阀值是 12 小时。所以请求过来的时候,如果有效期超过 24 小时,返回 401,如果超过 12 小时,用密钥B 生成新 token 返回前端。
  • 如果小于 12 小时,则先用密钥 A 进行解码,如果解出来,则放行请求,并用密钥B生成新 Token 给前端。如果解不出来,则用密钥B 继续解码,如果解不出来,则 401,解出来,则直接放行请求什么都不做。
  • 同时启动一个定时任务:开启更换密钥时间如果超过当前系统时间 24 小时,则更换密钥A的值为密钥B,同时更改更换密钥开关为 false。

Spring boot Demo

  • 还在整理中

资料

希望对你有帮助!