JWT 总结

  • Post author:
  • Post category:其他



title: JWT 总结

date: 2022-03-17 11:48:47

tags:

  • JWT

    categories:
  • 开发技术及框架

    cover: https://cover.png

    feature: false



1. 跨域身份验证



1.1 传统的 session 流程

  1. 浏览器发起请求登陆,向服务器发送用户名和密码
  2. 服务端验证身份,生成身份验证信息(用户角色、登录时间等),存储在服务端 的 session 中
  3. 服务器向用户返回 session_id,session 信息都会写入到用户的 Cookie
  4. 用户的每个后续请求都将通过在 Cookie 中取出 session_id 传给服务器
  5. 服务器收到 session_id 并对比之前保存的数据,确认用户的身份
  6. 服务器返回用户请求的内容



1.2 JWT 流程

  1. 浏览器发起请求登陆
  2. 服务端验证身份,根据算法,将用户标识符打包生成 JWT, 并且返回给浏览器
  3. 浏览器发起请求获取用户资料,把刚刚拿到的 JWT 一起发送给服务器
  4. 服务器发现数据中有 JWT,进行验证
  5. 服务器返回用户请求的内容



1.3 session 与 JWT 区别

  1. session 存储在服务端占用服务器资源,而 JWT 存储在客户端
  2. session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险,而 JWT 则通过参数传递
  3. session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用
  4. 存储在客户端的 JWT 比存储在服务端的 session 更具有扩展性



1.4 JWT 工作原理

在这里插入图片描述

在服务器身份验证之后,将生成一个 JSON 对象并将其发送回用户。之后,当用户与服务器通信时,客户在请求中发回 JSON 对象。服务器仅依赖于这个 JSON 对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。

服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展



2. 访问令牌的类型

在这里插入图片描述



3. JWT 的组成

JWTString=Base64(Header) + "." + Base64(Payload).HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

在这里插入图片描述

该对象为一个很长的字符串,字符之间通过 “.” 分隔符分为三个子串。

每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名



3.1 Header(JWT 头)

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示:

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

  • alg:

    表示签名使用的算法,默认为 HMAC SHA256(写为 HS256)

  • typ:

    表示令牌的类型,JWT 令牌统一写为 JWT。


最后,使用 Base64URL 算法将上述 JSON 对象转换为字符串保存



3.2 Payload(有效载荷)

有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据,包含三个部分:

  • 标准声明
  • 公共声明
  • 私有声明



3.2.1 标准声明字段

interface Stantard {
  iss?: string; // JWT的签发者,发行人
  sub?: string; // 主题
  aud?: string; // 接收JWT的一方,受众,用户
  exp?: number; // JWT的过期时间,这个过期时间必须要大于签发时间
  nbf?: number; // 定义在什么时间之前,该 JWT 都是不可用的
  iat?: number; // 该JWT签发的时间,发布时间
  jti?: number; //JWT的唯一身份标识,JWT ID
}



3.2.2 公共声明的字段

interface Public {
	[key: string]: any;
}

公共声明字段可以添加任意信息,但是因为可以被解密出来,所以不建议存放敏感信息。



3.2.3 私有声明的字段

interface Private {
	[key: string]: any;
}



3.2.4 示例

一个JWT 的 payload 如下:

{
	"ip": "127.0.0.1",
	"uuid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
	"iat": 1527523017
}

通过 Base64URL 算法加密生成第二部分的 payload

const payloadBuffer = Buffer.from(
	JSON.stringify({
		ip: "127.0.0.1",
		uuid: "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
		iat: 1527523017
	})
);
const payload = payloadBuffer.toString("base64");

console.log(payload);
// eyJpcCI6IjEyNy4wLjAuMSIsInV1aWQiOiJmZjEyMTJmNS1kOGQxLTQ0OTYtYmY0MS1kMmRkYTczZGUxOWEiLCJpYXQiOjE1Mjc1MjMwMTd9

默认情况下 JWT 是未加密的,因为只是采用 Base64URL 算法,拿到 JWT 字符串后可以转换回原本的 JSON 数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到 JWT 中,以防止信息泄露。JWT 只是适合在网络中传输一些非敏感的信息。



3.3 Signature(签名哈希)

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。首先,

需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开

。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)生成签名:

signature = 加密算法(header + "." + payload, 密钥);
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)


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



3.4 Base64URL 算法

JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx)。Base64 中用的三个字符是 “+”,“/” 和 “=”,由于在 URL 中有特殊含义,

因此 Base64URL 中对他们做了替换:“=” 去掉,“+” 用 “-” 替换,“/” 用 “_” 替换,这就是 Base64URL 算法。



4. JWT 安全性



4.1 Token如何做身份验证?

  1. 首先,JWT 的 Token 相当是明文,是可以解密的,任何存在 payload 的东西,都没有秘密可言,所以隐私数据不能签发 Token
  2. 而服务端,拿到 Token 后解密,即可知道用户信息,例如示例中的 uuid
  3. 有了 uuid,那么你就知道这个用户是谁,是否有权限进行下一步的操作



4.2 如何防止 Token 被串改?

  1. 此时 signature 字段就是关键了,能被解密出明文的只有 header 和 payload
  2. 假如被人串改了 payload,那么服务器可以

    通过 signature 去验证

    是否被篡改过,在

    服务端在执行一次 signature = 加密算法(header + “.” + payload, 密钥);,然后对比 signature 是否一致

    ,如果一致则说明没有被篡改。
  3. 所以服务器的密钥不能被泄漏。如果泄漏,将存在以下风险:


    客户端可以自行签发 Token、其他人可以肆意篡改 Token



4.3 问题

  1. JWT 默认不加密,但可以加密。生成原始令牌后,可以使用该令牌再次对其进行加密
  2. 当 JWT 未加密方法时,一些私密数据无法通过 JWT 传输
  3. JWT 不仅可用于认证,还可用于信息交换。善用 JWT 有助于减少服务器请求数据库的次数
  4. JWT 的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦 JWT 签发,在有效期内将会一直有效
  5. JWT 本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,

    JWT 的有效期不宜设置太长

    。对于某些重要操作,用户在使用时应该每次都进行进行身份验证
  6. 为了减少盗用和窃取,JWT 不建议使用 HTTP 协议来传输代码,而是

    使用加密的 HTTPS 协议进行传输



5. JWT 种类

JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息,JWT 的具体实现可以分为以下几种:

  • nonsecure JWT:未经过签名,不安全的 JWT
  • JWS:经过签名的 JWT
  • JWE:payload 部分经过加密的 JWT



5.1 nonsecure JWT

经过签名,不安全的 JWT。其 header 部分没有指定签名算法

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

并且也没有 Signature 部分



5.2 JWS



5.2.1 概念

JWS ,也就是 JWT Signature,其结构就是

在之前 nonsecure JWT 的基础上,在头部声明签名算法,并在最后添加上签名

。创建签名,是保证 JWT 不能被他人随意篡改。我们通常使用的 JWT 一般都是 JWS。

为了完成签名,除了用到 header 信息和 payload 信息外,还需要算法的密钥,也就是 secretKey。加密的算法一般有 2 类:

  • 对称加密:secretKey 指加密密钥,可以生成签名与验签
  • 非对称加密:secretKey 指私钥,只用来生成签名,不能用来验签(验签用的是公钥)

JWT 的密钥或者密钥对,一般统一称为JSON Web Key,也就是JWK



5.2.2 JWT 签名算法

到目前为止,JWT 的签名算法有三种:


  • HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512

  • RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)

  • ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)



5.3 JWE

JWS 是去验证数据的,而 JWE(JSON Web Encryption)是保护数据不被第三方的人看到的。通过 JWE,JWT 变得更加安全。

  • JWE 和 JWS 的公钥私钥方案不相同,JWS 中,私钥持有者加密令牌,公钥持有者验证令牌。而 JWE 中,私钥一方应该是唯一可以解密令牌的一方。
  • 在 JWE 中,公钥持有可以将新的数据放入 JWT 中,但是 JWS 中,公钥持有者只能验证数据,不能引入新的数据。因此,对于公钥/私钥的方案而言,JWS 和JWE 是互补的。



6. 使用



6.1 java-jwt


导入依赖

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.18.2</version>
</dependency>



6.1.1 对称签名

public class TestJWT {

    // Token的失效时间
    private final static long EXPIRATION_TOKEN = 60 * 1000;

    // Token的签名密钥
    private final static String SECRET_KEY = "123456";

	// 根据用户名生成 Token
    public static String generateToken(String username){
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        JWTCreator.Builder jwt = JWT.create();

        HashMap<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("type", "jwt");

        // jwt.withHeader(header)
        // JWT 的 header 部分,该 map 可以是空的,因为有默认值{"alg":HS256,"typ":"JWT"}
        String token = jwt.withHeader(new HashMap<>())
                .withClaim("userId", 7)  // Payload
                .withClaim("username", username)
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN)) // 过期时间
                .sign(algorithm);// 签名用的 secret

        System.out.println("生成的 Token:" + token);
        return token;
    }

	// 验证 Token
    public static DecodedJWT verify(String token){
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);

        Verification verification = JWT.require(algorithm);
        JWTVerifier jwtVerifier = verification.build();

        DecodedJWT decodedJWT = jwtVerifier.verify(token); // 通过密钥进行验证
        return decodedJWT;
    }

    public static void main(String[] args) {
        String token = generateToken("张三");
        DecodedJWT decodedJWT = verify(token);

        String header = decodedJWT.getHeader();
        System.out.println("JWT 头:" + header);

        Map<String, Claim> claims = decodedJWT.getClaims();
        System.out.println("有效负载:" + claims);
        Claim username = decodedJWT.getClaim("username");
        System.out.println("用户名:" + username);

        String signature = decodedJWT.getSignature();
        System.out.println("签名:" + signature);

        Date expiresAt = decodedJWT.getExpiresAt();
        System.out.println("过期时间" + expiresAt);
    }
}

在这里插入图片描述



6.1.2 非对称签名

public class TestJWT {

    // Token的失效时间
    private final static long EXPIRATION_TOKEN = 60 * 1000;

    // 使用 Hutool 构建 RSA,当使用无参构造时,将自动生成随机的公钥私钥密钥对
    private final static RSA rsa = new RSA();
    // private static final String RSA_PRIVATE_KEY = "..."; 私钥
    // private static final String RSA_PUBLIC_KEY = "...";  公钥

    // 生成 Token
    public static String generateToken(String username){
    	// 获取 RSA 私钥
        // RSA rsa = new RSA(RSA_PRIVATE_KEY, null);
        // RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();

        String token = JWT.create().withHeader(new HashMap<>())
                .withClaim("username", username)
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
                .sign(Algorithm.RSA256(null, rsaPrivateKey)); // 签名时传入私钥
        return token;
    }

    // 验证 Token
    public static DecodedJWT verify(String token){
    	// 获取 RSA 公钥
    	// RSA rsa = new RSA(null, RSA_PUBLIC_KEY);
   		// RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
        RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
      
        // 验签时传入公钥
        JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA256(rsaPublicKey, null)).build();

        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        return decodedJWT;
    }

    public static void main(String[] args) {
        String token = generateToken("张三");
        DecodedJWT decodedJWT = verify(token);

        String header = decodedJWT.getHeader();
        System.out.println("JWT 头:" + header);

        Map<String, Claim> claims = decodedJWT.getClaims();
        System.out.println("有效负载:" + claims);
        Claim username = decodedJWT.getClaim("username");
        System.out.println("用户名:" + username);

        String signature = decodedJWT.getSignature();
        System.out.println("签名:" + signature);

        Date expiresAt = decodedJWT.getExpiresAt();
        System.out.println("过期时间" + expiresAt);
    }
}

在这里插入图片描述



6.2 jjwt

导入依赖

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



6.2.1 对称签名

public class TestJWT {

    // Token的失效时间
    private final static long EXPIRATION_TOKEN = 60 * 1000;

    // Token的密钥
    private final static String SECRET_KEY = "123456";

    // 根据用户名生成 Token
    public static String generateToken(String username){
        JwtBuilder jwtBuilder = Jwts.builder();

        // jwtBuilder.setHeader(new HashMap<>());
        String token = jwtBuilder.setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("user")
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
		System.out.println("生成的 Token:" + token);
        return token;
    }

    // 验证 Token,从http请求头中获取 Token 字符串
    public static Jws<Claims> verify(String token){
        JwtParser jwtParser = Jwts.parser();
        Jws<Claims> claimsJws = jwtParser.setSigningKey(SECRET_KEY).parseClaimsJws(token);
        return claimsJws;
    }
    public static Jws<Claims> verify(HttpServletRequest request){
        String token = request.getHeader("token");
        JwtParser jwtParser = Jwts.parser();
        Jws<Claims> claimsJws = jwtParser.setSigningKey(SECRET_KEY).parseClaimsJws(token);
        return claimsJws;
    }

    public static void main(String[] args) {
        String token = generateToken("张三");
        Jws<Claims> claimsJws = verify(token);

        JwsHeader header = claimsJws.getHeader();
        System.out.println("Header:" + header);

        Claims body = claimsJws.getBody();
        System.out.println("PayLoad:" + body);
        String subject = body.getSubject();
        System.out.println("主题:" + subject);

        String signature = claimsJws.getSignature();
        System.out.println("签名:" + signature);
    }
}

在这里插入图片描述



6.2.2 jjwt 的 0.10版本以后

pom依赖要引入多个

<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>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

在 jjwt 0.10 版本之前,没有强制要求,secretKey 长度不满足要求时也可以签名成功。但是 0.10 版本后强制要求 secretKey 满足规范中的长度要求,否则生成 jws 时会抛出异常

  • HS256:要求至少 256 bits (32 bytes)
  • HS384:要求至少384 bits (48 bytes)
  • HS512:要求至少512 bits (64 bytes)
  • RS256 and PS256:至少2048 bits
  • RS384 and PS384:至少3072 bits
  • RS512 and PS512:至少4096 bits
  • ES256:至少256 bits (32 bytes)
  • ES384:至少384 bits (48 bytes)
  • ES512:至少512 bits (64 bytes)

之前的签名和验签方法都是传入密钥的字符串,已经过时。最新的方法需要传入 Key 对象

public static String generateToken(String username){
	String token = Jwts.builder()
		.setHeaderParam("alg", "HS256")
		.setSubject("user")
		.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
		.claim("username", username)
		// 传入Key对象
		.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
		.compact();
	return token;
}

// 验证 Token
public static Jws<Claims> verify(String token) {
	// 传入Key对象
	Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(token);
	return claimsJws;
}



6.2.3 非对称签名

public class TestJWT {

    // Token的失效时间
    private final static long EXPIRATION_TOKEN = 60 * 1000;

    // 使用 Hutool 构建 RSA
    private final static RSA rsa = new RSA();

    // 根据用户名生成 Token
    public static String generateToken(String username){
        // 获取 RSA 私钥
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();

        // jwtBuilder.setHeader(new HashMap<>());
        String token = Jwts.builder().setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("user")
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TOKEN))
                .claim("username", username)
                .signWith(rsaPrivateKey, SignatureAlgorithm.RS256)
                .compact();
        System.out.println("生成的 Token:" + token);
        return token;
    }

    // 验证 Token
    public static Jws<Claims> verify(String token){
        RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
        Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(rsaPublicKey).build().parseClaimsJws(token);
        return claimsJws;
    }

    public static void main(String[] args) {
        String token = generateToken("张三");
        Jws<Claims> claimsJws = verify(token);

        JwsHeader header = claimsJws.getHeader();
        System.out.println("Header:" + header);

        Claims body = claimsJws.getBody();
        System.out.println("PayLoad:" + body);
        String subject = body.getSubject();
        System.out.println("主题:" + subject);

        String signature = claimsJws.getSignature();
        System.out.println("签名:" + signature);
    }
}



7. 实际应用

  1. 在登录验证通过后,给用户生成一个对应的随机 Token (注意这个 Token 不是指 JWT,可以用 UUID 等算法生成),然后将这个Token 作为 Key 的一部分,用户信息作为 Value 存入 Redis,并设置过期时间,这个过期时间就是登录失效的时间
  2. 第1步中生成的随机 Token 作为 JWT 的 payload 生成 JWT 字符串返回给前端
  3. 前端之后每次请求都在

    请求头中的 Authorization 字段

    中携带 JWT 字符串
  4. 后端定义一个拦截器,每次收到前端请求时,都先从请求头中的 Authorization 字段中取出 JWT 字符串并进行验证,验证通过后解析出 payload 中的随机 Token,然后再用这个随机 Token 得到 Key,从 Redis 中获取用户信息,如果能获取到就说明用户已经登录
public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String JWT = request.getHeader("Authorization");
        try {
            // 1.校验JWT字符串
            DecodedJWT decodedJWT = JWTUtils.decode(JWT);
            // 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
            ...
            return true;
        }catch (SignatureVerificationException e){
            System.out.println("无效签名");
            e.printStackTrace();
        }catch (TokenExpiredException e){
            System.out.println("token已经过期");
            e.printStackTrace();
        }catch (AlgorithmMismatchException e){
            System.out.println("算法不一致");
            e.printStackTrace();
        }catch (Exception e){
            System.out.println("token无效");
            e.printStackTrace();
        }
        return false;
    }
}



版权声明:本文为ACE_U_005A原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。