02.JWT

134 阅读9分钟

JWT

1.是什么

JSON Web Token(JWT)是一种可以在多方之间安全共享数据的开放标准,JWT数据经过编码和数字签名生成,可以确保其真实性,也因此JWT通常用于身份认证

2.结构

组成

JWT其实就是一个很长的字符串,字符之间通过"."分隔符分为三个子串,各字串之间没有换行符。每一个子串表示了一个功能块,总共有三个部分:JWT头(header)、有效载荷(payload)、签名(signature),如下图所示:

image.png

示例

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52\_iSz4bQMYJjkI\_TLQ

Header

Header部分主要存储关于签名算法的信息,通常包含两个部分:token类型和采用的加密算法,使用Base64Url编码

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

Payload

存储业务数据的部分(也是一个JSON对象),一般在不特殊修改的情况下(可自定义字段),最后也是使用Base64 URL算法转换为String,主要包含几个部分:

  1. iss:发行人
  2. exp:到期时间
  3. sub:主题
  4. aud:用户
  5. iat:发布时间
  6. jti:JWT ID用于标识该JWT
  7. nbf:在此之前不可用

Signature

签名实际上是一个加密的过程,是对上面两部分数据通过指定的算法生成哈希,以确保数据不会被篡改

首先需要指定一个密码(secret),该密码仅仅保存在服务器中,并且不能向用户公开。然后使用WT头中指定的签名算法(默认情况下为HMAC SHA256),根据以下公式生成签名哈希:

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象

3.签名算法

  1. JWT签名算法,一般有两种选择:HS256和RS256
  2. HS256(带有SHA-256的HMAC)是一种对称加密算法,双方之间仅共享一个密钥。由于使用相同的密钥生成签名和验证签名,因此必须注意确保密钥不被泄露
  3. RS256(采用SHA-256的RSA签名)是一种非对称加密算法,它使用公共/私钥对;JWT的提供方采用私钥生成签名,JWT的使用方获取公钥以验证签名

4.工作流程

900b3e81f832b2f08c2e8aabb540536a.png asdkaosdji.png 89123u1293u12831l.png

5.注意

JWT不是用来做数据加密的,JWT不保证数据不泄露所以敏感的数据不要向JWT中存储,也可以理解为JWT的内容可以被解析但是不可以被篡改

6.特点

  1. 无状态:JWT包含认证信息,token只需要保存在客户端,服务端不需要保存会话信息,所以JWT是无状态的。这一点使得服务器压力大大降低,增加了系统的可用性和可扩展性。但是无状态同时也是JWT最大的缺点,服务器无法主动让token失效
  2. 安全性:客户端每次请求都会携带token,可以有效避免CSRF攻击,同时token会自动过期,可以减少token被盗用的情况,服务器会通过jwt签名验证token,可以避免token被篡改
  3. 可见性:JWT的payload部分可以直接通过Base64解码,所以可存储一些其他业务逻辑所必要的非敏感信息

7.问题

续期

传统的会话续期

传统httpsession和spring-session都是采用httpsession默认机制。内部会有线程不停的轮询会话列表。把那些内存中的会话列表中的过期时间和当前时间进行比较,如果超过> 30分钟 自动把session删除。如果在30以内的请求,会自动续期(时间会从0开始计数)

为什么要这样做?

你思考。如果没有续期,会怎么样?就好比你登录腾讯游戏,你登录有效时间是30分钟。那么也就意味着你每隔30分钟要退出重新登录一次。所以我们应该是在你登录以后,未来的每一次请求中,只要用户一直在发起请求,就把时间永远覆盖。这样就永远保持热活。那么不用在登录。除非你静默的实际超过了30分钟,那确实需要重新登录

83d6c48c4f42456084c7502e0310d653.png

注意:续期必须要支持注销JWT,否则无限续期跟永久JWT没区别

必须续期吗

不一定要必须续期,例如阿里云就没有做续期,虽然会存在会话突然失效的情况,但是会保存当前表单内容,减少对用户的影响,没有了续期就符合了JWT的思想:去中心化,无状态化

续期方案总览
  1. 双令牌机制
  2. Redis令牌不变变续期
双令牌机制第一种
  1. 登陆成功后,生成两个Token,accessToken(5天)与refreshToken(10天),并且把refreshToken存储到Redis中,设置过期时间
  2. 接口返回3个字段分别是:accessToken,refreshToken,accessToken过期时间戳
  3. 前端向后端请求的时候,判断accessToken是否快过期了,如果快过期了,则客户端使用refreshToken请求刷新令牌接口获取新的accessToken,并更新前端本地的accessToken,后端主要是生成新的双令牌并且返回。
  4. 如果存储在Redis中的refreshToken过期,则提示重新登陆
  5. refreshToken可以是随机字符串,不用也是JWT字符串
  6. 缺点:前端每次请求需要判断accessToken距离过期时间
  7. 优点:后端压力小,代码逻辑改动不大

59e843a732fb453494465217ec48880e.png

双令牌机制第二种
  1. 登陆成功后,生成两个Token,accessToken(30分钟)与refreshToken(1小时)
  2. 前端请求携带两个Token
  3. 服务端行为如下:
    • 如果accessToken在30分钟内,那么处理请求并响应
    • 如果accessToken过期并且refreshToken未过期,那么调用刷新方法重新生成两个Token然后处理请求并返回(两个Token附加在响应头返回),前端替换原有的两个令牌
    • 如果两个Token都过期,拒绝请求并返回,前端清除两个令牌
  4. 客户端同一个用户并发请求造成的令牌交叉覆盖问题可以忽略一般

e5b72ce8d0054eda9f10e13d56e99ae8.png 9da4a510ecb64187b7fb1d1fb9458ec3.png 999bc2e24fee4753b3a036efd254256f.png

Redis令牌不变变续期
  1. 登录成功后,生成accessToken,并把accessToken写入Redis(key是随机数,value是accessToken),过期时间比如是2小时
  2. 客户端发起请求,服务端做以下行为:
    • accessToken有效性校验
    • 获得Redis的key,从Redis中获取accessToken
    • 如果key不存在,则拒绝请求
    • 距离结束时间超过1小时的,正常处理请求并返回
    • 距离结束时间不足1消失的,正常处理返回响应,利用expire命令增加一小时
    • 2小时内没有收到任何请求,key已被自动删除,拒绝请求
  3. 要注销JWT,直接删除掉key即可
  4. 缺点:引入了Redis,破坏了JWT的无状态
  5. 优点:前端无感知,key生成规则复杂能尽量避免被盗用

e8af0cf838014ffa95af66f74ede8f7a.png

盗用

解决案例:在使用阿里云或淘宝的时候,登录了阿里云换了个地域或网络就需要重新登录。这是因为对应的token,不只是简单的保存了用户个人信息还保存了地理网络位置信息等,一起组成token,一旦有变化就会要求重新登录或短信验证等方式

解决方案:生成令牌的时候,加密的payload加入当前用户的ip,拦截器解密后获取payload的ip和当前访问ip是否是同一个,如果不是则提示重新登陆

优点:服务端无需存储相关内容,性能高,假如用户广州登录,泄露了token给杭州的黑客,依旧用不了

缺点:如果用户使用过程中ip频繁变动,则操作会经常提示重新登录,体验不友好

8.Java整合

介绍

jjiwt是一个提供JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解

依赖

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

整合

// 通过jjwt生成和解析令牌,不使用签名算法
@Test
public void test1(){
    // Header部分
    Map header = new HashMap();
    header.put("alg","none");
    header.put("type","JWT");
    // Body部分
    Map body = new HashMap();
    body.put("userId","100");
    body.put("account","admin");
    body.put("role","admin");
    // 生成jwt,id是个标识不给也可以唯一即可
    String jwt = Jwts.builder()
        .setHeader(header)
        .setClaims(body)
        .setId("101")
        .compact();
    // 解析jwt
    Jwt result = Jwts.parser().parse(jwt);
    Header header1 = result.getHeader();
    Object obj = result.getBody();
}
// 通过jjwt生成和解析令牌,使用对称加密的签名算法
@Test
public void test2(){
    // Header部分
    Map header = new HashMap();
    header.put("alg",SignatureAlgorithm.HS256.getValue());
    header.put("type","JWT");
    // Body部分
    Map body = new HashMap();
    body.put("userId","100");
    body.put("account","admin");
    body.put("role","admin");
    // 生成jwt,id是个标识不给也可以唯一即可
    String jwt = Jwts.builder()
        .setHeader(header)
        .setClaims(body)
        .setId("101")
        .signWith(SignatureAlgorithm.HS256,"密钥内容")
        .compact();
    // 解析jwt
    Jwt result = Jwts.parser().setSigningKey("密钥内容").parse(jwt);
    Header header1 = result.getHeader();
    Object obj = result.getBody();
}
// 通过jjwt生成和解析令牌,使用非对称加密的签名算法
@Test
public void test3(){
    // Header部分
    Map header = new HashMap();
    header.put("alg",SignatureAlgorithm.HS256.getValue());
    header.put("type","JWT");
    // Body部分
    Map body = new HashMap();
    body.put("userId","100");
    body.put("account","admin");
    body.put("role","admin");
    // 生成jwt,id是个标识不给也可以唯一即可
    String jwt = Jwts.builder()
        .setHeader(header)
        .setClaims(body)
        .setId("101")
        .signWith(SignatureAlgorithm.HS256,"密钥内容")
        .compact();
    // 解析jwt
    Jwt result = Jwts.parser().setSigningKey("密钥内容").parse(jwt);
    Header header1 = result.getHeader();
    Object obj = result.getBody();
}
// 通过jjwt生成和解析令牌,使用对称加密的签名算法
@Test
public void test4(){
    // Header部分
    Map header = new HashMap();
    header.put("alg",SignatureAlgorithm.RS256.getValue());
    header.put("type","JWT");
    // Body部分
    Map body = new HashMap();
    body.put("userId","100");
    body.put("account","admin");
    body.put("role","admin");
 //私钥,生成jwt令牌时需要使用私钥
        InputStream resourceAsStream =
                this.getClass().getClassLoader().getResourceAsStream("pri.key");
        DataInputStream dis = new DataInputStream(resourceAsStream);
        byte[] keyBytes = new byte[resourceAsStream.available()];
        dis.readFully(keyBytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = kf.generatePrivate(spec);
    
    // 生成jwt,id是个标识不给也可以唯一即可
    String jwt = Jwts.builder()
        .setHeader(header)
        .setClaims(body)
        .setId("101")
        .signWith(SignatureAlgorithm.RS256,privateKey)
        .compact();
        
        //使用jjwt提供的API解析jwt令牌
        resourceAsStream =
                this.getClass().getClassLoader().getResourceAsStream("pub.key");
        dis = new DataInputStream(resourceAsStream);
        keyBytes = new byte[resourceAsStream.available()];
        dis.readFully(keyBytes);
        X509EncodedKeySpec spec2 = new X509EncodedKeySpec(keyBytes);
        kf = KeyFactory.getInstance("RSA");
        PublicKey publicKey = kf.generatePublic(spec2);
    // 解析jwt
    Jwt result = Jwts.parser().setSigningKey(publicKey).parse(jwt);
    Header header1 = result.getHeader();
    Object obj = result.getBody();
}

pubic static Map<String,String> generator(String pubPath,String priPath) throws Exception{
    // 自定义随机密码
    String password="asdasfa";
    KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    SecureRandom secureRandom = new SecureRandom(password.getBytes());
    KeyPairGenerator.initialize(1024,secureRandom);
    KeyPair keyPair = keyPairGenerator.genKeyPair();
    byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
    byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
    FileUtil.writeBytes(publicKeyBytes,pubPath);
    FileUtil.writeBytes(privateKeyBytes,priPath);
}