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


    目录

    前言

    一、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 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认证,那我们就可以一直发请求,这是不安全的!

  • 相关阅读:
    【图形学】27 透明度混合
    牛客网:NC129 阶乘末尾0的数量
    Git——新建本地仓库并上传到Gitee
    10、信息打点——APP&小程序篇&抓包封包&XP框架&反编译&资产提取
    HTML && CSS
    MySQL之事务和redo日志
    ExtJS - UI组件 - MessageBox
    Servlet基本原理与常见API方法的应用
    基于图搜索的规划算法之A*家族(六):D* Lite算法
    Java语言
  • 原文地址:https://blog.csdn.net/weixin_74268571/article/details/133817786