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

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

前言

互联网安全一直是用户和开发者们关注的焦点。本文介绍了一种名为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认证,那我们就可以一直发请求,这是不安全的!


相关文章
|
1月前
|
JSON JavaScript 数据格式
jwt-auth插件实现了基于JWT(JSON Web Tokens)进行认证鉴权的功能。
jwt-auth插件实现了基于JWT(JSON Web Tokens)进行认证鉴权的功能。
44 1
|
3月前
|
存储 NoSQL 前端开发
jwt与redis,把生成的token放入redis中进行临时存储
jwt与redis,把生成的token放入redis中进行临时存储
77 0
|
24天前
|
XML JSON JavaScript
使用JSON和XML:数据交换格式在Java Web开发中的应用
【4月更文挑战第3天】本文比较了JSON和XML在Java Web开发中的应用。JSON是一种轻量级、易读的数据交换格式,适合快速解析和节省空间,常用于API和Web服务。XML则提供更强的灵活性和数据描述能力,适合复杂数据结构。Java有Jackson和Gson等库处理JSON,JAXB和DOM/SAX处理XML。选择格式需根据应用场景和需求。
|
1月前
|
JSON 大数据 数据格式
web后端-json递归获取key值
web后端-json递归获取key值
|
2月前
|
存储 JSON 安全
解密Web安全:Session、Cookie和Token的不解之谜
解密Web安全:Session、Cookie和Token的不解之谜
81 0
|
3月前
|
JSON 安全 网络安全
超详细的用户认证、权限、安全原理详解(认证、权限、JWT、RFC 7235、HTTPS、HSTS、PC端、服务端、移动端、第三方认证等等)
超详细的用户认证、权限、安全原理详解(认证、权限、JWT、RFC 7235、HTTPS、HSTS、PC端、服务端、移动端、第三方认证等等)
353 0
|
3月前
|
存储 JSON 算法
为什么JSON Web Token对于应用程序中的会话管理很有用
为什么JSON Web Token对于应用程序中的会话管理很有用
|
3月前
|
SQL Java 数据库连接
Spring-Security & JWT 实现 token
Spring-Security & JWT 实现 token
34 0
|
24天前
|
监控 JavaScript 前端开发
《理解 WebSocket:Java Web 开发的实时通信技术》
【4月更文挑战第4天】WebSocket是Java Web实时通信的关键技术,提供双向持久连接,实现低延迟、高效率的实时交互。适用于聊天应用、在线游戏、数据监控和即时通知。开发涉及服务器端实现、客户端连接及数据协议定义,注意安全、错误处理、性能和兼容性。随着实时应用需求增加,WebSocket在Java Web开发中的地位将更加重要。
|
1月前
|
Web App开发 前端开发 开发工具
介绍Web开发的基础知识
介绍Web开发的基础知识
29 7