JWT


JWT

1. JTW是什么?

JWT全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权

官网:https://jwt.io

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
    • 选用的何种算法
    • 选用的何种类型,一般为JWT
  • Payload:载荷,就是有效数据,一般包含下面信息:

    • 用户身份信息(注意,这里因为采用base64编码,可解码 是可逆的,因此不要存放敏感信息);
    • 注册声明:如token的签发时间,过期时间,签发人等这部分内容 好比身份证的信息这部分也会采用base64编码,得到第二部分数据
  • Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的密钥(secret)(不要泄漏,最好周期性更换,一般写在yml中),通过加密算法(不可逆的)生成一个签名。用于验证整个数据完整和可靠性。

生成的JWT token由三部分组成:Header.Payload.Signature。每个部分中间使用.分割开。

2. JWT 实现无状态 Web 服务

2.1. 什么是有状态

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。

缺点是什么?

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,无法进行水平扩展
  • 客户端请求依赖服务端,多次请求必须访问同一台服务器

2.2. 什么是无状态

服务器不需要记录客户端的状态信息,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

带来的好处是什么呢?

  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩
  • 减小服务端存储压力

2.3. 如何实现无状态

无状态登录的流程:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务的对token进行解密,判断是否有效。

流程图:

客户端请求登录,登录之后颁发凭证

整个登录过程中,最关键的点是什么?token的安全性

token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。

采用何种方式加密才是安全可靠的呢?

我们将采用:JWT + RSA非对称加密

2.4. JWT交互流程

步骤翻译:

1、用户登录

2、服务的认证,通过后根据secret生成token

3、将生成的token返回给浏览器

4、用户每次请求携带token

5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息

6、处理请求,返回响应结果

因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。

2.5. 非对称加密

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  1. 对称加密,如AES

    • 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优势:算法公开、计算量小、加密速度快、加密效率高
    • 缺陷:双方都使用同样密钥,安全性得不到保证
  2. 非对称加密,如RSA

    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
    • 私钥加密,持有私钥或公钥才可以解密
    • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
  3. 不可逆加密,如MD5,SHA

    基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

RSA算法历史:

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA

目前流行的还有oauth2

3. nimbus-jose-jwt 库

nimbus-jose-jwtjose4jjava-jwtjjwt 是几个 Java 中常见的操作 JWT 的库。就使用细节而言,nimbus-jos-jwt(和jose4j)要好于 java-jwt jjwt

nimbus-jose-jwt 官网:https://connect2id.com/products/nimbus-jose-jwt

所需坐标:

<!--jwt-->
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.31</version>
</dependency>

3.1. JWT 和 JWS

这里我们需要了解下 JWTJWSJWE 三者之间的关系:

  • JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息。
  • JWS(JSON Web Signature)和 JWE(JSON Web Encryption)是 JWT 规范的两种不同实现,我们平时最常使用的实现就是 JWS

简单来说,JWT 和 JWS、JWE 类似于接口与实现类。由于,我们使用的是 JWS ,所以,后续内容中,就直接列举 JWS 相关类,不再细分 JWS 和 JWE 了,numbus-jose-jwt 中的 JWE 相关类和接口我们也不会使用到

3.2. 加密算法

  • 对称加密指的是使用相同的秘钥来进行加密和解密,如果你的秘钥不想暴露给解密方,考虑使用非对称加密。在加密方和解密方是同一个人(或利益关系紧密)的情况下可以使用它。
  • 非对称加密指的是使用公钥和私钥来进行加密解密操作。对于加密操作,公钥负责加密,私钥负责解密,对于签名操作,私钥负责签名,公钥负责验证。非对称加密在 JWT 中的使用显然属于签名操作。在加密方和解密方是不同人(或不同利益方)的情况下可以使用它。

nimbus-jose-jwt 支持的算法都在它的 JWSAlgorithmJWEAlgorithm 类中有定义。

例如:JWSAlgorithm algorithm = JWSAlgorithm.HS256

3.3. 核心 API

3.3.1. 加密过程

  • 在 nimbus-jose-jwt 中,使用 Header 类代表 JWT 的头部,不过,Header 类是一个抽象类,我们使用的是它的子类 JWSHeader 。创建头部对象:

    JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) //指定加密算法
                    .type(JOSEObjectType.JWT) //指定类型为JWT
                    .build();
    
  • 你可以通过 .toBase64URL() 方法求得头部信息的 Base64 形式(这也是 JWT 中的实际头部信息):

    log.info("当前头部的Base64编码为:{}",jwsHeader.toBase64URL());
    
  • 使用 Payload 类的代表 JWT 的荷载部分,创建荷载部对象:

    Map<String,Object> map=new HashMap<>();
    map.put("username", "念心卓");
    Payload payload= new Payload(map);
    
  • 你可以通过.toBase64URL()方法求得荷载部信息的 Base64 形式(这也是 JWT 中的实际荷载部信息):

    log.info("当前载荷的Base64编码为:{}",payload.toBase64URL());
    
  • 签名部分

签名部分没有专门的类表示,只有通用类 Base64URL ,而且签名部分并非你自己创建出来的,而是靠 头部 + 荷载部 + 加密算法 算出来的。在 nimbus-jose-jwt 中,签名算法由 JWSAlgorithm 表示。

注意:在创建 JWSHeader 对象时就需要指定签名算法,因为在标准中,头部需要保存签名算法名字。

用头部和荷载部分,再加上指定的签名算法和密钥来生成签名部分的过程,在 nimbus-jose-jwt 中被称为『签名(sign)』。nimbus-jose-jwt 专门提供了一个签名器 JWSSigner ,用来参与到签名过程中。密钥就是在创建签名器的时候指定的:

JWSSigner signer = new MACSigner(密钥);

最终,整个 JWT 由一个 JWSObject 对象表示:

//3.2 将整个JWS由一个JWTObject表示
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
//3.3 进行签名,使用前两部分(header、payload)生成第三部分
jwsObject.sign(signer);

在 nimbus-jose-jwt 中 JWSObject 是有状态的:未签名、已签名和签名中。很显然,在执行完 .sign() 方法之后,JWSObject 对象就变成了已签名状态。

当然,我们最终『要』的是 JWT 字符串,而不是对象,这里接着对代表 JWT 的 JWSObject 对象调用.serialize()方法即可:

log.info("生成的JWT token为:{}",jwsObject.serialize());

完整示例:

/**
 * 生成jwt token
 * @return token
 * @throws JOSEException
 */
public String createToken() throws JOSEException {
    log.info("开始生成Token........");
    //1. 创建头部对象
    JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) //指定加密算法
            .type(JOSEObjectType.JWT) //指定类型为JWT
            .build();
    log.info("当前JWSHeader为:{}",jwsHeader);
    log.info("当前头部的Base64编码为:{}",jwsHeader.toBase64URL());

    //2. 创建载荷对象
    //创建载荷
    Map<String,Object> map=new HashMap<>();
    map.put("username", "念心卓");
    Payload payload= new Payload(map);
    log.info("当前Payload为:{}",payload);
    log.info("当前载荷的Base64编码为:{}",payload.toBase64URL());

    //3. 创建签名部分
    //3.1 创建签名器
    JWSSigner signer = new MACSigner(security);
    //3.2 将整个JWS由一个JWTObject表示
    JWSObject jwsObject = new JWSObject(jwsHeader, payload);
    //3.3 进行签名,使用前两部分(header、payload)生成第三部分
    jwsObject.sign(signer);

    //4. 生产token字符串
    log.info("生成的JWT token为:{}",jwsObject.serialize());
    return jwsObject.serialize();
}

如果出现:com.nimbusds.jose.KeyLengthException: The secret length must be at least 256 bits异常,是因为密钥的长度不够,增加密钥长度即可。

3.3.2. 获得token中的内容

/**
 * 取出token中的内容
 */
@Test
public void getToken() throws IOException {
    //获得token头部内容
    String jwsHeader="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
    //反编码
    BASE64Decoder decoder=new BASE64Decoder();
    byte[] txtByte= jwsHeader.getBytes(StandardCharsets.UTF_8);
    System.out.println(new String(decoder.decodeBuffer(jwsHeader),"UTF-8"));

    //获得token载荷中的内容
    String payload="aGVsbG8gd29ybGQ";
    System.out.println(new String(decoder.decodeBuffer(payload),"UTF-8"));
}

3.3.3. 解密

反向的解密和验证过程核心 API 就 2 个:JWSObject 的静态方法 parse 方法和验证其 JWSVerifier 对象。

JWSObject.parse() 方法是上面的 serialize 方法的反向操作,它可以通过一个 JWT 串生成 JWSObject 。有了 JWObject 之后,你就可以获得 header 和 payload 部分了。

如果你想直接验证 JWSObject 对象的合法性,你需要创建一个 JWSVerifier 对象。

//创建验证器
JWSVerifier jwsVerifier = new MACVerifier("密钥");//密钥要和加密时的相同

然后直接调用 jwsObject 对象的 verify 方法:

if (!jwsObject.verify(jwsVerifier)) {
    throw new RuntimeException("token 签名不合法!");
}

完整示例:

/**
 * 验证jwt token是否合法
 * @param token token字符串
 * @return 是否合法
 * @throws ParseException
 * @throws JOSEException
 */
public boolean verify(String token) throws ParseException, JOSEException {
    log.info("开始验证Token........");
    //1. 将token字符串转化为JWSObject对象(serialize方法的反向操作),这样就可以拿到header和payload了
    JWSObject jwsObject = JWSObject.parse(token);
    //2. 创建JWSVerifier一个对象(验证器),验证JWSObject的合法性
    JWSVerifier jwsVerifier =  new MACVerifier(security);
    //3. 验证JWSObject的合法性
    log.info("传入的token验证是否合法:{}",jwsObject.verify(jwsVerifier));
    return jwsObject.verify(jwsVerifier);
}

4. 完整的JWT工具类

其他依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

其他配置:

jwt:
  security: 1234567890qwertyuiopasdfghjklzxcvbnm1234567890mnbvcxzlkjhgfdsapoiuytrewq #要保证有256bits

完整的jwt工具类:

package com.nxz.jwtdemo.utils;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
@ConfigurationProperties(prefix = "jwt")
@Setter
public class JwtUtil {

    private String security;

    /**
     * 生成jwt token
     * @return token
     * @throws JOSEException
     */
    public String createToken() throws JOSEException {
        log.info("开始生成Token........");
        //1. 创建头部对象
        JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) //指定加密算法
                .type(JOSEObjectType.JWT) //指定类型为JWT
                .build();
        log.info("当前JWSHeader为:{}",jwsHeader);
        log.info("当前头部的Base64编码为:{}",jwsHeader.toBase64URL());

        //2. 创建载荷对象
        //创建载荷
        Map<String,Object> map=new HashMap<>();
        map.put("username", "念心卓");
        Payload payload= new Payload(map);
        log.info("当前Payload为:{}",payload);
        log.info("当前载荷的Base64编码为:{}",payload.toBase64URL());

        //3. 创建签名部分
        //3.1 创建签名器
        JWSSigner signer = new MACSigner(security);
        //3.2 将整个JWS由一个JWTObject表示
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        //3.3 进行签名,使用前两部分(header、payload)生成第三部分
        jwsObject.sign(signer);

        //4. 生产token字符串
        log.info("生成的JWT token为:{}",jwsObject.serialize());
        return jwsObject.serialize();
    }

    /**
     * 验证jwt token是否合法
     * @param token token字符串
     * @return 是否合法
     * @throws ParseException
     * @throws JOSEException
     */
    public boolean verify(String token) throws ParseException, JOSEException {
        log.info("开始验证Token........");
        //1. 将token字符串转化为JWSObject对象(serialize方法的反向操作),这样就可以拿到header和payload了
        JWSObject jwsObject = JWSObject.parse(token);
        //2. 创建JWSVerifier一个对象(验证器),验证JWSObject的合法性
        JWSVerifier jwsVerifier =  new MACVerifier(security);
        //3. 验证JWSObject的合法性
        log.info("传入的token验证是否合法:{}",jwsObject.verify(jwsVerifier));
        return jwsObject.verify(jwsVerifier);
    }

    /**
     * 从token中解析载荷获取内容
     * @param token jwt token
     * @return token中载荷的内容
     * @throws ParseException
     */
    public String getUser(String token) throws ParseException {
        log.info("开始获取载荷中的内容........");
        //1. 获取到JWSObject对象
        JWSObject jwsObject = JWSObject.parse(token);
        //通过JWSObject对象来获取载荷中的对象
        Map<String, Object> json = jwsObject.getPayload().toJSONObject();
        return (String) json.get("username");
    }

}

控制器测试方法:

package com.nxz.jwtdemo.controller;

import com.nimbusds.jose.JOSEException;
import com.nxz.jwtdemo.utils.JwtUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.text.ParseException;

@RestController
@RequestMapping("/test")
public class TestController {

    @Resource
    private JwtUtil jwtUtil;

    @GetMapping("/test1")
    public String  test1(){
        return "test1";
    }

    @GetMapping("/createToken")
    public String createToken(){
        try {
            return jwtUtil.createToken();
        } catch (JOSEException e) {
            e.printStackTrace();
        }
        return null;
    }

    @GetMapping("/verifyToken/{token}")
    public Boolean verifyToken(@PathVariable String token){
        try {
            return jwtUtil.verify(token);
        } catch (ParseException | JOSEException e) {
            e.printStackTrace();
        }
        return Boolean.FALSE;
    }

    @GetMapping("/getUser/{token}")
    public String getUser(@PathVariable String token){
        try {
            return jwtUtil.getUser(token);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}

文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录