解锁互联网安全的新钥匙:JWT(JSON Web Token)

目录

前言

一、JWT简介

[1. 什么是JWT?](#1. 什么是JWT?)

​编辑

[2. JWT的工作原理](#2. JWT的工作原理)

3.JWT如何工作的

[4. JWT的优势](#4. JWT的优势)

[5. 在实际应用中使用JWT](#5. 在实际应用中使用JWT)

6.传统Session和JWT认证的区别

6.1.session认证方式

6.2.JWT认证方式

[7.基于Token的身份认证 与 基于服务器的身份认证](#7.基于Token的身份认证 与 基于服务器的身份认证)

二、JWT的结构

[(1) Header](#(1) Header)

[(2) Payload](#(2) Payload)

[(3) Signature](#(3) Signature)

三、JWT的使用

1.工具类

2.JWT的生成与解析

3.token刷新并延长默认有效时间

4.测试JWT的有效时间

5.模拟JWT令牌过期

四、案例讲解

1.后端编写

2.前端编写


前言

互联网安全一直是用户和开发者们关注的焦点。本文介绍了一种名为JWT(JSON Web Token)的新型安全传输标准,探讨了其工作原理、优势,并展示了在实际应用中的强大功能。无论你是一个前端开发者、后端工程师还是安全专家,这篇博客都将为你揭开JWT的神秘面纱,并带来惊喜和启发。

一、JWT简介

1. 什么是JWT?

JWT(JSON Web Token)是一种开放的标准(RFC 7519),用于在不同实体之间安全地传输、认证和验证数据。它使用 JSON 对象作为载荷(Payload),并使用数字签名或加密算法对其进行安全性保护。JWT通常由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

官网地址 :https://jwt.io/introduction

2. JWT的工作原理

  • 头部(Header) :头部包含了描述签名算法和加密算法等元数据的信息。它通常由两部分组成:令牌类型(typ)和加密算法(alg)。例如,{"typ": "JWT", "alg": "HS256"}表示该令牌使用HS256算法进行签名。

  • 载荷(Payload):载荷是真正存储数据的部分。它包含了一些声明(Claim),用于描述数据的一些属性和相关信息,比如用户ID、角色、过期时间等。载荷可以被加密或签名,但不能包含敏感信息。

  • 签名(Signature):签名是对头部和载荷进行数字签名以保证数据的完整性和安全性。签名需要使用到密钥,确保只有持有正确密钥的实体才能验证签名,并且防止数据篡改。

上述原理图描述:

  1. 应用程序或客户端向授权服务器请求授权。
  2. 授予授权后,授权服务器将向应用程序返回访问令牌。
  3. 应用程序使用访问令牌访问受保护的资源(如 API)。

3.JWT如何工作的

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。

**无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,**通常放在Authorization header中,用Bearer schema。

header应该看起来是这样的:

Authorization: Bearer

服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。

如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。

4. JWT的优势

  • 无状态性:由于JWT自包含了用户身份和相关信息,服务器无需存储会话信息或状态,因此可以轻松实现无状态的服务端架构。

  • **可扩展性和灵活性:**JWT的载荷可以包含自定义的声明,根据实际需求灵活添加所需数据,并在验证时进行相应处理。

  • 跨平台和跨语言:JWT是基于标准的JSON格式,因此可以在不同平台和编程语言之间进行交互和解析,使得系统的集成更加容易。

  • 安全性:通过数字签名或加密,JWT可以确保数据的完整性、真实性和保密性。

5. 在实际应用中使用JWT

  • 用户认证:通过使用JWT作为身份验证机制,服务器可以验证用户的身份并可靠地提供受保护的资源。

  • 单点登录(SSO):用户在成功登录后,可以通过JWT在多个应用之间共享身份信息,无需再次登录。

  • 授权和权限管理:JWT携带了用户角色和权限等信息,在服务端可以轻松进行鉴权和权限控制。

6.传统Session和JWT认证的区别

6.1.session认证方式

http协议本身是一种无状态的协议,如果用户向服务器提供了用户名和密码来进行用户认证,下次请求时,用户还要再一次进行用户认证才行。因为根据http协议,服务器并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储─份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样应用就能识别请求来自哪个用户。

暴露的问题

  • 用户经过应用认证后,应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大;
  • 用户认证后,服务端做认证记录,如果认证的记录被保存在内存中的话,用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源。在分布式的应用上,限制了负载均衡器的能力。以此限制了应用的扩展能力;
  • session是基于cookie来进行用户识别,cookie如果被截获,用户很容易受到CSRF(跨站伪造请求攻击)攻击;
  • 在前后端分离系统中应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。

6.2.JWT认证方式

认证流程

  • 前端通过Web表单将自己的用户名和密码发送到后端的接口。该过程一般是HTTP的POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  • 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。
  • 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage(浏览器本地缓存)或sessionStorage(session缓存)上,退出登录时前端删除保存的JWT即可。
  • 前端在每次请求时将JWT放入HTTP的Header中的Authorization位。(解决XSS和XSRF问题)HEADER
  • 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确﹔检查Token是否过期;检查Token的接收方是否是自己(可选)
  • ·验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

7.基于Token的身份认证 与 基于服务器的身份认证

7.1 基于服务器的身份认证

在讨论基于Token的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:
HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证

传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。

这种基于服务器的身份认证方式存在一些问题:

  • Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
  • Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。
  • CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
  • CSRF : 用户很容易受到CSRF攻击。

7.2. JWT与Session的差异 相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

7.3. 基于Token的身份认证是如何工作的 基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。

没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

虽然这一实现可能会有所不同,但其主要流程如下:

-用户携带用户名和密码请求访问 -服务器校验用户凭据 -应用提供一个token给客户端 -客户端存储token,并且在随后的每一次请求中都带着它 -服务器校验token并返回数据

注意:

-每一次请求都需要token -Token应该放在请求header中 -我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *

**7.4. 用Token的好处 - 无状态和可扩展性:**Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。 - 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!

还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。

7.5. JWT与OAuth的区别 -OAuth2是一种授权框架 ,JWT是一种认证协议 -无论使用哪种方式切记用HTTPS来保证数据的安全性 -OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。

二、JWT的结构

在其紧凑的形式中,JWT由以点(.)分隔的三个部分组成,它们是:

  • Header
  • Payload
  • Signature

类似于xxxx.xxxx.xxxx格式,真实情况如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

并且你可以通过官网JSON Web Tokens - jwt.io解析出三部分表示的信息( 可使用 JWT.io Debugger 来解码、验证和生成 JWT ):

(1) Header

报头通常由两部分组成: Token的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256或 RSA)。

例如:

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

最终这个 JSON 将由base64进行加密(该加密是可以对称解密的),用于构成 JWT 的第一部分,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9就是base64加密后的结果。

(2) Payload

Token的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明: registered claims, public claims, and private claims。

例如:

{
  "sub": "1234567890",// 注册声明
  "name": "John Doe",// 公共声明
  "admin": true // 私有声明
}

这部分的声明也会通过base64进行加密,最终形成JWT的第二部分eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
registered claims(注册声明)

这些是一组预定义的声明,它们 不是强制性的,而是推荐的 ,以 提供一组有用的、可互操作的声明

例如:

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

注意:声明名称只有三个字符,因为 JWT 意味着是紧凑的。

Public claims(公共的声明)

使用 JWT 的人可以随意定义这些声明( 可以自己声明一些有效信息如用户的id,name等,但是不要设置一些敏感信息,如密码 )。但是为了避免冲突,应该在 JWT注册表中定义它们,或者将它们定义为包含抗冲突名称空间的 URI。

Private claims(私人声明)

这些是创建用于在同意使用它们的各方之间共享信息的习惯声明,既不是注册声明,也不是公开声明( 私人声明是提供者和消费者所共同定义的声明 )。

注意:对于已签名的Token,这些信息虽然受到保护,不会被篡改,但任何人都可以阅读。除非加密,否则不要将机密信息放在 JWT 的有效负载或头元素中。

(3) Signature

要创建Signature,您必须获取编码的标头(header)、编码的有效载荷(payload)、secret、标头中指定的算法,并对其进行签名。

例如,如果您想使用 HMAC SHA256算法,签名将按以下方式创建:

HMACSHA256(
  base64UrlEncode(header) + "." +base64UrlEncode(payload),
  secret
  )

上面的JSON将会通过HMACSHA256算法结合secret进行加盐签名(私钥加密),其中header和payload将通过base64UrlEncode()方法进行base64加密然后通过字符串拼接 "." 生成新字符串,最终生成JWT的第三部分SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

注意: secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了

三、JWT的使用

1.工具类

在实际项目中一般会把JWT相关操作封装成工具类使用

package com.ctb.ssm.jwt;

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

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

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

/**
 * JWT验证过滤器:配置顺序 CorsFilte->JwtUtilsr-->StrutsPrepareAndExecuteFilter
 *
 */
public class JwtUtils {
	/**
	 * JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认30分钟
	 */
	public static final long JWT_WEB_TTL = 30 * 60 * 1000;

	/**
	 * 将jwt令牌保存到header中的key
	 */
	public static final String JWT_HEADER_KEY = "jwt";

	// 指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了。
	private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
	private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙
	private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key

	static {
		byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
		JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
	}

	private JwtUtils() {
	}

	/**
	 * 解密jwt,获得所有声明(包括标准和私有声明)
	 * 
	 * @param jwt
	 * @return
	 * @throws Exception
	 */
	public static Claims parseJwt(String jwt) {
		Claims claims = Jwts.parser()
				.setSigningKey(JWT_KEY)
				.parseClaimsJws(jwt)
				.getBody();
		return claims;
	}

	/**
	 * 创建JWT令牌,签发时间为当前时间
	 * 
	 * @param claims
	 *            创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
	 * @param ttlMillis
	 *            JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间
	 * @return jwt令牌
	 */
	public static String createJwt(Map<String, Object> claims, 
			long ttlMillis) {
		// 生成JWT的时间,即签发时间 2021-10-30 10:02:00 -> 30 10:32:00
	
		long nowMillis = System.currentTimeMillis();

		
		//链式语法:
		// 下面就是在为payload添加各种标准声明和私有声明了
		// 这里其实就是new一个JwtBuilder,设置jwt的body
		JwtBuilder builder = Jwts.builder()
				// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
				.setClaims(claims)
				// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
				// 可以在未登陆前作为身份标识使用
				.setId(UUID.randomUUID().toString().replace("-", ""))
				// iss(Issuser)签发者,写死
				.setIssuer("ctb")
				// iat: jwt的签发时间
				.setIssuedAt(new Date(nowMillis))
				// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
				// .setSubject("{}")
				// 设置签名使用的签名算法和签名使用的秘钥
				.signWith(SIGNATURE_ALGORITHM, JWT_KEY)
				// 设置JWT的过期时间
				.setExpiration(new Date(nowMillis + ttlMillis));

		return builder.compact();
	}

	/**
	 * 复制jwt,并重新设置签发时间(为当前时间)和失效时间
	 * 
	 * @param jwt
	 *            被复制的jwt令牌
	 * @param ttlMillis
	 *            jwt的有效时间(单位毫秒),当前时间+有效时间=过期时间
	 * @return
	 */
	public static String copyJwt(String jwt, Long ttlMillis) {
		//解密JWT,获取所有的声明(私有和标准)
		//old
		Claims claims = parseJwt(jwt);

		// 生成JWT的时间,即签发时间
		long nowMillis = System.currentTimeMillis();

		// 下面就是在为payload添加各种标准声明和私有声明了
		// 这里其实就是new一个JwtBuilder,设置jwt的body
		JwtBuilder builder = Jwts.builder()
				// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
				.setClaims(claims)
				// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
				// 可以在未登陆前作为身份标识使用
				//.setId(UUID.randomUUID().toString().replace("-", ""))
				// iss(Issuser)签发者,写死
				// .setIssuer("zking")
				// iat: jwt的签发时间
				.setIssuedAt(new Date(nowMillis))
				// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
				// .setSubject("{}")
				// 设置签名使用的签名算法和签名使用的秘钥
				.signWith(SIGNATURE_ALGORITHM, JWT_KEY)
				// 设置JWT的过期时间
				.setExpiration(new Date(nowMillis + ttlMillis));
		return builder.compact();
	}
}

JWT工具类中的方法注释

  1. public static final long JWT_WEB_TTL = 30 * 60 * 1000;:定义了一个常量,表示Web应用中JWT的有效时间,单位为毫秒,默认值为30分钟。

  2. public static final String JWT_HEADER_KEY = "jwt";:定义了一个常量,表示在header中保存JWT的键名,默认为"jwt"。

  3. private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;:定义了一个静态常量,表示签名算法,这里使用的是HS256。

  4. private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";:定义了一个静态常量,表示JWT的密钥。

  5. static {...}:这是一个静态代码块,用于初始化JWT的密钥。首先将JWT的密钥从Base64格式解码为字节数组,然后使用这个字节数组创建一个SecretKey对象。

  6. public static Claims parseJwt(String jwt) {...}:这是一个静态方法,用于解析JWT,返回一个Claims对象,其中包含了JWT的所有声明。

  7. public static String createJwt(Map<String, Object> claims, long ttlMillis) {...}:这是一个静态方法,用于创建JWT令牌。首先根据传入的claims和ttlMillis计算出JWT的签发时间和过期时间,然后使用Jwts的builder模式构建JWT,并设置签名算法和密钥,最后返回生成的JWT字符串。

  8. public static String copyJwt(String jwt, Long ttlMillis) {...}:这是一个静态方法,用于复制JWT。首先调用parseJwt方法解析传入的JWT,获取其所有的声明,然后使用这些声明和传入的ttlMillis创建一个新的JWT,并返回。

2.JWT的生成与解析

  • 通过Java代码实现JWT的生成( 使用的是JJWT框架 )

先导入JJWT的依赖(JJWT是JWT的框架)

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

测试代码如下:

private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

	@Test
	public void test1() {// 生成JWT
		
		//JWT Token=Header.Payload.Signature
		//头部.载荷.签名
		//Payload=标准声明+私有声明+公有声明
		
		//定义私有声明
		Map<String, Object> claims = new HashMap<String, Object>();
		claims.put("username", "ctb");
		claims.put("age", 23);
		
	    //TTL:Time To Live
		String jwt = JwtUtils.createJwt(claims, JwtUtils.JWT_WEB_TTL);
		System.out.println(jwt);

		//获取Payload(包含标准和私有声明)
		Claims parseJwt = JwtUtils.parseJwt(jwt);
		for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
			System.out.println(entry.getKey() + "=" + entry.getValue());
		}
		Date d1 = parseJwt.getIssuedAt();
		Date d2 = parseJwt.getExpiration();
		System.out.println("令牌签发时间:" + sdf.format(d1));
		System.out.println("令牌过期时间:" + sdf.format(d2));
	}

运行结果:

通过官网进行解码:

  • 通过Java代码实现JWT的解码( 使用的是JJWT框架 )

测试代码如下:

@Test
	public void test2() {// 解析oldJwt
		//io.jsonwebtoken.ExpiredJwtException:JWT过期异常
		//io.jsonwebtoken.SignatureException:签名异常
		//eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzU1NjE3MjcsImlhdCI6MTYzNTU1OTkyNywiYWdlIjoxOCwianRpIjoiN2RlYmIzM2JiZTg3NDBmODgzNDI5Njk0ZWE4NzcyMTgiLCJ1c2VybmFtZSI6InpzcyJ9.dUR-9JUlyRdoYx-506SxXQ3gbHFCv0g5Zm8ZGzK1fzw
		String newJwt="eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjdGIiLCJleHAiOjE2OTcyMDYzNDYsImlhdCI6MTY5NzIwNDU0NiwiYWdlIjoyMywianRpIjoiYmQ2OTNiYWIxZDAxNDMwMWExMmNjOGMyNDJkNDdmOGEiLCJ1c2VybmFtZSI6ImN0YiJ9.lWtz13pyHJUYWd2OrSLE-MGYmHqzvACnAtIJOUFS1UM";
//		String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzU1NjE3MjcsImlhdCI6MTYzNTU1OTkyNywiYWdlIjoxOCwianRpIjoiN2RlYmIzM2JiZTg3NDBmODgzNDI5Njk0ZWE4NzcyMTgiLCJ1c2VybmFtZSI6InpzcyJ9.dUR-9JUlyRdoYx-506SxXQ3gbHFCv0g5Zm8ZGzK1fzw";
		Claims parseJwt = JwtUtils.parseJwt(newJwt);
		for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
			System.out.println(entry.getKey() + "=" + entry.getValue());
		}
		Date d1 = parseJwt.getIssuedAt();
		Date d2 = parseJwt.getExpiration();
		System.out.println("令牌签发时间:" + sdf.format(d1));
		System.out.println("令牌过期时间:" + sdf.format(d2));
	}

运行结果:

3.token刷新并延长默认有效时间

测试代码如下:

@Test
	public void test3() {// 复制jwt,并延时30分钟
		String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6a2luZyIsImV4cCI6MTY2MjM0Njg3MSwiaWF0IjoxNjYyMzQ1MDcxLCJhZ2UiOjE4LCJqdGkiOiI4YjllNzc3YzFlMDM0MjViYThmMDVjNTFlMTU3NDQ1MiIsInVzZXJuYW1lIjoienNzIn0.UWpJxPxwJ09PKxE2SY5ME41W1Kv3jP5bZGKK-oNUDuM";
		//String newJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDU3NTM2NTUsImlhdCI6MTYwNTc1MTg1NSwiYWdlIjoxOCwianRpIjoiYmNmN2Q1MzQ2YjE3NGU2MDk1MmIxYzQ3ZTlmMzQyZjgiLCJ1c2VybmFtZSI6InpzcyJ9.m1Qn84RxgbKCnsvrdbbAnj8l_5Jwovry8En0j4kCxhc";
		//String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjI5MDMzNjAsImlhdCI6MTU2MjkwMTU2MCwiYWdlIjoxOCwianRpIjoiZDVjMzE4Njg0MDcyNDgyZDg1MDE5ODVmMDY3OGQ4NjkiLCJ1c2VybmFtZSI6InpzcyJ9.XDDDRRq5jYq5EdEBHtPm7GcuBz4S0VhDTS1amRCdf48";
		String newJwt = JwtUtils.copyJwt(oldJwt, JwtUtils.JWT_WEB_TTL);
		System.out.println(newJwt);
		Claims parseJwt = JwtUtils.parseJwt(newJwt);
		for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
			System.out.println(entry.getKey() + "=" + entry.getValue());
		}
		Date d1 = parseJwt.getIssuedAt();
		Date d2 = parseJwt.getExpiration();
		System.out.println("令牌签发时间:" + sdf.format(d1));
		System.out.println("令牌过期时间:" + sdf.format(d2));
	}

复制并延长JWT的有效期,并输出解析后的JWT的相关信息。

运行结果:

4.测试JWT的有效时间

测试代码如下:

@Test
	public void test4() {// 测试JWT的有效时间
		Map<String, Object> claims = new HashMap<String, Object>();
		claims.put("username", "zss");
		String jwt = JwtUtils.createJwt(claims, 3 * 1000L);
		System.out.println(jwt);
		Claims parseJwt = JwtUtils.parseJwt(jwt);
		Date d1 = parseJwt.getIssuedAt();
		Date d2 = parseJwt.getExpiration();
		System.out.println("令牌签发时间:" + sdf.format(d1));
		System.out.println("令牌过期时间:" + sdf.format(d2));
	}

它创建了一个带有指定声明信息和有效期的JWT,并输出解析后的JWT的令牌签发时间和过期时间。在本例中,JWT的有效期为3秒。

运行结果:

5.模拟JWT令牌过期

测试代码如下:

@Test
	public void test5() {// 三秒后再解析上面过期时间只有三秒的令牌,因为过期则会报错io.jsonwebtoken.ExpiredJwtException
		//String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzU1NjMzODIsImlhdCI6MTYzNTU2MTU4MiwiYWdlIjoxOCwianRpIjoiN2RlYmIzM2JiZTg3NDBmODgzNDI5Njk0ZWE4NzcyMTgiLCJ1c2VybmFtZSI6InpzcyJ1.F4pZFCjWP6wlq8v_udfhOkNCpErF5QlL7DXJdzXTHqE";
		String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6a2luZyIsImV4cCI6MTY2MjM0Njg3MSwiaWF0IjoxNjYyMzQ1MDcxLCJhZ2UiOjE4LCJqdGkiOiI4YjllNzc3YzFlMDM0MjViYThmMDVjNTFlMTU3NDQ1MiIsInVzZXJuYW1lIjoienNzIn9.UWpJxPxwJ09PKxE2SY5ME41W1Kv3jP5bZGKK-oNUDuM";
		Claims parseJwt = JwtUtils.parseJwt(oldJwt);
		// 过期后解析就报错了,下面代码根本不会执行
		Date d1 = parseJwt.getIssuedAt();
		Date d2 = parseJwt.getExpiration();
		System.out.println("令牌签发时间:" + sdf.format(d1));
		System.out.println("令牌过期时间:" + sdf.format(d2));
	}

测试在令牌过期后解析该令牌会发生什么情况。它试图解析一个过期的JWT,并演示了当JWT过期时会抛出ExpiredJwtException异常。

运行结果:

四、案例讲解

1.后端编写

JWT是跨语言的,自然也涉及到跨域问题,在我们的过滤器里面加入需允许JWT的跨域请求

CorsFilter

package com.ctb.ssm.util;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 配置tomcat允许跨域访问
 * 
 * @author Administrator
 *
 */
public class CorsFilter implements Filter {
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
	}
	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
			throws IOException, ServletException {
		HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
		HttpServletRequest req = (HttpServletRequest) servletRequest;
		// Access-Control-Allow-Origin就是我们需要设置的域名
		// Access-Control-Allow-Headers跨域允许包含的头。
		// Access-Control-Allow-Methods是允许的请求方式
		httpResponse.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名
		httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
		
		//允许客户端发一个新的请求头jwt
		httpResponse.setHeader("Access-Control-Allow-Headers","responseType,Origin,X-Requested-With, Content-Type, Accept, jwt");
		//允许客户端处理一个新的响应头jwt
		httpResponse.setHeader("Access-Control-Expose-Headers", "jwt,Content-Disposition");
		
		//httpResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
		//httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
		
		// axios的ajax会发两次请求,第一次提交方式为:option,直接返回即可
		if ("OPTIONS".equals(req.getMethod())) {
			return;
		}
		filterChain.doFilter(servletRequest, servletResponse);
	}
	@Override
	public void destroy() {

	}
}

在web.xml进行配置过滤器

  <!--CrosFilter跨域过滤器-->
  <filter>
    <filter-name>corsFilter</filter-name>
    <filter-class>com.ctb.ssm.util.CorsFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>corsFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

注意:

我们在登陆后通过JWT保存我们的用户数据,首先我们在这就需要排除登录通过JWT认证

JWTFilter

package com.ctb.ssm.jwt;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import io.jsonwebtoken.Claims;

/**
 * * JWT验证过滤器,配置顺序 :CorsFilter-->JwtFilter-->struts2中央控制器
 * 
 * @author biao
 *
 */

public class JwtFilter implements Filter {

	// 排除的URL,一般为登陆的URL(请改成自己登陆的URL)
	private static String EXCLUDE = "^/user/userLogin?.*$";

	private static Pattern PATTERN = Pattern.compile(EXCLUDE);

	private boolean OFF = true;// true关闭jwt令牌验证功能

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
	}

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) request;
		HttpServletResponse resp = (HttpServletResponse) response;
		//获取当前请求路径。只有登录的请求路径不进行校验之外,其他的URL请求路径必须进行JWT令牌校验
		//http://localhost:8080/ssh2/bookAction_queryBookPager.action
		//req.getServletPath()==/bookAction_queryBookPager.action
		String path = req.getServletPath();
		if (OFF || isExcludeUrl(path)) {// 登陆直接放行
				chain.doFilter(request, response);
				return;
		}

		// 从客户端请求头中获得令牌并验证
		//token=头.载荷.签名
		String jwt = req.getHeader(JwtUtils.JWT_HEADER_KEY);
		Claims claims = this.validateJwtToken(jwt);
		//在这里请各位大哥大姐从JWT令牌中提取payload中的声明部分
		//从声明部分中获取私有声明
		//获取私有声明中的User对象 -> Modules
		Boolean flag=false;
		if (null == claims) {
			// resp.setCharacterEncoding("UTF-8");
			resp.sendError(403, "JWT令牌已过期或已失效");
			return;
		} else {
			
			//1.获取已经解析后的payload(私有声明)
			//2.从私有声明中当前用户所对应的权限集合List<String>或者List<Module>
			//3.循环权限(Module[id,url])
			// OK,放行请求 chain.doFilter(request, response);
			// NO,发送错误信息的JSON
			// ObjectMapper mapper=new ObjectMapper()
			// mapper.writeValue(response.getOutputStream(),json)
			
			String newJwt = JwtUtils.copyJwt(jwt, JwtUtils.JWT_WEB_TTL);
			resp.setHeader(JwtUtils.JWT_HEADER_KEY, newJwt);
			chain.doFilter(request, response);
		}
	}

	/**
	 * 验证jwt令牌,验证通过返回声明(包括公有和私有),返回null则表示验证失败
	 */
	private Claims validateJwtToken(String jwt) {
		Claims claims = null;
		try {
			if (null != jwt) {
				//该解析方法会验证:1)是否过期 2)签名是否成功
				claims = JwtUtils.parseJwt(jwt);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return claims;
	}

	/**
	 * 是否为排除的URL
	 * 
	 * @param path
	 * @return
	 */
	private boolean isExcludeUrl(String path) {
		Matcher matcher = PATTERN.matcher(path);
		return matcher.matches();
	}

	// public static void main(String[] args) {
	// String path = "/sys/userAction_doLogin.action?username=zs&password=123";
	// Matcher matcher = PATTERN.matcher(path);
	// boolean b = matcher.matches();
	// System.out.println(b);
	// }

}

private boolean OFF = false;我们在开发过程中可以通过这个属性来决定我们要不要使用JWT,如果我们开发过程都需要JWT的话测试是非常麻烦的。

在web.xml也需配置

<!--JwtFilter-->
  <filter>
    <filter-name>jwtFilter</filter-name>
    <filter-class>com.ctb.ssm.jwt.JwtFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>jwtFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

UserController

在controller层编写保存JWT的方法并回显给前端

/**
     * 登录
     * @param userVo
     * @param response
     * @return
     */
    @RequestMapping("/userLogin")
    @ResponseBody
    public JsonResponseBody<?> userLogin(UserVo userVo, HttpServletResponse response){
        if(userVo.getUsername().equals("admin")&&userVo.getPassword().equals("123")){
            //私有要求claim
            Map<String,Object> json=new HashMap<String,Object>();
            json.put("username", userVo.getUsername());
            //生成JWT,并设置到response响应头中
            String jwt=JwtUtils.createJwt(json, JwtUtils.JWT_WEB_TTL);
            response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
            return new JsonResponseBody<>("用户登陆成功!",true,0,null);
        }else{
            return new JsonResponseBody<>("用户名或密码错误!",false,0,null);
        }
    }

2.前端编写

在这里我们也是结合上篇博客Vuex存值取值与异步请求处理继续编写的

在store/state.js中定义jwt变量

export default{
  jwt:'',
}

在store/mutations.js编写设置jwt的方法

export default{
    //state指的是state.js文件中导出的对象
    //payload是vue文件传递过来的参数
  setJwt: (state, payload) => {
      state.jwt = payload.jwt;
    }

}

在store/getters.js编写获取jwt的方法

export default{
 
  getJwt: (state) => {
      return state.jwt
    }

}

登录过后请求主页会将开始后端响应的JWT保存到Vuex,下次发送请求的使用就会带上这个JWT,后端校验如果不是登录请求又没有JWT将不会"放行请求"

结果:

复制相同链接重新加载页面,将不会出现数据库左侧列表信息。

我们的JWT生效了,在第二次请求中没有带JWT是过不了后端请求的!若是我们没有借助JWT认证,那我们就可以一直发请求,这是不安全的!

相关推荐
迷雾漫步者6 分钟前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-33 分钟前
验证码机制
前端·后端
xlsw_35 分钟前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹1 小时前
基于java的改良版超级玛丽小游戏
java
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫2 小时前
泛型(2)
java
超爱吃士力架2 小时前
邀请逻辑
java·linux·后端
南宫生2 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论