首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

JWT(auth0):RS256非对称加密算法实现Token的签发、验证

前言

?日常开发中,客户端与服务器通常采用 HTTP 协议进行通信,但 HTTP 是没有状态的,无法记录用户的身份信息和行为。

?会话跟踪技术是一种在客户端与服务器间保持 HTTP 状态的解决方案,我们所熟知的有 Cookie + Session、URL 重写、Token 等。

?Cookie 在浏览器保存 SessionID、Session 实际内容保存在服务端,目前的项目都是前后端分离 + 微服务,所以会面临 Session 共享问题,随着用户量的增多,开销就会越大。URL 重写又是通过明文传输,不安全容易被劫持。

Token 的优势:

  • Token 支持跨域访问,Cookie 不可以跨域访问。
  • Token 支持多平台,Cookie 只支持部分 web 端。

What is JWT ?

?JWT 的全称是 Json Web Token,是一种基于 JSON 的、用于在网络上声明某种主张的令牌(token)规范。

?官方解释:

JWT 由三部分组成:hand、payload、signature,各部分通过 ‘ . ’ 连接

xxxx . yyyy . zzzz

1、HEAD

?头部是一个 JSON 对象,存储描述数据类型(JWT)和签名算法(HSA256、RSA256),通过 Base64UrlEncode 编码后生成 head 。

2、PAYLOAD

?负载存放一些传输的有效声明,可以使用官方提供的声明,也可以自定义声明。同样通过 Base64UrlEncode 编码后生成 payload。声明可以分为三种类型:

  • Registered claims:

官方预定义的、非强制性的但是推荐使用的、有助于交互的声明(注意使用这些声明只能是三个字符)。

  • Public claims:

保留给 JWT 的使用者自定义。但是需要注意避免使用IANA JSON Web Token Registry中定义的关键字。

  • Private claims:

保留给 JWT 的使用者自定义,用来传送传输双方约定好的消息。((—_—)是不是没搞懂 public claims 和 private claims 的区别,阿浪也不知道)

3、SIGNATURE

?数据签名是 JWT 的核心部分,构成较为复杂,且无法被反编码。

代码语言:javascript
复制
HS256加密:
signature?=?HMACSHA256(?base64UrlEncode(header)?+?"."?+base64UrlEncode(payload),?secret?);
???
RS256加密:??????
signature?=?RSASHA256(base64UrlEncode(header)?+?"."?+base64UrlEncode(payload),?publicKey,?privateKey)

signature 可以选择对称加密算法或者非对称加密算法,常用的就是 HS256、RS256。

  • 对称加密: 加密方和解密方利用同一个秘钥对数据进行加密和解密。
  • 非对称加密: 加密方用私钥加密,并把公钥告诉解密方用于解密。

4、JWT 执行逻辑

?逻辑清晰明了,用户首次登陆时,通过传输账号密码验证身份,验证成功后,服务器生成 Token 响应用户。用户后续请求只需要传送 Token,服务器只需对 Token 进行校验来确认身份。

双 Token 保证 活跃用户

1、活跃用户

?Token 用于身份认证时,如果有效期设置太长,泄露了会不安全。如果设置太短,用户频繁的重新登陆,程序员的祖坟有可能不保。那如何界定有效时间呢?这就要引入一个概念:用户的活跃性。

?系统把用户分为活跃用户和不活跃用户,对于不活跃用户,token 过期后需要重新登陆,因为使用频率较低,token 失活后重新登陆,他的感受没有那么强烈。

?活跃用户在 token 过期后,不应该直接登陆,而是要根据他的活跃时间来判定是否重新激活 token,当符合条件时,直接激活 Token,带给用户最好的体验。

若 token 有效期时长为 at,活跃用户时长计为 rt,且用户每次操作客户端后活跃时间都与之同步刷新。

  • 当 rt == at 时

这种情况,当然可以确定属于活跃用户。在整个 token 的有效期用户都在操作,如果这时 token 失效让重新登陆,用户体验确实不好。

  • 假设存在 rt > at 的情况

既然 rt 与 at 相等时,属于活跃用户,这种可以算是激进分子了。

  • 当 rt < at 时

这种情况比较复杂,我们无法界定 rt 在 at 中所占比例为多少时属于活跃用户,而且我们也无法推测 token 失效后,用户啥时候再次请求,因此定义为不活跃用户。

2、accessToken、refreshToken 两兄弟

用户首次登陆后,获得 accessToken(时长较短)和 refreshToken(时间较长),每次请求判断 accessToken 是否过期。

当 access_token 过期后,判断 refreshToken 是否过期,若没过期,则通过 refreshToken 刷新获取新的 access_token,如果都过期,就需要重新登录了。

由活跃用户分析可知,当 rt >= at,用户在 at 时间内都是活跃的。设 accessToken 的有效期为用户的活跃时间 rt,当 rt <= refresh_Time,直接刷新 rt。所以可以认为 [accessToken 创建开始时间点 ,2 * accessToken 有效时长 ] 时间内用户是活跃的

建议:refreshToken 时间 >= 2 * accessToken 时间。

auth0 大法

JWT 只是规范,就像 Java 中的接口,无法直接使用,需要一个实现规范的具体实现库。平时开发中较多使用 jjwt,据传 auth0 的底层实现效率更高。注意,auth0 不是 OAuth2,不要搞混了。

首先,加入 maven 依赖,最新版本就是 3.16.0。

HS256 算法

HS256 是对称加密算法,相对来说比较简单易上手,网上例子也很详尽,感兴趣可以自己查找资料。我们主要来看看非对称加密算法。

RS256 算法

1、生成密钥对

想签发 Token,首先要生成 PublicKey 和 PrivateKey。JDK 的 java.security. interfaces 包提供了 RS 算法的密钥对类型。我们直接构建一个存方密钥对的 POJO 类。

代码语言:javascript
复制
public?class?RSA256Key?{

????private?RSAPublicKey?publicKey;
????private?RSAPrivateKey?privateKey;

????public?RSA256Key()?{
????}

????public?RSA256Key(RSAPublicKey?publicKey,?RSAPrivateKey?privateKey)?{
????????this.publicKey?=?publicKey;
????????this.privateKey?=?privateKey;
????}
}
***省略getter和setter***

然后写一个密钥生成的工具类,通过官方信息可知,密钥对的实例生成后可重复使用。因此,我打算采用单例的双重校验锁来控制密钥对象的生成。如果并发量过大的话,自己可以加一个自定义线程池去生成。

代码语言:javascript
复制
?????//数字签名
????public?static?final?String?KEY_ALGORITHM?=?"RSA";

????//RSA密钥长度
????public?static?final?int?KEY_SIZE?=?1024;

????//唯一的密钥实例
????private?static?volatile?RSA256Key?rsa256Key;

??/**
?????*?生成?公钥/私钥
?????*
?????*??由双重校验锁保证创建唯一的密钥实例,因此创建完成后仅有唯一实例。
?????*??当被JVM回收后,才会创建新的实例
?????*?@return
?????*?@throws?NoSuchAlgorithmException
?????*/
????public?static?RSA256Key?generateRSA256Key()?throws?NoSuchAlgorithmException?{

????????//第一次校验:单例模式只需要创建一次实例,若存在实例,不需要继续竞争锁,
????????if?(rsa256Key?==?null)?{
????????????//RSA256Key单例的双重校验锁
????????????synchronized(RSA256Key.class)?{
????????????????//第二次校验:防止锁竞争中自旋的线程,拿到系统资源时,重复创建实例
????????????????if?(rsa256Key?==?null)?{
????????????????????//密钥生成所需的随机数源
????????????????????KeyPairGenerator?keyPairGen?=?KeyPairGenerator.getInstance(KEY_ALGORITHM);
????????????????????keyPairGen.initialize(KEY_SIZE);
????????????????????//通过KeyPairGenerator生成密匙对KeyPair
????????????????????KeyPair?keyPair?=?keyPairGen.generateKeyPair();
????????????????????//获取公钥和私钥
????????????????????RSAPublicKey?publicKey?=?(RSAPublicKey)?keyPair.getPublic();
????????????????????RSAPrivateKey?privateKey?=?(RSAPrivateKey)?keyPair.getPrivate();
????????????????????rsa256Key?=?new?RSA256Key();
????????????????????rsa256Key.setPublicKey(publicKey);
????????????????????rsa256Key.setPrivateKey(privateKey);
????????????????}

????????????}
????????}
????????return?rsa256Key;
????}

单例的双重校验锁能够严格保证 RSAPublicKey 对象生成的唯一性,当线程们进入 generateRSA256Key()方法验证实例对象为空时,最快的线程拿到锁资源,并阻塞后续线程。

KeyPairGenerator 是密钥生成的核心类,根据我们自定义的密钥长度 KEY_SIZE 来生成密钥。密钥生成创建 RSA256Key 实例对象时,此处有个坑(当然是并发量足够大时),希望有大佬指点:虽然 synchronized 阻塞住了部分线程,但当 RSA256Key 实例化后还未赋值前,正巧有新线程刚检测 rsa256Key,直接跳到后续逻辑,因为密钥实例值为空报出异常

2、签发 Token

?Token 的签发逻辑很简单,auth0 为我们封装的很好,只需要向 Algorithm 的静态方法 RSA256 传递私钥,通过 JWT 类内的 withXXX()方法传参即可。

代码语言:javascript
复制
?/**
?????*?签发Token
?????*
?????*?withIssuer()给PAYLOAD添加一跳数据?=>?token发布者
?????*?withClaim()给PAYLOAD添加一跳数据?=>?自定义声明?(key,value)
?????*?withIssuedAt()?给PAYLOAD添加一条数据?=>?生成时间
?????*?withExpiresAt()给PAYLOAD添加一条数据?=>?保质期
?????*
?????*?@param?data
?????*?@return
?????*?@throws?NoSuchAlgorithmException
?????*/
????public?static?String?creatTokenByRS256(Object?data)?throws?NoSuchAlgorithmException?{
????????//初始化?公钥/私钥
????????RSA256Key?rsa256Key?=?SecretKeyUtil.generateRSA256Key();

????????//加密时,使用私钥生成RS算法对象
????????Algorithm?algorithm?=?Algorithm.RSA256(rsa256Key.getPrivateKey());

????????return?JWT.create()
????????????????//签发人
????????????????.withIssuer(ISSUER)
????????????????//接收者
????????????????.withAudience(data.toString())
????????????????//签发时间
????????????????.withIssuedAt(new?Date())
????????????????//过期时间
????????????????.withExpiresAt(DateUtil.addHours(2))
????????????????//相关信息
????????????????.withClaim("data",?JsonUtil.toJsonString(data))
????????????????//签入
????????????????.sign(algorithm);
????}
  • 有个值得吐槽的一点(下面是对 auth0 的源码分析,不感兴趣的可以跳过)
代码语言:javascript
复制
?????/**
?????*?Creates?a?new?Algorithm?instance?using?SHA256withRSA.?Tokens?specify?this?as?"RS256".
?????*
?????*?@param?key?the?key?to?use?in?the?verify?or?signing?instance.
?????*?@return?a?valid?RSA256?Algorithm.
?????*?@throws?IllegalArgumentException?if?the?Key?Provider?is?null.
?????*?@deprecated?use?{@link?#RSA256(RSAPublicKey,?RSAPrivateKey)}?or?{@link?#RSA256(RSAKeyProvider)}
?????*/
????@Deprecated
????public?static?Algorithm?RSA256(RSAKey?key)?throws?IllegalArgumentException?{
????????RSAPublicKey?publicKey?=?key?instanceof?RSAPublicKey???(RSAPublicKey)?key?:?null;
????????RSAPrivateKey?privateKey?=?key?instanceof?RSAPrivateKey???(RSAPrivateKey)?key?:?null;
????????return?RSA256(publicKey,?privateKey);
????}
????
????/**
?????*?Creates?a?new?Algorithm?instance?using?SHA256withRSA.?Tokens?specify?this?as?"RS256".
?????*
?????*?@param?publicKey??the?key?to?use?in?the?verify?instance.
?????*?@param?privateKey?the?key?to?use?in?the?signing?instance.
?????*?@return?a?valid?RSA256?Algorithm.
?????*?@throws?IllegalArgumentException?if?both?provided?Keys?are?null.
?????*/
????public?static?Algorithm?RSA256(RSAPublicKey?publicKey,?RSAPrivateKey?privateKey)?throws?IllegalArgumentException?{
????????return?RSA256(RSAAlgorithm.providerForKeys(publicKey,?privateKey));
????}
?

?因为我们是使用的 RSAPublicKey 和 RSAPrivateKey 存储的密钥,而且两种类型都继承自 RSAKey,所以我们可以直接调用 RSA256(RSAKey key),只需传入私钥,逻辑会自动为公钥赋 null,顺序调用第二个方法。

但是该方法标记了 @Deprecated,说明官方废除了这方法。我们只能直接调用第二个方法,所以传参需要我们自己指定 null 值,而且有些不了解 RS256 算法的人, 会同时传入公钥与私钥。

代码语言:javascript
复制
?????/**
?????*?Creates?a?new?Algorithm?instance?using?SHA256withRSA.?Tokens?specify?this?as?"RS256".
?????*
?????*?@param?keyProvider?the?provider?of?the?Public?Key?and?Private?Key?for?the?verify?and?signing?instance.
?????*?@return?a?valid?RSA256?Algorithm.
?????*?@throws?IllegalArgumentException?if?the?provided?Key?is?null.
?????*/
????public?static?Algorithm?RSA256(RSAKeyProvider?keyProvider)?throws?IllegalArgumentException?{
????????return?new?RSAAlgorithm("RS256",?"SHA256withRSA",?keyProvider);
????}

?通过调用上面两个方法生成 RSAKeyProvider,调入该方法,最终生成 Algorithm 对象。

3、校验 Token

?校验与签发同样简单,只是通过 PublicKey 生成 Algorithm,因为我把加密解密都放在了服务端,省去了很多不必要的麻烦。

代码语言:javascript
复制
??public?static?boolean?verifierToken(String?token)?throws?NoSuchAlgorithmException?{

????????//获取公钥/私钥
????????RSA256Key?rsa256Key?=?SecretKeyUtil.generateRSA256Key();

????????//根据密钥对生成RS256算法对象
????????Algorithm?algorithm?=?Algorithm.RSA256(rsa256Key.getPublicKey());

????????System.out.println("PublicKey:?"?+?rsa256Key.getPublicKey().getPublicExponent());

????????//解密时,使用gong钥生成算法对象
????????JWTVerifier?verifier?=?JWT.require(algorithm)
????????????????????????????????????.withIssuer(ISSUER)
????????????????????????????????????.build();

????????try?{
????????????//验证Token,verifier自动验证
????????????DecodedJWT?jwt?=?verifier.verify(token);
????????????return?true;
????????}catch?(JWTVerificationException?e){
????????????log.error("Token无法通过验证!?"?+?e.getMessage());
????????????return?false;
????????}

通过 JWTVerifier 对象可生成 DecodedJWT,如果想获取具体的 TOken 信息,可通过 DecodedJWT 获取。

对 auth0 底层实现感兴趣的同学,可以从 gitHub 上 clone 下来自己跑起来看一看。学一学别人源码中的设计模式和逻辑处理。

源码:github.com/auth0/java-jwt

文中所涉及的代码在我的 GitHub 中:西门阿浪

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/e55bb7e46be860902e39f9280
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com