jwt问题记录

一、版本

1.1 新旧版本maven依赖不同

0.9.x及以下的版本是这样导入的:

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

0.10.x及以上的版本是这样导入的:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <!-- 可选:jjwt-gson/jjwt-orgjson/jjwt-jackson -->
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

1.2 新旧版本接口调用方式不同

创建JwtToken

0.9.x及以下的版本是这样创建JwtToken的:

builder.setExpiration(LocalDateUtil.localDateTime2Date(LocalDateTime.now().plusSeconds(expire)))
	.signWith(SignatureAlgorithm.RS256,RSA_KEY_HELPER.getPrivateKey64(privateKeyPath)).compact();

0.10.x及以上的版本是这样创建JwtToken的:

builder.setExpiration(LocalDateUtil.localDateTime2Date(LocalDateTime.now().plusSeconds(expire)))
	.signWith( RSA_KEY_HELPER.getPrivateKey64(privateKeyPath),SignatureAlgorithm.RS256).compact();

其实没太大变化,只是调换了signWith方法的参数位置。

解析JwtToken

0.9.x及以下的版本是这样解析JwtToken的:

Jwts.parser().setSigningKey(RSA_KEY_HELPER.getPublicKey64(pubKeyPath)).parseClaimsJws(token);

0.10.x及以上的版本是这样解析JwtToken的:

Jwts.parserBuilder().setSigningKey(RSA_KEY_HELPER.getPublicKey64(pubKeyPath)).build().parseClaimsJws(token);

其实也没太大变化,只是多了层Builder。

二、密钥字节长度问题

2.1 RS256算法的密钥字节必须大于等于2048

公钥和私钥的最小字节数必须大于等于2048,否则会报错。

io.jsonwebtoken.security.WeakKeyException: The verification key's size is 1024 bits which is not secure enough for the RS256 algorithm.  The JWT JWA Specification (RFC 7518, Section 3.3) states that keys used with RS256 MUST have a size >= 2048 bits.  Consider using the io.jsonwebtoken.security.Keys class's 'keyPairFor(SignatureAlgorithm.RS256)' method to create a key pair guaranteed to be secure enough for RS256.  See https://tools.ietf.org/html/rfc7518#section-3.3 for more information.
	at io.jsonwebtoken.SignatureAlgorithm.assertValid(SignatureAlgorithm.java:424)
	at io.jsonwebtoken.SignatureAlgorithm.assertValidVerificationKey(SignatureAlgorithm.java:315)
	at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:364)
	at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:513)
	at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:573)

jwt其他密钥长度规定:

RS256和PS256的密钥长度至少2048位字节
RS384和PS384的密钥长度至少3072位字节
RS512和PS512的密钥长度至少4096位字节

2.2 RSA生成公钥和私钥

import tang.zhiyin.base.util.Base64Util;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;

public class KeyGenUtil {

    /**
     * 生成RSA公钥和私钥
     *
     * @return Key[0]=公钥,Key[1]=私钥
     * @throws Exception
     */
    public static Key[] createKey() throws Exception {
        Key[] keys = new Key[2];
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
        keyPairGen.initialize(2048);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        keys[0] = keyPair.getPublic();
        keys[1] = keyPair.getPrivate();
        return keys;
    }

    public static void main(String[] args) throws Exception {
        Key[] keys = createKey();
        String publicKey = Base64Util.encodeBase64(keys[0].getEncoded());
        System.out.println(publicKey);
        String privateKey = Base64Util.encodeBase64(keys[1].getEncoded());
        System.out.println(privateKey);
    }
}

三、jwt的缺点和问题

3.1 令牌一经发放,无法作废

jwt的令牌一旦生成,在有效期内无法作废,这是一个很严重的问题
比如,后台管理员将某个用户禁用了,此时如果你只是使用jwt来验证用户,那么此用户仍然可以登录。

解决办法:

方法1、将jwtToken存入redis或mysql中,并在数据库中设置jwtToken的有效期(一般和jwt中保持一致)。在验证用户(即验证jwtToken)的时候,也从redis或者mysql中拉取数据库中的有效期,如果数据库中的有效期过期,则登录失败。禁用用户时,只需要修改redis或mysql中jwtToken的有效期即可。

方法2、和方法1基本相同,jwtToken中包含一个userToken,将userToken存入redis或mysql中,并在数据库中设置userToken的有效期。在验证用户(即验证jwtToken)的时候,也从redis或者mysql中拉取数据库中userToken的有效期,如果数据库中的有效期过期,则登录失败。禁用用户时,只需要修改redis或mysql中userToken的有效期即可。
实操:

@Data
public class JwtTokenJson {
    private String userId;
    private String userName;
    private String userToken;
}
JwtBuilder builder= Jwts.builder()
	.setSubject(String.valueOf(jwtTokenJson.getUserId()))
	.claim(JwtInfoKeys.username, jwtTokenJson.getUserName())
	.claim(JwtInfoKeys.usertoken, jwtTokenJson.getUserToken());
String jwtToken = builder.setExpiration(LocalDateUtil.localDateTime2Date(LocalDateTime.now().plusSeconds(expire))).signWith( RSA_KEY_HELPER.getPrivateKey64(privateKeyPath),SignatureAlgorithm.RS256).compact();

jwtToken里包含一个userToken,userToken的有效期交给Redis或者mysql管理。这样一来,用户即使jwtToken没过期,但如果userToken过期了也无法登录成功。

注意

如果放入redis中还好,如果放入mysql中则严重影响服务性能,因为这个查询token是否过期的操作,每次请求都会调用。相当于放弃jwt的一个优势:验证用户登录时,无需查询数据库。

3.2 令牌一经发放,无法延长有效期

jwt的令牌一旦生成,无法做任何修改,无法延期,这也是一个很严重的问题
比如你令牌的有效期是5分钟,你无法做到,在4分钟的时候给这个令牌重新续期5分钟,即使用户在4分钟的时候操作过页面也不行。
jwt无法像session一样可以自动续期,session的有效期是只要有户有新的操作,那么session有效期会自动更新,jwt则不行。

解决办法:

方法1、将jwtToken存到mysql或redis,捕获ExpiredJwtException异常,重新生成jwtToken

try {
            return Jwts.parserBuilder().setSigningKey(RSA_KEY_HELPER.getPublicKey64(pubKeyPath)).build().parseClaimsJws(jwtToken);
        } catch (ExpiredJwtException ex) {//过期
            //使用token查询mysql或redis(前提是你有将token存到mysql或redis)相关的用户信息
            //根据用户信息重新生成token
          	...
        }

方法2、双jwtToken互相续期。

用户登录后生成两个jwtToken给前端,一个有效期短,另一个有效期长。
验证用户登录时,校验两个jwtToken,只要其中有一个校验成功,即认为验证通过。
并且,如果有过期的jwtToken,则生成一个新的jwtToken,代替过期的那个jwtToken并返回前端。

举例说明,用户登录后生成两个jwtToken给前端,一个叫A有效期2天,一个叫B有效期4天。如果用户在第3天有操作网站,那么验证jwtToken的地方,需要将A丢弃,并生成一个新的jwtToken-C,有效期4天,顶替A的位置,用户在第5天如果有操作网站,那么验证jwtToken的地方,需要将B丢弃,并生成一个新的jwtToken-D,有效期4天,顶替B的位置。

方法3、每次请求都重新创建一个jwtToken。
此方法能实现自动续期、无期限续期的效果。

注意

方法2优点:不用查询数据库。
方法2缺点:双倍的token数据,浪费带宽。双倍的代码量更难维护。
方法3不推荐使用,太浪费计算资源了,但是不追求性能的CMS、OA、CRM、HR之类的系统可以用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值