1 什么是JWT

JSON Web Token(JWT)是一种开放标准(RFC7519),它定义了一种紧凑而独立的方式,用于在各方之间作为JSON对象安全地传输信息。此信息是可以验证和信任的,因为它是经过数字签名的。
来源JWT官网(https://jwt.io/introduction)
通俗的来说,jwt就是应用认证的一种解决方案,在讨论jwt之前,需要先了解的传统的session认证和token认证区别。

1.1 传统的session认证步骤:

  1. 客户端向服务端发送账号密码进行认证。
  2. 服务端在校验账号密码正确之后,将当前用户的基本信息保存到当前会话(session)中,并将sessionid返回给客户端。
  3. 客户端在拿到sessionid之后将其保存到cookie中,并在以后的每次请求中都携带该sessionid。
  4. 服务端根据客户端传过来的sessionid对当前用户进行认证,判断是否合法。
1.1.1 session认证存在的问题:
  1. 所有的session都在服务端进行保存,当用户量不断增大的时候服务器的开销也会随之增大。
  2. 在一个应用的服务器有多台的情况下,针对一个用户,就必须在每台服务器上都维护一个相同的session,或者引入Redis等中间件对session进行统一的管理。

1.2 token认证步骤:

  1. 客户端向服务端发送账号密码进行认证。
  2. 服务端在校验账号密码正确之后,生成token字符串返回给客户端。
  3. 客户端收到token之后进行保存,并在之后的每一次请求中将该token放到请求头中一并发送。
  4. 服务端在收到客户端传递的token之后,对token进行验证,判断是否合法。
  5. 使用token进行认证的话,服务端不需要保存任何会话信息或者用户信息,所有的信息都分散保存在客户端中,客户端每次请求的时候自己带上token即可。

2 JWT的构成

jwt是一个很长的字符串,中间用“.”分割成三部分,实际的jwt如下所示:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsZWUiLCJpc3MiOiJsZWUiLCJpZCI6IjEyMzQ1NiIsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE2MzQwMTU2MTgsImlhdCI6MTYzNDAwODQxOCwianRpIjoiZTY4ODAwNzUtODA5Mi00ZTM0LWEzZDctYmQ1YmQ4ZTA0YjBkIn0.MOPUmnWzbFijlM_vGESF6YdBDvSW3QtIh1qS8i_yuPc

jwt的三个部分如下:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

2.1 Header

Header部分是一个json字符串,保存的是jwt的元数据,格式如下:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg:签名的加密方式
  • typ:表示这个令牌(token)的类型(type),JWT令牌统一写为JWT
    最后将Header的json字符串进行Base64编码之后就得到了jwt的第一部分

2.2 Payload

Payload部分同样是一个json字符串,其中保存的是有效信息,这些信息主要包含三个部分:

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明
2.2.1 标准中注册的声明(JWT官方规定的字段):
  • iss:jwt签发者
  • sub:jwt所面向的用户
  • aud:接收jwt的一方
  • exp:jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf:定义在什么时间之前,该jwt都是不可用的
  • iat:jwt的签发时间
  • jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
2.2.2 公共的声明 :

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。

2.2.3 私有的声明 :

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
Payload示例:

{
  "sub":"lee",
  "iss":"lee",
  "id":"123456",
  "exp":"1634015618",
  "iat":"1634008418",
  "jti":"e6880075-8092-4e34-a3d7-bd5bd8e04b0d",
  "userName":"admin"
}

最后将Payload的json字符串进行Base64编码之后就得到了jwt的第二部分

2.3 Signature

jwt的第三部分就是签名部分,针对以下三个部分进行加密即可:

  • Header(Base64编码后的)
  • Payload(Base64编码后的)
  • secret
    将Base64编码后的Header和Payload用“.”进行连接,然后使用Header中的加密算法进行加盐secret加密。
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

最后加密出的字符串就是jwt的第三部分。

3 JWT的使用

当客户端接收到服务端发来的jwt字符串之后,将其保存到Cookie或者localStorage里面,然后每次向服务器发送请求的时候都带上JWT,将jwt放到HTTP请求的头信息Authorization中。

Authorization: Bearer <token>

3.1 服务端验证步骤

  1. 当服务端接收到客户端的token字符串之后,首先对字符串的第一部分进行解码,得到Header。
  2. 取出Header中的加密方式,使用该加密方式和secret对字符串的第一部分和第二部分进行加密,如果得到的签名和传递过来的签名一致,则认为该token合法。
  3. 对第二部分进行解码,得到需要的数据。

3.2 代码

使用jjwt实现jwt的签发和解析获取payload中的数据

  1. 引入
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>
  1. Java代码
package com.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class JwtDemo {

	//私钥 生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取
	private final static String secret = "123456789";

	/**
	 * 创建jwt
	 */
	public static String createJwtToken(){

		// Header
		Map<String, Object> map = new HashMap<>();
		map.put("alg", "HS256");
		map.put("typ", "JWT");


		//Payload
		Map<String,Object> claims = new HashMap<String,Object>();

		//自定义数据,根据业务需要添加
		claims.put("id","123456");
		claims.put("userName", "admin");

		//标准中注册的声明
		claims.put("iss", "lee");

		//生成jwt
		return Jwts.builder()
            .setHeader(map)         // 添加Header信息
            .setClaims(claims)      // 添加Payload信息
            .setId(UUID.randomUUID().toString()) // 设置jti:是JWT的唯一标识
            .setIssuedAt(new Date())       // 设置iat: jwt的签发时间
            .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置exp:jwt过期时间,3600秒=1小时
            .setSubject("Jack")    //设置sub:代表这个jwt所面向的用户
            .signWith(SignatureAlgorithm.HS256, secret)//设置签名:通过签名算法和秘钥生成签名
            .compact();
	}

	/**
	 * 从jwt中获取 Payload 信息
	 */
	private static Claims getClaimsFromJwt(String jwt) {
		Claims claims = null;
		try {
			claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(jwt).getBody();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return claims;
	}

	public static void main(String[] args) {
		String jwtToken = createJwtToken();
		System.out.println("JWT Token: "+ jwtToken);
		System.out.println("=======================================================");

		Claims claims = getClaimsFromJwt(jwtToken);
		System.out.println("claims: " + claims);
	}
}

运行结果

JWT Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKYWNrIiwiaXNzIjoibGVlIiwiaWQiOiIxMjM0NTYiLCJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNjM0MDMzMjEwLCJpYXQiOjE2MzQwMjYwMTAsImp0aSI6ImQ4ZDk3ZjMzLTIwMTctNGQzMi04ZDRlLWM0Yzc3ZjU2NDUzMyJ9.N9SRwmwmFEFGU5hgRsQqJSngmasvTGZ5WXi1t5JO3MI
=======================================================
claims: {sub=Jack, iss=lee, id=123456, userName=admin, exp=1634033210, iat=1634026010, jti=d8d97f33-2017-4d32-8d4e-c4c77f564533}

3.3 建议

  1. Payload中不要存放敏感信息。
  2. 保存好secret私钥,不要泄露。
  3. jwt的有效期需要根据应用来决定,时间不宜设置过长。
  4. 如果可以,请使用HTTPS。