JWT(JSON Web Token)是一种基于 JSON 的开放标准。多应用于传递身份认证信息和授权信息的场景。

同时它还支持多种加密算法对它的信息进行加密和解密处理,典型的有 RSA 算法 和 HMAC 算法。

👀 提示:下列演示代码均来自 SpringSecurity OAuth2 实现应用授权 中 demo,如果对 OAuth2 不甚了解的小伙伴,可先对 OAuth2 进行了解,也可以前往链接地址看看我对 OAuth2 的一些理解。

🚀 源码地址:https://github.com/NekoChips/springboot

🚀 相关链接:

  1. JWT 简介:https://jwt.io/introduction/
  2. Spring Security OAuth2 JWT 应用:SpringSecurity OAuth2 实现应用授权

1. 初识 JWT

image.png

通过生成的 JWT 字符串和解析之后的内容可以看出,JWT 由三个部分组成,每个部分间以 (.) 分隔:

  1. Header:头部信息
  2. Payload:载荷信息
  3. Signature:签证信息

Header

从解析后的内容上看,header 中包含了两个信息:

  1. typ:表示加密串的声明类型,此处为 jwt。
  2. alg:表示该加密串使用的加密算法,这里是 HS256,也是默认的加密算法。

使用 base64 对解析后的头部信息进行解码:

image.png

也就是说 JWT 的头部信息是通过 base64 进行编码的。

Payload

载荷就是用来存放加密后的信息的。官网上说到 JWT 的载荷信息中包含了三个声明部分:

  1. Registered claims:注册声明
  2. Public claims:公共声明
  3. Private claims:私有声明

Registered claims

注册声明是一组非强制性但建议使用的预定义声明,目的是提供一组可用的、可协作的预定义声明。主要包含以下部分:

  1. iss: jwt 的签发者
  2. exp:jwt 的过期时间
  3. sub:jwt 所面向的用户
  4. aud:jwt 的接收方
  5. jti:一串 uuid,用于作为 jwt 的唯一标识

上面解析后的内容中只包含了 exp 和 jti 作为注册声明。

Public claims

公共声明可以添加任何信息,一般添加用户的相关信息。由于该部分信息是公共的,所以不建议存放敏感信息。

上面解析后的内容中,username、scope、authorities、client_id 皆可归类为公共声明。

Private claims

私有声明是 jwt 的提供方和接收方共同定义的声明,同样不建议存放敏感信息。

同样的,JWT 的载荷部分也是通过 base64 编码后的产物。

Signature

官网给出的解释为:

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.

大致意思就是:签证部分是用 已编码的 header已编码的 payload 使用(.)拼接成的字符串,然后按照 header 中声明的加密算法一个提供的 secret 盐值 对该字符串进行加密,最后生成的加密值即为签证部分。

上面图例中可以看出该 JWT 签证生成的部分为:

image.png

👀 很多集成了 JWT 的框架中都提供了这一部分的配置,如前面较多使用的 SpringSecurity


2. 使用 JWT

前面使用过 SpringSecurity OAuth2 集成的 JWT 功能,详情移步至 SpringSecurity OAuth2 实现应用授权

2.1 SpringSecurity 中的应用

在之前的示例项目中,JWT 使用的是对称加密的方式,然而 Spring Security OAuth2 还提供了 JWT 非对称加密的方式。我们可以对其进行配置。

这里我们使用非对称的 RSA 公钥私钥 方式对 JWT 进行加密。

首先创建一个 RSAUtils 工具类

public class RsaUtils {

    public static final String PUBLIC_KEY = "RSAPublicKey";

    public static final String PRIVATE_KEY = "RSAPrivateKey";

    private static final String ENCRYPT_ALGORITHM = "RSA";

    private static final int KEY_SIZE = 2048;

    // 用于存放随机生成的密钥对
    public static Map<String, String> keyMap = new HashMap<>(2);

    /**
     * 随机生成密钥对
     *
     * @return 密钥对
     */
    public static KeyPair getKeyPair() throws NoSuchAlgorithmException {
        // 基于 RSA 算法生成 keyPairGenerator ,用于生成密钥对
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ENCRYPT_ALGORITHM);
        // 使用随机数初始化 keyPairGenerator
        keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
        // 生成密钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        // 编码后的 密钥对 添加至map
        keyMap.put(PUBLIC_KEY, Base64Utils.encodeToString(publicKey.getEncoded()));
        keyMap.put(PRIVATE_KEY, Base64Utils.encodeToString(privateKey.getEncoded()));

        return keyPair;
    }
}

JwtTokenConfig 中配置 JWT 转换器

//......

    // jwtToken 转换器
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
        // 设置 jwtToken 签名密钥
//        tokenConverter.setSigningKey(PUBLIC_KEY);
        try {
            KeyPair keyPair = RsaUtils.getKeyPair();
            tokenConverter.setKeyPair(keyPair);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return tokenConverter;
    }
//......

JwtAccessTokenConverter.setVerifier(SignatureVerifier verifier) 方法需要传入一个 签证校验器 对象,过 SignatureVerifier 接口有如下实现类和接口:

image.png

查看源码

// ......
	public void setSigningKey(String key) {
		Assert.hasText(key);
		key = key.trim();

		this.signingKey = key;

		if (isPublic(key)) {
			signer = new RsaSigner(key);
			logger.info("Configured with RSA signing key");
		}
		else {
			// Assume it's a MAC key
			this.verifierKey = key;
			signer = new MacSigner(key);
		}
	}
// ......

我们之前使用的 setSigningKey(String key) 方法所传入的 key 被用于创建了 SingerVerifier 的实现类 --MacSigner 对象。

该方法也可以由传入的 key 类型来判断应该生成的 签名器 对象。

查看 isPublic(String key) 方法

// ......
	private boolean isPublic(String key) {
		return key.startsWith("-----BEGIN");
	}
// ......

由此可以判断传入的 key 是否是 RSA 公钥,从而创建响应的 RSA 签名器 RSASigner

然而我这里并没有使用 setSigningKey(String key) 的方法来创建 RSASigner 签名器

// ......
	    KeyPair keyPair = RsaUtils.getKeyPair();
            tokenConverter.setKeyPair(keyPair);
// ......

而是通过 setKeyPair(KeyPair keyPair) 方法直接创建了一个 RSASigner 对象RSAVerifier 对象 传递给 JwtAccessTokenConverter 从而完成对 JWT 的 RSA 非对称 加密,这一点我们可以从源码中看出:

	public void setKeyPair(KeyPair keyPair) {
		PrivateKey privateKey = keyPair.getPrivate();
		Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
		signer = new RsaSigner((RSAPrivateKey) privateKey);
		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
		verifier = new RsaVerifier(publicKey);
		verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
				+ "\n-----END PUBLIC KEY-----";
	}

🚀 这里不禁要感叹一句 SpringSecurity 真的是越用越好用,每一次都能给我带来惊喜!

我们测试一下,使用 postman 请求密码方式获取 token

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJzZWNyZXQiOiJwcml2YXRlX3NlY3JldCIsImV4cCI6MTU4NDUwNzU1NiwiYXV0aG9yaXRpZXMiOlsidXNlcjpsaXN0IiwiZGVwdDpkaXNzb2x2ZSIsInJvbGU6dXBkYXRlIiwidXNlcjpwYXNzd29yZCIsImRlcHQ6YWRkIiwicGVybTppbmZvIiwicm9sZTppbmZvIiwicm9sZTphZGQiLCJ1c2VyOnNldFJvbGVzIiwiZGVwdDpsaXN0Iiwicm9sZTpyZW1vdmUiLCJ1c2VyOmluZm8iLCJwZXJtOnVwZGF0ZSIsInJvbGU6bGlzdCIsInBlcm06bGlzdCIsInBlcm06cmVtb3ZlIiwicm9sZTpzZXRQZXJtcyIsImRlcHQ6aW5mbyIsInBlcm06YWRkIiwidXNlcjp1cGRhdGUiLCJ1c2VyOnJlbW92ZSIsImRlcHQ6dXBkYXRlIiwidXNlcjpzZXREZXB0IiwiUk9MRV9BRE1JTiIsInVzZXI6YWRkT25lIl0sImp0aSI6ImQwYjBkMzA2LWY1NmUtNDEzMi05MDUzLTJlODA3ZTFkYTYwNSIsImNsaWVudF9pZCI6InRlc3QifQ.dxuqIl_Tevpv2UBA5yDkPmYVvehJv4RisUEAqD9WJVgENXpx4SoZk3GrAo_2DuNSwUVt6ayPjkLcXQAJzj5OdYUfSQ4c0gCwJ4MJ-wi0cT0WPWJCF2X8WwBu-Cnwu-XdzlymOpsJp_XUAhUFffMnAe-ZhdAOwoxtAcqN-m_e6UE-UTYUYsIwnPp9t7gNWw0erOfXM7c4ezOmLhd9_7QGE63qHZMeHYA9T42J2Ca9N6WdOL6Eh9dN1euSOz0JBsI4VaA2PJZGRR90r0RQV1eG8eH-_iVUXK66wZlkzMXfalujKIjgepcCDZc5wP-EmBgBqBywDSLXvkefW6nhT82zNw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJkMGIwZDMwNi1mNTZlLTQxMzItOTA1My0yZTgwN2UxZGE2MDUiLCJzZWNyZXQiOiJwcml2YXRlX3NlY3JldCIsImV4cCI6MTU4NDU5MDM1NiwiYXV0aG9yaXRpZXMiOlsidXNlcjpsaXN0IiwiZGVwdDpkaXNzb2x2ZSIsInJvbGU6dXBkYXRlIiwidXNlcjpwYXNzd29yZCIsImRlcHQ6YWRkIiwicGVybTppbmZvIiwicm9sZTppbmZvIiwicm9sZTphZGQiLCJ1c2VyOnNldFJvbGVzIiwiZGVwdDpsaXN0Iiwicm9sZTpyZW1vdmUiLCJ1c2VyOmluZm8iLCJwZXJtOnVwZGF0ZSIsInJvbGU6bGlzdCIsInBlcm06bGlzdCIsInBlcm06cmVtb3ZlIiwicm9sZTpzZXRQZXJtcyIsImRlcHQ6aW5mbyIsInBlcm06YWRkIiwidXNlcjp1cGRhdGUiLCJ1c2VyOnJlbW92ZSIsImRlcHQ6dXBkYXRlIiwidXNlcjpzZXREZXB0IiwiUk9MRV9BRE1JTiIsInVzZXI6YWRkT25lIl0sImp0aSI6IjBmNzJhMmIyLTY4N2MtNGM0MC1hZWM1LWE1N2QyYzZiODA5MSIsImNsaWVudF9pZCI6InRlc3QifQ.RCMbCvmFzh_Rxbl-Vz6WccNHWn35gR3cQyUEJYvQFaXRJbAudXU72i3I2bwngn04HVfwJtRW2B-wJzmcGfpz5zRq74OrCKWESk0-kxg91hf82FgPf-LLFZRzlrNF1QCRoxfHRNy9BdIro-lxhoaQu9RxW4YmKOD12fpaStaAD8N7fGHRQRHFKpZvcp-8r_opUzavxx7HK-80rvJJizzKN1LnOcj3InUY-w6PYHwoJ_vdKmTwESoJmoT8Rr7BtmuFlIdC5vtu9fYiEkatjEfQxJ_1LoQew1R3WGBaeL6vpIdMQ5F-JtCGQoo6sp9b4lJjsYO4Wo9QgmSQ58J4y-vVAw",
    "expires_in": 3599,
    "scope": "all",
    "secret": "private_secret",
    "jti": "d0b0d306-f56e-4132-9053-2e807e1da605"
}

我们对 token 进行解码,便捷起见,只看头部信息

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

很显然,JWT 的加密方式为 RS256

至此,JWT 在 Spring Security 中 对称加密非对称加密 两种方式我们都有所了解了。


3. 总结

JWT 是目前主流的信息加密信息传输的方式,掌握它是很有必要的。后续还将使用其他的语言对其进行应用,在这里将会一一记录。

🚀 相关链接:

  1. JWT 简介:https://jwt.io/introduction/
  2. Spring Security OAuth2 JWT 应用:SpringSecurity OAuth2 实现应用授权

关于作者:NekoChips
本文地址:https://chenyangjie.com.cn/articles/2020/03/15/1584254901664.html
版权声明:本篇所有文章仅用于学习和技术交流,本作品采用 BY-NC-SA 4.0 许可协议,如需转载请注明出处!
许可协议:知识共享许可协议