当前位置:   article > 正文

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

jsonwebtoken secretkey

目录

前言

一、JWT简介

1. 什么是JWT?

​编辑

2. JWT的工作原理

3.JWT如何工作的

4. JWT的优势

5. 在实际应用中使用JWT

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

6.1.session认证方式

6.2.JWT认证方式

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

二、JWT的结构

(1) Header

(2) Payload

(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)。

例如:

  1. {
  2. "alg": "HS256",
  3. "typ": "JWT"
  4. }

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

(2) Payload

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

例如:

  1. {
  2. "sub": "1234567890",// 注册声明
  3. "name": "John Doe",// 公共声明
  4. "admin": true // 私有声明
  5. }

这部分的声明也会通过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算法,签名将按以下方式创建:

  1. HMACSHA256(
  2. base64UrlEncode(header) + "." +base64UrlEncode(payload),
  3. secret
  4. )

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

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

 三、JWT的使用

1.工具类

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

  1. package com.ctb.ssm.jwt;
  2. import java.util.Date;
  3. import java.util.Map;
  4. import java.util.UUID;
  5. import javax.crypto.SecretKey;
  6. import javax.crypto.spec.SecretKeySpec;
  7. import org.apache.commons.codec.binary.Base64;
  8. import io.jsonwebtoken.Claims;
  9. import io.jsonwebtoken.JwtBuilder;
  10. import io.jsonwebtoken.Jwts;
  11. import io.jsonwebtoken.SignatureAlgorithm;
  12. /**
  13. * JWT验证过滤器:配置顺序 CorsFilte->JwtUtilsr-->StrutsPrepareAndExecuteFilter
  14. *
  15. */
  16. public class JwtUtils {
  17. /**
  18. * JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认30分钟
  19. */
  20. public static final long JWT_WEB_TTL = 30 * 60 * 1000;
  21. /**
  22. * 将jwt令牌保存到header中的key
  23. */
  24. public static final String JWT_HEADER_KEY = "jwt";
  25. // 指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了。
  26. private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
  27. private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙
  28. private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key
  29. static {
  30. byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
  31. JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
  32. }
  33. private JwtUtils() {
  34. }
  35. /**
  36. * 解密jwt,获得所有声明(包括标准和私有声明)
  37. *
  38. * @param jwt
  39. * @return
  40. * @throws Exception
  41. */
  42. public static Claims parseJwt(String jwt) {
  43. Claims claims = Jwts.parser()
  44. .setSigningKey(JWT_KEY)
  45. .parseClaimsJws(jwt)
  46. .getBody();
  47. return claims;
  48. }
  49. /**
  50. * 创建JWT令牌,签发时间为当前时间
  51. *
  52. * @param claims
  53. * 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
  54. * @param ttlMillis
  55. * JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间
  56. * @return jwt令牌
  57. */
  58. public static String createJwt(Map<String, Object> claims,
  59. long ttlMillis) {
  60. // 生成JWT的时间,即签发时间 2021-10-30 10:02:00 -> 30 10:32:00
  61. long nowMillis = System.currentTimeMillis();
  62. //链式语法:
  63. // 下面就是在为payload添加各种标准声明和私有声明了
  64. // 这里其实就是new一个JwtBuilder,设置jwt的body
  65. JwtBuilder builder = Jwts.builder()
  66. // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
  67. .setClaims(claims)
  68. // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
  69. // 可以在未登陆前作为身份标识使用
  70. .setId(UUID.randomUUID().toString().replace("-", ""))
  71. // iss(Issuser)签发者,写死
  72. .setIssuer("ctb")
  73. // iat: jwt的签发时间
  74. .setIssuedAt(new Date(nowMillis))
  75. // 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
  76. // .setSubject("{}")
  77. // 设置签名使用的签名算法和签名使用的秘钥
  78. .signWith(SIGNATURE_ALGORITHM, JWT_KEY)
  79. // 设置JWT的过期时间
  80. .setExpiration(new Date(nowMillis + ttlMillis));
  81. return builder.compact();
  82. }
  83. /**
  84. * 复制jwt,并重新设置签发时间(为当前时间)和失效时间
  85. *
  86. * @param jwt
  87. * 被复制的jwt令牌
  88. * @param ttlMillis
  89. * jwt的有效时间(单位毫秒),当前时间+有效时间=过期时间
  90. * @return
  91. */
  92. public static String copyJwt(String jwt, Long ttlMillis) {
  93. //解密JWT,获取所有的声明(私有和标准)
  94. //old
  95. Claims claims = parseJwt(jwt);
  96. // 生成JWT的时间,即签发时间
  97. long nowMillis = System.currentTimeMillis();
  98. // 下面就是在为payload添加各种标准声明和私有声明了
  99. // 这里其实就是new一个JwtBuilder,设置jwt的body
  100. JwtBuilder builder = Jwts.builder()
  101. // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
  102. .setClaims(claims)
  103. // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
  104. // 可以在未登陆前作为身份标识使用
  105. //.setId(UUID.randomUUID().toString().replace("-", ""))
  106. // iss(Issuser)签发者,写死
  107. // .setIssuer("zking")
  108. // iat: jwt的签发时间
  109. .setIssuedAt(new Date(nowMillis))
  110. // 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
  111. // .setSubject("{}")
  112. // 设置签名使用的签名算法和签名使用的秘钥
  113. .signWith(SIGNATURE_ALGORITHM, JWT_KEY)
  114. // 设置JWT的过期时间
  115. .setExpiration(new Date(nowMillis + ttlMillis));
  116. return builder.compact();
  117. }
  118. }

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的框架)

  1. <dependency>
  2. <groupId>io.jsonwebtoken</groupId>
  3. <artifactId>jjwt</artifactId>
  4. <version>0.9.1</version>
  5. </dependency>

测试代码如下:

  1. private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
  2. @Test
  3. public void test1() {// 生成JWT
  4. //JWT Token=Header.Payload.Signature
  5. //头部.载荷.签名
  6. //Payload=标准声明+私有声明+公有声明
  7. //定义私有声明
  8. Map<String, Object> claims = new HashMap<String, Object>();
  9. claims.put("username", "ctb");
  10. claims.put("age", 23);
  11. //TTL:Time To Live
  12. String jwt = JwtUtils.createJwt(claims, JwtUtils.JWT_WEB_TTL);
  13. System.out.println(jwt);
  14. //获取Payload(包含标准和私有声明)
  15. Claims parseJwt = JwtUtils.parseJwt(jwt);
  16. for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {
  17. System.out.println(entry.getKey() + "=" + entry.getValue());
  18. }
  19. Date d1 = parseJwt.getIssuedAt();
  20. Date d2 = parseJwt.getExpiration();
  21. System.out.println("令牌签发时间:" + sdf.format(d1));
  22. System.out.println("令牌过期时间:" + sdf.format(d2));
  23. }

运行结果: 

通过官网进行解码: 

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

测试代码如下:

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

运行结果: 

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

测试代码如下:

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

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

运行结果:

4.测试JWT的有效时间

测试代码如下:

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

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

 运行结果:

5.模拟JWT令牌过期

测试代码如下:

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

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

运行结果:

四、案例讲解

1.后端编写

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

CorsFilter 

  1. package com.ctb.ssm.util;
  2. import java.io.IOException;
  3. import javax.servlet.Filter;
  4. import javax.servlet.FilterChain;
  5. import javax.servlet.FilterConfig;
  6. import javax.servlet.ServletException;
  7. import javax.servlet.ServletRequest;
  8. import javax.servlet.ServletResponse;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. /**
  12. * 配置tomcat允许跨域访问
  13. *
  14. * @author Administrator
  15. *
  16. */
  17. public class CorsFilter implements Filter {
  18. @Override
  19. public void init(FilterConfig filterConfig) throws ServletException {
  20. }
  21. @Override
  22. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
  23. throws IOException, ServletException {
  24. HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
  25. HttpServletRequest req = (HttpServletRequest) servletRequest;
  26. // Access-Control-Allow-Origin就是我们需要设置的域名
  27. // Access-Control-Allow-Headers跨域允许包含的头。
  28. // Access-Control-Allow-Methods是允许的请求方式
  29. httpResponse.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名
  30. httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
  31. //允许客户端发一个新的请求头jwt
  32. httpResponse.setHeader("Access-Control-Allow-Headers","responseType,Origin,X-Requested-With, Content-Type, Accept, jwt");
  33. //允许客户端处理一个新的响应头jwt
  34. httpResponse.setHeader("Access-Control-Expose-Headers", "jwt,Content-Disposition");
  35. //httpResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  36. //httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
  37. // axios的ajax会发两次请求,第一次提交方式为:option,直接返回即可
  38. if ("OPTIONS".equals(req.getMethod())) {
  39. return;
  40. }
  41. filterChain.doFilter(servletRequest, servletResponse);
  42. }
  43. @Override
  44. public void destroy() {
  45. }
  46. }

在web.xml进行配置过滤器

  1. <!--CrosFilter跨域过滤器-->
  2. <filter>
  3. <filter-name>corsFilter</filter-name>
  4. <filter-class>com.ctb.ssm.util.CorsFilter</filter-class>
  5. </filter>
  6. <filter-mapping>
  7. <filter-name>corsFilter</filter-name>
  8. <url-pattern>/*</url-pattern>
  9. </filter-mapping>

注意:

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

JWTFilter

  1. package com.ctb.ssm.jwt;
  2. import java.io.IOException;
  3. import java.util.regex.Matcher;
  4. import java.util.regex.Pattern;
  5. import javax.servlet.Filter;
  6. import javax.servlet.FilterChain;
  7. import javax.servlet.FilterConfig;
  8. import javax.servlet.ServletException;
  9. import javax.servlet.ServletRequest;
  10. import javax.servlet.ServletResponse;
  11. import javax.servlet.http.HttpServletRequest;
  12. import javax.servlet.http.HttpServletResponse;
  13. import io.jsonwebtoken.Claims;
  14. /**
  15. * * JWT验证过滤器,配置顺序 :CorsFilter-->JwtFilter-->struts2中央控制器
  16. *
  17. * @author biao
  18. *
  19. */
  20. public class JwtFilter implements Filter {
  21. // 排除的URL,一般为登陆的URL(请改成自己登陆的URL)
  22. private static String EXCLUDE = "^/user/userLogin?.*$";
  23. private static Pattern PATTERN = Pattern.compile(EXCLUDE);
  24. private boolean OFF = true;// true关闭jwt令牌验证功能
  25. @Override
  26. public void init(FilterConfig filterConfig) throws ServletException {
  27. }
  28. @Override
  29. public void destroy() {
  30. }
  31. @Override
  32. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  33. throws IOException, ServletException {
  34. HttpServletRequest req = (HttpServletRequest) request;
  35. HttpServletResponse resp = (HttpServletResponse) response;
  36. //获取当前请求路径。只有登录的请求路径不进行校验之外,其他的URL请求路径必须进行JWT令牌校验
  37. //http://localhost:8080/ssh2/bookAction_queryBookPager.action
  38. //req.getServletPath()==/bookAction_queryBookPager.action
  39. String path = req.getServletPath();
  40. if (OFF || isExcludeUrl(path)) {// 登陆直接放行
  41. chain.doFilter(request, response);
  42. return;
  43. }
  44. // 从客户端请求头中获得令牌并验证
  45. //token=头.载荷.签名
  46. String jwt = req.getHeader(JwtUtils.JWT_HEADER_KEY);
  47. Claims claims = this.validateJwtToken(jwt);
  48. //在这里请各位大哥大姐从JWT令牌中提取payload中的声明部分
  49. //从声明部分中获取私有声明
  50. //获取私有声明中的User对象 -> Modules
  51. Boolean flag=false;
  52. if (null == claims) {
  53. // resp.setCharacterEncoding("UTF-8");
  54. resp.sendError(403, "JWT令牌已过期或已失效");
  55. return;
  56. } else {
  57. //1.获取已经解析后的payload(私有声明)
  58. //2.从私有声明中当前用户所对应的权限集合List<String>或者List<Module>
  59. //3.循环权限(Module[id,url])
  60. // OK,放行请求 chain.doFilter(request, response);
  61. // NO,发送错误信息的JSON
  62. // ObjectMapper mapper=new ObjectMapper()
  63. // mapper.writeValue(response.getOutputStream(),json)
  64. String newJwt = JwtUtils.copyJwt(jwt, JwtUtils.JWT_WEB_TTL);
  65. resp.setHeader(JwtUtils.JWT_HEADER_KEY, newJwt);
  66. chain.doFilter(request, response);
  67. }
  68. }
  69. /**
  70. * 验证jwt令牌,验证通过返回声明(包括公有和私有),返回null则表示验证失败
  71. */
  72. private Claims validateJwtToken(String jwt) {
  73. Claims claims = null;
  74. try {
  75. if (null != jwt) {
  76. //该解析方法会验证:1)是否过期 2)签名是否成功
  77. claims = JwtUtils.parseJwt(jwt);
  78. }
  79. } catch (Exception e) {
  80. e.printStackTrace();
  81. }
  82. return claims;
  83. }
  84. /**
  85. * 是否为排除的URL
  86. *
  87. * @param path
  88. * @return
  89. */
  90. private boolean isExcludeUrl(String path) {
  91. Matcher matcher = PATTERN.matcher(path);
  92. return matcher.matches();
  93. }
  94. // public static void main(String[] args) {
  95. // String path = "/sys/userAction_doLogin.action?username=zs&password=123";
  96. // Matcher matcher = PATTERN.matcher(path);
  97. // boolean b = matcher.matches();
  98. // System.out.println(b);
  99. // }
  100. }

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

在web.xml也需配置

  1. <!--JwtFilter-->
  2. <filter>
  3. <filter-name>jwtFilter</filter-name>
  4. <filter-class>com.ctb.ssm.jwt.JwtFilter</filter-class>
  5. </filter>
  6. <filter-mapping>
  7. <filter-name>jwtFilter</filter-name>
  8. <url-pattern>/*</url-pattern>
  9. </filter-mapping>

UserController

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

  1. /**
  2. * 登录
  3. * @param userVo
  4. * @param response
  5. * @return
  6. */
  7. @RequestMapping("/userLogin")
  8. @ResponseBody
  9. public JsonResponseBody<?> userLogin(UserVo userVo, HttpServletResponse response){
  10. if(userVo.getUsername().equals("admin")&&userVo.getPassword().equals("123")){
  11. //私有要求claim
  12. Map<String,Object> json=new HashMap<String,Object>();
  13. json.put("username", userVo.getUsername());
  14. //生成JWT,并设置到response响应头中
  15. String jwt=JwtUtils.createJwt(json, JwtUtils.JWT_WEB_TTL);
  16. response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);
  17. return new JsonResponseBody<>("用户登陆成功!",true,0,null);
  18. }else{
  19. return new JsonResponseBody<>("用户名或密码错误!",false,0,null);
  20. }
  21. }

2.前端编写

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

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

  1. export default{
  2. jwt:'',
  3. }

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

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

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

  1. export default{
  2. getJwt: (state) => {
  3. return state.jwt
  4. }
  5. }

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

结果:

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/380315?site
推荐阅读