JJWT 的使用

实验环境

  • Jsonwebtoken-jjwt:0.11.2

JWT 简介

JWT 全称 Json web token, 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准。token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,token 也可直接被用于认证,也可被加密。

优点

传统的 session 认证需要在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给至服务端,这样服务端就能识别请求来自于哪个用户;通常而言 session 都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大;如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上才能拿到授权的资源,这意味着限制了应用的扩展能力;cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。

而基于 token 的鉴权机制类似于 http 协议是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息,既可以减小服务端的开销,也不限制应用的扩展能力。

除此之外,JWT 的构成非常简单,字节占用很小,非常便于传输的;因为 json 的通用性,JWT 可以进行跨语言支持的;JWT 的 payload 部分,可以在自身存储一些其他业务逻辑所必要的非敏感信息。

JWT 认证鉴权流程

  1. 用户使用用户名密码来请求服务器
  2. 服务器进行认证(验证用户的信息)
  3. 服务器通过验证后发送给用户一个 token
  4. 客户端存储 token,并在每次请求的请求头中附上 token 的值
  5. 服务端验证token值,并返回数据

JWT 结构

JWT 由三段信息构成的,这三段信息由 “.” 连接。
例如:

eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiJhMDgxOGU1MjRhYmM0MDg0YmFkNzBmYzMwZmJkNmQ2NyIsImF1dGhvcml0aWVzIjoiYWRtaW4iLCJzdWIiOiJhZG1pbiJ9.JN1mTRaSZrD49K2sjZt4ZsvQ5zsdi2ZxZau5kyXDk8bYnJxNWc7uL7eoiCXUsjrT9_x0EVTqZdD1Rxm6xJNl6Q

这三段信息分别为头部(header)、载荷(payload)、签证(signature),这些信息进行base64加密后构成了 token。

  • header: 承载两部分信息:

    {
    'typ': 'JWT', // 声明类型
    'alg': 'HS256' // 声明加密的算法
    }
    
  • playload:存放有效信息的地方,包含三个部分:标准中注册的声明、公共的声明、私有的声明:

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

  • iss: jwt 签发者

  • sub: jwt 所面向的用户

  • aud: 接收 jwt 的一方

  • exp: jwt 的过期时间,这个过期时间必须要大于签发时间

  • nbf: 定义在什么时间之前,该 jwt 都是不可用的

  • iat: jwt 的签发时间

  • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击

公共的声明 : 公共的声明一般添加用户的相关信息或其他业务需要的必要信息.不建议添加敏感信息;
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,不建议存放敏感信息。

  {
    "sub": "1",
    "name": "admin",
    "admin": true
  }
  • signature:通过 header 中声明的加密方式及使用 “secret” 加盐对 header + payload 进行加密后生成

    $ݦMfxfː绝fqe%ÓƘMYή/%Բ:ӷU:t=QƮ$ٺ
    

    JWT token 的生成

JJWT 使用 Jwt 构造器 JwtBuilder 生成 token
例如:

Jwts.builder()
    .setClaims(claims)
    .setExpiration(generateExpirationDate())
    .signWith(SignatureAlgorithm.HS512, SIGNING_KEY)
    .compressWith(CompressionCodecs.DEFLATE)
    .compact();
  • Jwts.builder() 返回了一个 DefaultJwtBuilder(),DefaultJwtBuilder 包含了一些 Header 和 Payload 的一些常用设置方法;

    private Header header; // 头部
    private Claims claims; // 声明
    private String payload; // 载荷
    private SignatureAlgorithm algorithm; // 签名算法
    private Key key; // 签名key
    private byte[] keyBytes; // 签名key的字节数组
    private CompressionCodec compressionCodec; // 压缩算法
    
    • setHeader() 设置 Header 追加参数
    • setHeaderParam() 向 Header 追加参数
    • setHeaderParams() 向 Header 追加参数
    • setPayload() 设置 payload(payload 和 claims,两个属性均可作为载荷,jjwt中二者只能设置其一,如果同时设置会抛出异常)
    • setClaims() 设置 claims(payload 和 claims,两个属性均可作为载荷,jjwt中二者只能设置其一,如果同时设置会抛出异常)
    • claim() 如果 builder 中 Claims 属性为空,则创建 DefaultClaims 对象,并把键值放入;如果 Claims 属性不为空,获取之后判断键值,存在则更新,不存在则直接放入。
    • setIssuer() 设置签发者(可选)
    • setSubject() 设置面向用户(可选)
    • setAudience() 设置接收者(可选)
    • setExpiration() 设置过期时间(可选)
    • setNotBefore() 设置不能被接收处理时间,在此之前不能被接收处理(可选)
    • setIssuedAt() 设置签发时间(可选)
    • setId() 设置 token 唯一标识(可选)
    • compressWith() 压缩方法,当载荷过长时可对其进行压缩。jjwt实现的两种压缩方法 CompressionCodecs.GZIP 和 CompressionCodecs.DEFLATE
    • signWith() 签名方法。两个参数分别是签名算法和自定义的签名Key(盐)。签名key可以byte[]、String及Key的形式传入。前两种形式均存入builder的keyBytes属性,后一种形式存入builder的key属性。如果是第二种(及String类型)的key,则将其进行base64解码获得byte[]。

  • JwtBuilder.compact() 生成JWT。
    生成过程如下:

    • 荷校验
    • 获取key,如果是 keyBytes 则通过 keyBytes 及算法名生成 key 对象
    • 将所使用签名算法写入 header。如果使用压缩,将压缩算法写入 header
    • 将 Json 形式的 header 转为 bytes,再 Base64 编码
    • 将 Json 形式的 claims 转为 bytes,如果需要压缩则压缩,再进行 Base64 编码
    • 拼接 header 和 claims。如果签名 key 为空,则不进行签名(末尾补分隔符” . “);如果签名 key 不为空,以拼接的字符串作为参数,按照指定签名算法进行签名计算签名部分,签名部分同样进行 Base64 编码
    • 返回完整JWT

JWT token 的解析

JJWT 使用 Jwt 分析器 JwtParser 解析 token
例如从 token 中获取载荷:

claims = Jwts.parser()
    .setSigningKey(secret)
    .parseClaimsJws(token)
    .getBody();
  • Jwts.parser() 返回了 DefaultJwtParser 对象,DefaultJwtParser() 包含了如下属性:

    private byte[] keyBytes; // 签名 key 字节数组
    private Key key; // 签名 key
    private SigningKeyResolver signingKeyResolver; //签名Key解析器
    private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); // 压缩解析器
    Claims expectedClaims = new DefaultClaims(); // 期望 Claims
    private Clock clock = DefaultClock.INSTANCE; // 时间工具实例
    private long allowedClockSkewMillis = 0;  // 允许的时间偏移量
    
    • setSigningKey() 设置签名 key
    • isSigned() 校验 JWT 是否进行签名
    • requireIssuedAt() 进行签发时间校验
    • requireIssuer() 进行签发者校验
    • requireAudience() 进行接收者校验
    • requireSubject() 进行面向用户校验
    • requireId() 进行 token 唯一标识校验
    • requireExpiration() 进行过期时间校验
    • requireNotBefore() 进行不能被接收处理时间校验
    • require() 自定义claims字段的校验
    • setClock() 设置parser内Clock实例
    • setAllowedClockSkewSeconds() 设置允许的时间偏移(秒)。如果传入负值,将设置为0,即相当于未设置
    • setSigningKeyResolver() 设置签名key获取器
    • setCompressionCodecResolver() 设置压缩解析器

  • JwtParser.parse() 解析token;方法传入一个 JWT 字符串,返回一个 JWT 对象。

解析过程如下:
* 切分:以分隔符”.“切分 JWT 的三个部分
* 头部解析:将头部原始 Json 键值存入 map。根据是否加密创建不同的头部对象
* 载荷解析:先对载荷进行 Base64 解码,如果有经过压缩,那么在解码后再进行解压缩,将值赋予 payload。如果载荷是 json 形式,将 json 键值读入 map,将值赋予 claims
* 签名解析:如果存在签名部分,则对签名进行解析。首先根据头部的签名算法信息,获取对应的算法,然后获取签名 key,接着创建签名校验器,最后进行各种校验:比对校验(根据头部和载荷重新计算签名并比对)、时间校验(根据当前时间和时间偏移判断是否过期)、Claims 参数校验(校验 parser 设置的 require 部分)
* 校验完成后,以 header,claims 或者 payload 创建 DefaultJwt 对象返回