User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Java   ›   Java Notes   ›   正文
Java Notes

Java—JJWT使用

2020-09-10 15:22:56
1659  0 0
参考目录 隐藏
1) JWT token的生成
2) 生成token
3) Header
4) 再看载荷 Payload
5) builder所需的东西都俱备了,那就可以build了!
6) JWT token的解析
7) 解析Token
8) 再看方法
9) 接下来是重头戏方法:解析

阅读完需:约 12 分钟

其中JAVA语言到(2018-06)有6个实现库,如下图:

按顺序依次是

Auth0实现 的 java-jwt

Brian Campbell实现的 jose4j

connect2id实现的 nimbus-jose-jwt

Les Haziewood实现的 jjwt

Inversoft实现的prime-jwt

Vertx实现的vertx-auth-jwt.


JWT token的生成

一个JWT由三部分组成:
Header(头部) —— base64编码的Json字符串
Payload(载荷) —— base64编码的Json字符串
Signature(签名)—— 使用指定算法,通过Header和Payload加盐计算的字符串

各部分以” . “分割,如:

eyJhbGciOiJIUzUxMiJ9.eyJjcnQiOjE1MjgzNDM4OTgyNjgsImV4cCI6MTUyODM0MzkxOCwidXNlcm5hbWUiOiJ0b20ifQ.E-0jxKxLICWgcFEwNwQ4pfhdMzchcHmsd8G_BTsWgkUmVwPzDd7jJlf94cAdtbwTLMm27ouYYzTTxMXq7W1jvQ

直接通过base64解码可获得

Header:

{"alg":"HS512"}

Payload:

{"crt":1528343898268,"exp":1528343918,"username":"tom"}

依赖:

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

生成token

public static String generateToken(Stringusername){

    Map claims= new HashMap<>();
    claims.put(CLAIM_KEY_USERNAME,username);
    claims.put(CLAIM_KEY_CREATE_TIME,new Date(System.currentTimeMillis()));

    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate())
            .signWith(SignatureAlgorithm.HS512, SIGNING_KEY)
            //.compressWith(CompressionCodecs.DEFLATE)
            .compact();

}

Jwts.builder() 返回了一个 DefaultJwtBuilder()

DefaultJwtBuilder属性

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private Header header; //头部
    private Claims claims; //声明
    private String payload; //载荷
    private SignatureAlgorithm algorithm; //签名算法
    private Key key; //签名key
    private byte[] keyBytes; //签名key的字节数组
    private CompressionCodec compressionCodec; //压缩算法

DefaultJwtBuilder包含了一些Header和Payload的一些常用设置方法


Header

  • setHeader() 有两种参数形式,一种是Header接口的实现,一种是Map。其中Header接口也继承自Map。如果以第二种形式(即Map)作为参数,在setHeader的时候会生成默认的Header接口实现DefaultHeader对象。两种参数形式调用setHeader(),都会令Header重新赋值。即:
        this.header = header;
        或者
        this.header = new DefaultHeader(header);
  • setHeaderParam() 和 setHeaderParams() 向Header追加参数
    两个方法都使用ensureHeader() 方法(返回当前header 如果不存在则创建DefaultHeader)

如果即不设置签名,也不进行压缩,header是不是就应该没有了呢?
不是。即时不进行签名,alg也应该存在,不然对其进行解析会出错。
在生成jwt的时候,如果不设置签名,那么header中的alg应该为none。jjwt中compact()方法实现如下:

        if (key != null) {
            jwsHeader.setAlgorithm(algorithm.getValue());
        } else {
            //no signature - plaintext JWT:
            jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
        }
{"alg":"none"}

再看载荷 Payload

值得注意的是,载荷部分存在两个属性:payload和claims。两个属性均可作为载荷,jjwt中二者只能设置其一,如果同时设置,在终端方法compact() 中将抛出异常

        //payload和claims均不设置
        if (payload == null && Collections.isEmpty(claims)) {
            throw new IllegalStateException("Either 'payload' or 'claims' must be specified.");
        }
        //payload和claims同时设置
        if (payload != null && !Collections.isEmpty(claims)) {
            throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one.");
        }
        //签名key(盐)不设置
        if (key != null && keyBytes != null) {
            throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either one.");
        }
  • setPayload() 设置payload,直接赋值
  • setClaims() 设置claims,以参数创建一个新Claims对象,直接赋值
  • claim() 如果builder中Claims属性为空,则创建DefaultClaims对象,并把键值放入;如果Claims属性不为空,获取之后判断键值,存在则更新,不存在则直接放入。

还提供了 JWT标准 7个保留声明(Reserved claims)的设置方法,7个声明都是可选的,也就是说可以不用设置。

  • setIssuer()
  • setSubject()
  • setAudience()
  • setExpiration()
  • setNotBefore()
  • setIssuedAt()
  • setId()
iss: 签发者
sub: 面向用户
aud: 接收者
iat(issued at): 签发时间
exp(expires): 过期时间
nbf(not before):不能被接收处理时间,在此之前不能被接收处理
jti:JWT ID为web token提供唯一标识

Payload 举例:

{"sub":"subject","aud":"sina.com","iss":"baidu.com","iat":1528360628,"nbf":1528360631,"jti":"253e6s5e","exp":1528360637}

当然也可以在Payload中添加一些自定义的属性claims键值对

  • compressWith() 压缩方法。当载荷过长时可对其进行压缩。可采用jjwt实现的两种压缩方法CompressionCodecs.GZIP和CompressionCodecs.DEFLATE
  • signWith() 签名方法。两个参数分别是签名算法和自定义的签名Key(盐)。签名key可以byte[] 、String及Key的形式传入。前两种形式均存入builder的keyBytes属性,后一种形式存入builder的key属性。如果是第二种(及String类型)的key,则将其进行base64解码获得byte[] 。

builder所需的东西都俱备了,那就可以build了!

  • compact() 生成JWT。过程如下:
  1. 载荷校验,前文已经提及。
  2. 获取key。如果是keyBytes则通过keyBytes及算法名生成key对象。
  3. 将所使用签名算法写入header。如果使用压缩,将压缩算法写入header。
  4. 将Json形式的header转为bytes,再Base64编码
  5. 将Json形式的claims转为bytes,如果需要压缩则压缩,再进行Base64编码
  6. 拼接header和claims。如果签名key为空,则不进行签名(末尾补分隔符” . “);如果签名key不为空,以拼接的字符串作为参数,按照指定签名算法进行签名计算签名部分 sign(String jwtWithoutSignature),签名部分同样也会进行Base64编码。
  7. 返回完整JWT

jjwt实现的 DefaultJwtSigner 提供了一个带工厂参数的构造方法。并将jjwt实现的 DefaultSignerFactory
静态实例传入,根据不同的签名算法创建对应的签名器进行签名。


JWT token的解析

来看JWT解析的实现类 DefaultJwtParser()

解析Token

典型代码示例:

    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

上述代码从JWT Token中获取了载荷。

与Builder类似,Jwts.parser() 返回了DefaultJwtParser 对象

DefaultJwtParser() 属性

    //don't need millis since JWT date fields are only second granularity:
    private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    private static final int MILLISECONDS_PER_SECOND = 1000;

    private ObjectMapper objectMapper = new ObjectMapper();

    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() 与builder中签名方法signWith()对应,parser中的此方法拥有与signWith()方法相同的三种参数形式,用于设置JWT的签名key,用户后面对JWT进行解析。
  • isSigned() 校验JWT是否进行签名。方法很简单,以分隔符” . “,截取JWT第三段,即签名部分进行判断。

同样,与builder中设置一些标准Claims的set方法对应,parser中包含一系列require方法进行claims校验。

  • requireIssuedAt()
  • requireIssuer()
  • requireAudience()
  • requireSubject()
  • requireId()
  • requireExpiration()
  • requireNotBefore()
    并且还提供了自定义claims字段的校验方法
  • require()

当claims不匹配时,会抛出 IncorrectClaimException 异常。

  • setClock() 设置parser内Clock实例
  • setAllowedClockSkewSeconds() 设置允许的时间偏移(秒)。如果传入负值,将设置为0,即相当于未设置。此方法与builder中的setNotBefore() 相关。当builder中设置了setNotBefore()。那么该Token必须要在这个时间之后才可用。而parser中设置允许的时间偏移,就相当于把这个时间点变成了一个时间范围。
    举个例子,比如生成JWT Token的时候设置了setNotBefore为当前时间的5秒后,那么马上对这个Token进行解析会抛出PrematureJwtException异常,提示该JWT的接收时间还没到。那么如果我在解析的时候设置了允许的时间偏移为5秒或者更长,那么判断这个JWT时就可以接收解析了。另外这个偏移也会影响JWT的过期时间。比如JWT本来已经过期5秒,如果我设置偏移为5秒或者更长,那么仍作为有效的JWT凭据。
  • setSigningKeyResolver() 设置签名key获取器。需实现两个获取签名key的方法。
  • setCompressionCodecResolver() 设置压缩解析器。需实现一个获取解码解码器的方法。

上述两个解析器具体的使用应该会在解析的方法中有所体现。

接下来是重头戏方法:解析

  • parse() 方法传入一个JWT字符串,返回一个JWT对象。

解析过程:

  1. 切分。
    以分隔符” . “切分JWT的三个部分。如果分隔符数量错误或者载荷为空,将抛出 MalformedJwtException 异常。
  2. 头部解析。
    将头部原始Json键值存入map。根据是否加密创建不同的头部对象。jjwt的DefaultCompressionCodecResolver根据头部信息的压缩算法信息,添加不同的压缩解码器。
  3. 载荷解析。
    先对载荷进行Base64解码,如果有经过压缩,那么在解码后再进行解压缩。此时将值赋予payload。如果载荷是json形式,将json键值读入map,将值赋予claims 。
        if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
            Map<String, Object> claimsMap = readValue(payload);
            claims = new DefaultClaims(claimsMap);
        }
  • 签名解析。
    如果存在签名部分,则对签名进行解析。

(1)首先根据头部的签名算法信息,获取对应的算法。

如果签名部分不为空,但是签名算法为null或者’none’,将抛出MalformedJwtException异常。

(2)获取签名key

重复异常

  • 如果同时设置了key属性和keyBytes属性,parser不知道该使用哪个值去作为签名key解析,将抛出异常。
  • 如果key属性和keyBytes属性只存在一个,但是设置了signingKeyResolver,也不知道该去解析前者还是使用后者,将抛出异常。

如果设置了key(setSigningKey() 方法)则直接使用生成Key对象。如果两种形式( key和keyBytes )都没有设置,则使用SigningKeyResolver(通过setSigningKeyResolver()方法设置)获取key。

当然,获取key为null会抛出异常。

(3)创建签名校验器

JJWT实现了一个默认的签名校验器DefaultJwtSignatureValidator。该类提供了两个构造方法,外部调用的构造方法传入算法和签名key,再加上一个DefaultSignatureValidatorFactory工厂实例传递调用另一个构造函数,以便工厂根据不同算法创建不同类型的Validator。

    public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) {
        this(DefaultSignatureValidatorFactory.INSTANCE, alg, key);
    }

    public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) {
        Assert.notNull(factory, "SignerFactory argument cannot be null.");
        this.signatureValidator = factory.createSignatureValidator(alg, key);
    }

这里和builder类似。

(4)比对验证
根据头部和载荷重新计算签名并比对。
如果不匹配,抛出SignatureException异常。

(5)时间校验
根据当前时间和时间偏移判断是否过期。
根据当前时间和时间偏移判断是够未到可接收时间。

(6)Claims参数校验
即校验parser前面设置的所以require部分。校验完成后,以header,claims或者payload创建DefaultJwt对象返回。

至此,已经完成JWT Token的校验过程。校验通过后返回JWT对象。


  • parse(String compact, JwtHandler<T> handler)方法根据 parse(String jwt) 解析返回的JWT对象类型以及Body是payload还是claims,采用不同的适配器处理。
    解析时也可直接指定适配器。
  • parsePlaintextJwt 载荷为文本(不是Json),未签名
  • parseClaimsJwt 载荷为claims(即Json),未签名
  • parsePlaintextJws 载荷为文本(不是Json),已签名
  • parseClaimsJws 载荷为claims(即Json),已签名

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593863浏览

随机文章
Nginx—安装运行Lua脚本
3年前
Vue关于路由拦截
5年前
SpringCloud—OpenFeign(一)声明式服务调用
5年前
Java—并发编程(八)线程池– (2) 线程池的原理
3年前
axios自定义请求配置—transformRequest
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593863 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付