前言

官网介绍 翻译如下:

JWT 是什么?

    JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑和独立的方式,可以作为 JSON 对象在各方之间安全地传输信息。这个信息可以被验证和信任,因为它是用数字签名完成的。jwt 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

    虽然 JWT 也可以加密以提供各方之间的保密,但专注于签名 Token 。签名 Token 可以验证其中包含的声明的完整性,而加密Token 可以向其他各方隐藏这些声明。当使用公钥/私钥对签名时,签名还证明只有持有私钥的一方是签名的一方。

什么时候使用 JWT?

​    授权:这是使用JWT最常见的场景。一旦用户登录,每个后续请求都将包括JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用JWT的一个特性,因为它的开销很小,并且可以轻松地跨不同的领域使用。

​    信息交换: JSON Web令牌是在双方之间安全传输信息的好方法。因为jwt可以被签名,例如,使用公钥/私钥对,所以您可以确定发件人就是他们所说的那个人。此外,由于签名是使用标头和有效负载来计算的,因此您还可以验证内容是否没有被篡改。

可以看到 JWT 常用于身份鉴权方面,它也是目前最流行的跨域身份验证解决方案。

传统的单机后端服务经常会使用 Session 来保存用户登录状态信息,但这种模式的问题是,遇到非浏览器如微信小程序,那不能使用Session 来保存用户信息,还有在多机(服务器集群)或面向服务的跨域体系的话,简单的 Session 就不能处理这种情况。

JWT 就是针对上述提供的一种灵活的解决方案,它是无状态的 Token,通过客户端保存数据,而服务器根本不保存会话数据,每个请求都被发送回服务器。

JWT 的实现库有很多种,针对不同编程语言有对应的库,具体可以见官网。java 也有好几种,官网排在最前面的是开源库 java-jwt

github: https://github.com/auth0/java-jwt

pom 依赖如下:

<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>4.2.2</version>
</dependency>

一、JWT 规范

JWT 由三个部分: Header, Payload, Signature 组成,三部分用(.)分开。

1.1 Header 头部

头部通常由两部分组成:Token 的类型,即 JWT,以及正在使用的签名算法,如HMAC SHA256或RSA。

{
  "alg": "HS256",
  "typ": "JWT"
}

这部分 json 数据通常会被 Base64 编码为一串字符串为 JWT 的头部,也就是第一部分,可通过 js 函数 btoa 实现

btoa('{"alg": "HS256","typ": "JWT"}')

得到

eyJhbGciOiAiSFMyNTYiLCJ0eXAiOiAiSldUIn0=

1.2 Payload 载荷

这是 JWT 的核心部分,也是数据传输的主体,包含了要传输的数据,也有一些已经定义好的字段,可以称之为 Claims

下面是一些常见的 Claims :

简称全称含义
issIssuer发行方
subSubject主体
audAudience(接收)目标方
expExpiration Time过期时间
nbfNot Before早于该定义的时间的JWT不能被接受处理
iatIssued AtJWT发行时的时间戳
jtiJWT IDJWT的唯一标识

例子:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1613297468168
}

同样也会被base64编码为字符串作为 JWT 的第二部分

btoa('{"sub": "1234567890","name": "John Doe","admin": true,"exp": 1613297468168}')

得到

eyJzdWIiOiAiMTIzNDU2Nzg5MCIsIm5hbWUiOiAiSm9obiBEb2UiLCJhZG1pbiI6IHRydWUsImV4cCI6IDE2MTMyOTc0NjgxNjh9

1.3 Signature 签名

签名的作用就是保护 Claims 的完整性(或者数据加密),保证 JWT 传输的过程中 Claims 不被篡改(或者不被破解)。

举个例子:使用HMAC SHA256 算法对头部和载荷用以下方式进行加密,就能得到签名。header 和 payload 都是明文经 base64 编码传输,通过签名来保证没有被篡改,即使得到明文部分,也知道对应的加密算法,但没有秘钥也很难伪造签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

二、Java 实现

2.1 创建

  • 基本使用

    使用 java-jwt 提供的 API 可以轻松生成 token

    
    String userId = "10086";
    LocalDateTime now = LocalDateTime.now();
    long expireDays = 7;
    String s= ""; 
    
    Date startDate = localDateTimeToDate(now);
    Date endDate = localDateTimeToDate(now.plusDays(expireDays));
    
    Algorithm algorithm = Algorithm.HMAC256(s);
    Map<String, Object> header = new HashMap<>();
    header.put("alg", "HS256");
    header.put("typ", "JWT");
    
    String token = JWT.create()
            .withHeader(header)
            .withAudience(userId)
            .withIssuedAt(startDate)
            .withExpiresAt(endDate)
            .withClaim("test", "test values")
            .sign(algorithm);
    
    System.out.println(token);
    // eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxMDA4NiIsInRlc3QiOiJ0ZXN0IHZhbHVlcyIsImV4cCI6MTY3NDU2NzAxMSwiaWF0IjoxNjczOTYyMjExfQ.GSpwAX6WG3RMplu4TdSsYLWTjNlqGh3KC-ifhA-_x50
    
  • 更换加密算法

    java-jwt 内置了非常多的加密算法,例如:可以使用 RSA256

    KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
    rsa.initialize(512);
    KeyPair keyPair = rsa.generateKeyPair();
    PublicKey publicKey = keyPair.getPublic();
    PrivateKey privateKey = keyPair.getPrivate();
    System.out.println(Base64.getEncoder().encodeToString(publicKey.getEncoded()));
    System.out.println(Base64.getEncoder().encodeToString(privateKey.getEncoded()));
    Algorithm rsa256Algorithm = Algorithm.RSA256((RSAPublicKey) publicKey, (RSAPrivateKey) privateKey);
    

2.2 解密

DecodedJWT jwt = JWT.require(algorithm).build().verify(token);

String jwtAlgorithm = jwt.getAlgorithm();
String jwtId = jwt.getAudience().get(0);
Map<String, Claim> claims = jwt.getClaims();

System.out.printf("%s \n%s \n%s%n", jwtAlgorithm, jwtId, claims);

2.3 校验

  • 时间校验

    JWT token可以包含一些用于验证它的日期相关信息:

    • 该token是已发布的token: “iat” < NOW
    • 该token还没有过期: “exp” > NOW
    • 该token已经被使用:“nbf” < NOW

    对日期的验证是在验证token时自动执行的,如果验证不通过则会抛出 JWTVerificationException 异常。

    验证方法中 acceptLeeway 这个方法是设置 leeway 这个属性的,通过查看源码可以发现 是留有余地的秒值

    private boolean assertInstantIsFuture(Instant claimVal, long leeway, Instant now) {
        return claimVal == null || !now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal);
    }
    
    private boolean assertInstantIsPast(Instant claimVal, long leeway, Instant now) {
        return claimVal == null || !now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal);
    }
    
    JWTVerifier verifier = JWT.require(algorithm)
            .acceptLeeway(1)
            .build();
    verifier.verify(token);
    
  • claim 校验

    除了上述的时间校验,还可以校验传递的信息,即 claim,通不过就抛异常 JWTVerificationException

    JWTVerifier verifier = JWT.require(algorithm)
                    .acceptLeeway(1)
                    .withClaim("test", "123")
                    .withClaimPresence("test")
                    .build();
    verifier.verify(token);
    

参考

  1. https://jwt.io/introduction
  2. JWT全面解读、详细使用步骤
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐