Skip to content

🤖 Tlias 智能学习辅助系统_5


16.2.3 JWT 令牌


前面我们介绍了基于令牌技术来实现会话追踪。这里所提到的令牌就是用户身份的标识,

其本质就是一个字符串。令牌的形式有很多,我们使用的是功能强大的 JWT令牌


16.2.3.1 介绍

  • JWT全称 JSON Web Token (官网:https://jwt.io/ ),定义了一种简洁的、自包含的格式,

    用于在通信双方以 json 数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

    • 简洁:是指 jwt 就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

    • 自包含:指的是 jwt 令牌看似是一个随机的字符串,但是我们是可以根据自身的需求在 jwt 令牌中

      存储自定义的数据内容。如:可以直接在 jwt 令牌中存储用户的相关信息。

    • 简单来讲,jwt 就是将原始的 json 数据格式进行了安全的封装,

      这样就可以直接基于 jwt 在通信双方安全的进行信息传输了。


  • JWT 的组成: ( JWT 令牌由三个部分组成,三个部分之间使用英文的点来分割)
    • 第一部分:Header (头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

    • 第二部分:Payload (有效载荷),携带一些自定义信息、默认信息等。

      例如:{"id":"1","username":"Tom"}

    • 第三部分:Signature (签名),防止Token 被篡改、确保安全性。将 headerpayload

      并加入指定秘钥,通过指定签名算法计算而来。


📌 注意:

签名的目的就是为了防 jwt 令牌被篡改,而正是因为 jwt 令牌最后一个部分数字签名的存在,

所以整个jwt 令牌是非常安全可靠的。一旦 jwt 令牌当中任何一个部分、任何一个字符被篡改了,

整个令牌在校验的时候都会失败,所以它是非常安全可靠的。


f7b78aa0-ed73-4c17-8c6e-eb5e7ba3c1ae
  • JWT 是如何将原始的 JSON 格式数据,转变为字符串的呢?
    • 其实在生成 JWT 令牌时,会对 JSON 格式的数据进行一次编码:进行 base64 编码

    • Base64:是一种基于 64 个可打印的字符来表示二进制数据的编码方式。

      既然能编码,那也就意味着也能解码。所使用的 64 个字符分别是

      A到Z、a到z、 0-9,一个加号,一个斜杠,加起来就是 64 个字符。任何数据经过 base64 编码之后,

      最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号

    • 需要注意的是 Base64 是编码方式,而不是加密方式。


16.2.3.2 生成和校验

简单介绍了 JWT 令牌以及 JWT 令牌的组成之后,

接下来我们就来学习基于 Java 代码如何生成和校验 JWT 令牌。


  • 1). 首先我们先来实现 JWT 令牌的生成。要想使用 JWT 令牌,需要先引入 JWT 的依赖:
xml
<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

记得刷新一下 maven ...


  • 创建一个测试类尝试生成和解析 JWT
    • 路径:src/test/java/com/itheima/JwtTest.java
java
package com.itheima;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @ author : irai
 * @ create : 2025-09-19 12:13
 */


public class JwtTest {

    //生成JWT令牌 - Jwts.buider()
    @Test
    public void  testGenerateJwt(){

        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("id",1);
        dataMap.put("username","irai");

        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "aXJhaQ==") //指定加密算法,秘钥
                .addClaims(dataMap) //添加自定义信息
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置过期时间
                .compact(); //生成令牌

        System.out.println(jwt);
    }

    //解析JWT令牌 - Jwts.parser()
    @Test
    public void testParseJWT(){
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJpcmFpIiwiZXhwIjoxNzU4MjU5NTM5fQ.iUVeocV4sd3IknqjWz9erJjATIKQnL8mDywglJByEdY";
        Claims claims = Jwts.parser()
                .setSigningKey("aXJhaQ==") //指定生成时的秘钥
                .parseClaimsJws(token) // 传入要解析的令牌
                .getBody();// 获取自定义信息
        System.out.println(claims);
    }

}

  • 生成测试:
微信截图_20250919124041
  • 下面我们做一个测试:把令牌 header 中的数字 9 变为 8 ,运行测试方法后发现报错:
2e6361ea-56be-4cc7-b57a-900bd411ad19
  • 结论:篡改令牌中的任何一个字符,在对令牌进行解析时都会报错,所以 JWT 令牌是非常安全可靠的。


  • 解析测试:
微信截图_20250919124126
  • 继续测试:修改生成令牌的时指定的过期时间,修改为1分钟。

  • 等待1分钟之后运行测试方法发现也报错了,说明:JWT令牌过期后,令牌就失效了,解析的为非法令牌。


📌 通过以上测试,我们在使用 JWT 令牌时需要注意:

  • JWT 校验时使用的签名秘钥,必须和生成 JWT 令牌时使用的秘钥是配套的。
  • 如果 JWT 令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。

16.2.3.3 登录时下发令牌

  • 然后我们得学会使用 AI ,我们可以把测试代码给 AI 让它帮我们写一个工具类 JwtUtils

📌 AI 提示词:

txt
请帮我基于如下单元测试方法,改造成一个JWT令牌操作的工具类,类名:JwtUtils,具体要求如下:
1. 工具类中有两个方法,一个方法生成令牌,另一个是解析令牌
2. 生成令牌时使用的秘钥,和测试类中的一致即可
3. 令牌的过期时间设置为 12 小时

原始的测试类的代码如下:
package com.itheima;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @ author : irai
 * @ create : 2025-09-19 12:13
 */


public class JwtTest {

    //生成JWT令牌 - Jwts.buider()
    @Test
    public void  testGenerateJwt(){

        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("id",1);
        dataMap.put("username","irai");

        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "aXJhaQ==") //指定加密算法,秘钥
                .addClaims(dataMap) //添加自定义信息
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置过期时间
                .compact(); //生成令牌

        System.out.println(jwt);
    }

    //解析JWT令牌 - Jwts.parser()
    @Test
    public void testParseJWT(){
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJpcmFpIiwiZXhwIjoxNzU4MjU5NTM5fQ.iUVeocV4sd3IknqjWz9erJjATIKQnL8mDywglJByEdY";
        Claims claims = Jwts.parser()
                .setSigningKey("aXJhaQ==") //指定生成时的秘钥
                .parseClaimsJws(token) // 传入要解析的令牌
                .getBody();// 获取自定义信息
        System.out.println(claims);
    }

}

  • 1). 生成工具类如下, 放到路径 com.itheima.utils 下:
java
package com.itheima.utils;

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

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

/**
 * JWT 工具类
 */
public class JwtUtils {

    // 秘钥(与测试类保持一致)
    private static final String SECRET_KEY = "aXJhaQ==";

    // 过期时间:12 小时
    private static final long EXPIRATION = 12 * 60 * 60 * 1000; // 单位毫秒

    /**
     * 生成 JWT 令牌
     *
     * @param claims 要嵌入的自定义数据(如用户ID、用户名等)
     * @return JWT 令牌字符串
     */
    public static String generateJwt(Map<String, Object> claims) {
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法和密钥
                .addClaims(claims) // 添加自定义信息
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 过期时间
                .compact(); // 构建并返回令牌
    }

    /**
     * 解析 JWT 令牌
     *
     * @param jwt JWT 令牌字符串
     * @return Claims 包含解析出的所有声明信息
     */
    public static Claims parseJWT(String jwt) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY) // 使用相同的密钥解析
                .parseClaimsJws(jwt) // 解析并验证签名
                .getBody(); // 返回负载内容(Claims)
    }
}

  • 完善 EmpServiceImpl中的 login 方法逻辑, 登录成功,生成 JWT 令牌并返回
java
@Override
public LoginInfo login(Emp emp) {
    Emp empLogin = empMapper.getUsernameAndPassword(emp);
    if(empLogin != null){
        //1. 生成JWT令牌
        Map<String,Object> dataMap = new HashMap<>();
        dataMap.put("id", empLogin.getId());
        dataMap.put("username", empLogin.getUsername());
        
        String jwt = JwtUtils.generateJwt(dataMap);
        LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);
        return loginInfo;
    }
    return null;
}

  • 重启服务,打开 Apifox 测试登录接口:
微信截图_20250919125529
  • 打开浏览器完成前后端联调操作:利用开发者工具,抓取一下网络请求
微信截图_20250919125735

登录请求完成后,可以看到 JWT 令牌已经响应给了前端,此时前端就会将 JWT 令牌存储在浏览器本地。


  • 服务器响应的 JWT 令牌存储在本地浏览器哪里了呢?
    • 在当前案例中,JWT令牌存储在浏览器的本地存储空间 localstorage中了。 localstorage 是浏览器的本地存储,在移动端也是支持的。
微信截图_20250919125902
  • 我们在发起一个查询班级数据的请求,此时我们可以看到在请求头中包含一个 token ( JWT 令牌),

    后续的每一次请求当中,都会将这个令牌携带到服务端。

微信截图_20250919130118

16.2.4 过滤器 Filter


刚才通过浏览器的开发者工具,我们可以看到在后续的请求当中,都会在请求头中携带JWT令牌到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的 JWT 令牌。

那怎么样来统一拦截到所有的请求校验令牌的有效性呢?这里我们会学习两种解决方案:

  1. Filter 过滤器
  2. Interceptor 拦截器

我们首先来学习过滤器 Filter


16.2.4.1 Filter 快速入门

  • 什么是 Filter
    • Filter 表示过滤器,是 JavaWeb 三大组件 (ServletFilterListener) 之一。

    • 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能

      • 使用了过滤器之后,要想访问 web 服务器上的资源,必须先经过滤器,

        过滤器处理完毕之后,才可以访问对应的资源。

    • 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

8eb0c900-bce1-45f0-9b07-696d766449af
  • 下面我们通过 Filter 快速入门程序掌握过滤器的基本使用操作:
    • 第1 步,定义过滤器 :定义一个类,实现 Filter 接口,并重写其所有方法。

    • 第2 步,配置过滤器:Filter 类上加 @WebFilter 注解,配置拦截资源的路径。

      引导类上加 @ServletComponentScan 开启 Servlet 组件支持。


  • 1). 定义过滤器
    • 路径:``
java
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init ...");
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求...");
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println("destroy ... ");
    }
}

注意实现类不要选错!!

微信截图_20250921175010

📌 说明:

  • init 方法:过滤器的初始化方法。在 web 服务器启动的时候会自动的创建 Filter 过滤器对象,

    在创建过滤器对象的时候会自动调用 init 初始化方法,这个方法只会被调用一次。

  • doFilter 方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,

    每拦截到一次请求就会调用一次 doFilter() 方法。

  • destroy 方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,

    而这个销毁方法也只会被调用一次。


  • 2). 配置过滤器

在定义完 Filter 之后,Filter 其实并不会生效,还需要完成 Filter的配置,Filter 的配置非常简单,只需要在 Filter

类上添加一个注解:@WebFilter,并指定属性 urlPatterns,通过这个属性指定过滤器要拦截哪些请求

java
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init ...");
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求...");
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println("destroy ... ");
    }
}

当我们在 Filter 类上面加了 @WebFilter 注解之后,接下来我们还需要在启动类上面加上一个注解

@ServletComponentScan,通过这个@ServletComponentScan 注解来开启 SpringBoot 项目

对于 Servlet 组件的支持。

java
@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {
    public static void main(String[] args) {
        SpringApplication.run(TliasManagementApplication.class, args);
    }
}

重新启动服务,打开 Apifox,执行登录的请求,可以看到控制台输出了过滤器中的内容:

e32d4c4d-013b-45fc-8e73-a1afa23ad8cc
  • 不断发送登录请求,都拦截到了,但是没有数据响应回 Apifox

📌 注意:

在过滤器 Filter 中,如果不执行放行操作,将无法访问后面的资源。

放行操作:chain.doFilter(request, response);


  • 增加放行的操作
java
@Slf4j
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径 ( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("init ...");
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        log.info("拦截到了请求...");
        //放行
        chain.doFilter(servletRequest,servletResponse);
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        log.info("destroy ... ");
    }
}

  • 这时候既拦截到了,又放行了
微信截图_20250921180032
  • 也返回了数据
微信截图_20250921180046
16.2.4.2 登录校验过滤器

16.2.4.2.1 分析

前面简单介绍了 Filter 使用 ,接下来最后一步,我们需要

使用过滤器 Filter 来完成案例当中的登录校验功能。

f7b60643-4897-41d2-86e7-51232cac3955

我们先来回顾下前面分析过的登录校验的基本流程:

  • 要进入到后台管理系统,我们必须先完成登录操作,此时就需要访问登录接口 login

  • 登录成功之后,我们会在服务端生成一个 JWT令牌,并且把 JWT 令牌返回给前端,

    前端会将 JWT 令牌存储下来 , 在浏览器的 localstorage 中。

  • 在后续的每一次请求当中,都会将 JWT 令牌携带到服务端,请求到达服务端之后,

    要想去访问对应的业务功能,此时我们必须先要校验令牌的有效性。

  • 对于校验令牌的这一块操作,我们使用登录校验的过滤器,在过滤器当中来校验令牌的有效性。

    如果令牌是无效的,就响应一个错误的信息,也不会再去放行访问对应的资源了。

    如果令牌存在,并且它是有效的,此时就会放行去访问对应的 web 资源,执行相应的业务操作。


📌 思考:

  • 所有的请求,拦截到了之后,都需要校验令牌吗 ?

    • 答案:登录请求例外
  • 拦截到请求后,什么情况下才可以放行,执行业务操作 ?

    • 答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果

16.2.4.2.2 具体流程

b870918e-24d5-4d15-bc92-ba48e9afbd59

基于上面的业务流程,我们分析出具体的操作步骤:

  1. 获取请求 url
  2. 判断请求 url 中是否包含 login,如果包含,说明是登录操作,放行
  3. 获取请求头中的令牌(token
  4. 判断令牌是否存在,如果不存在,响应 401
  5. 解析 token,如果解析失败,响应 401
  6. 放行

16.2.4.2.3 代码实现

  • com.itheima.filter 包下创建TokenFilter,具体代码如下:
java
package com.itheima.filter;

import com.itheima.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        //先转换成 Http...
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        //1. 获取到请求路径
        String requestURI = request.getRequestURI(); // /login

        //2. 判断是否是登录请求,如果路径中包含 /login ,说明是登录操作-->放行
        if(requestURI.contains("login")){
            //是登录请求-->放行
            log.info("登录操作,放行");
            chain.doFilter(request,response);
            return;
        }

        //3. 获取请求头中的token
        String token = request.getHeader("token");

        //4.判断token是否存在,如果不存在,说明用户没有登录,返回错误信息(响应401状态码)
        if(token == null || token.isEmpty()){
            log.info("令牌为空,返回401");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        //5. 如果token存在,校验令牌,如果校验失败-->返回错误信息(响应401状态码)
        try {
            JwtUtils.parseJWT(token);
        } catch (Exception e) {
            log.info("令牌非法,返回401");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        //6.校验通过,放行
        log.info("令牌合法,放行");
        chain.doFilter(request,response);

    }
}

登录校验的过滤器我们编写完成了,接下来我们就可以重新启动服务来做一个测试:

❗ 注意: 记得把 DemoFilter@WebFilter(urlPatterns = "/*") 注解给注释掉


  • 测试1:未登录是否可以访问部门管理页面

首先关闭浏览器,重新打开浏览器,在地址栏中输入:http://localhost:90

由于用户没有登录,登录校验过滤器返回错误信息,前端页面根据返回的错误信息结果,自动跳转到登录页面了

1fe6efed-4a32-4298-b4cc-e57682393fdd
  • 测试2:先进行登录操作,再访问部门管理页面

登录校验成功之后,可以正常访问相关业务操作页面

dbf08882-590c-44ae-94e0-07eb04a98326
16.2.4.3 Filter 详解

  • Filter 过滤器的快速入门程序我们已经完成了,接下来我们就要详细的介绍一下过滤器 Filter

    在使用中的一些细节。主要介绍以下3个方面的细节:

    • 过滤器的执行流程

    • 过滤器的拦截路径配置

    • 过滤器链


16.2.4.3.1 执行流程

  • 首先我们先来看下过滤器的执行流程:
d56252cb-e224-4a74-8761-b8f61737d89f
  • 过滤器当中我们拦截到了请求之后,如果希望继续访问后面的 web 资源,就要执行放行操作,

    放行就是调用 FilterChain 对象当中的 doFilter() 方法,在调用 doFilter() 这个方法之前所编写的

    代码属于放行之前的逻辑。在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后

    如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在 doFilter() 这行代码之后。


  • 测试代码:
java
@WebFilter(urlPatterns = "/*") 
public class DemoFilter implements Filter {
    
    @Override //初始化方法, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法执行了");
    }
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        
        System.out.println("DemoFilter   放行前逻辑.....");

        //放行请求
        filterChain.doFilter(servletRequest,servletResponse);

        System.out.println("DemoFilter   放行后逻辑.....");
        
    }

    @Override //销毁方法, 只调用一次
    public void destroy() {
        System.out.println("destroy 销毁方法执行了");
    }
}

  • 启动之后运行测试:
微信截图_20250928152316
16.2.4.3.2 拦截路径

  • 执行流程我们搞清楚之后,接下来再来介绍一下过滤器的拦截路径,

    Filter 可以根据需求,配置不同的拦截资源路径:

拦截路径urlPatterns含义
拦截具体路径/login只有访问 /login 路径时,才会被拦截
目录拦截/emps/*访问 /emps 下的所有资源,都会被拦截
拦截所有/*访问所有资源,都会被拦截

  • 如要是拦截具体路径(/login) 就为以下代码:
java
@WebFilter(urlPatterns = "/login")  //拦截/login具体路径
public class DemoFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("DemoFilter   放行前逻辑.....");
        //放行请求
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("DemoFilter   放行后逻辑.....");
    }
}

16.2.4.3.3 过滤器链

  • 最后我们在来介绍下过滤器链,什么是过滤器链呢?所谓过滤器链指的是在一个 web 应用程序当中,

    可以配置多个过滤器,多个过滤器就形成了一个过滤器链。

78e00f46-8134-4648-aae2-74c25b21facb
  • 比如:在我们 web 服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。

    而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个 Filter,

    放行之后再来执行第二个 Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的 web 资源。

    访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,

    还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。

    先要执行过滤器2放行之后的逻辑,再来执行过滤器1放行之后的逻辑,最后在给浏览器响应数据。


📌 说明:

过滤器链上过滤器的执行顺序:注解配置的 Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:

  • AbcFilter
  • DemoFilter

这两个过滤器来说,AbcFilter 会先执行,DemoFilter 会后执行。


16.2.5 拦截器 Interceptor


16.2.5.1 快速入门

什么是拦截器?

  • 是一种动态拦截方法调用的机制,类似于过滤器。
  • 拦截器是 Spring 框架中提供的,用来动态拦截控制器方法的执行。
  • 拦截器的作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
6ff60b78-7ccf-41f6-88ae-af3f5eb60ca8

在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,

将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带 JWT 令牌且是合法令牌),

就可以直接放行,去访问 spring 当中的资源。如果校验时发现并没有登录或是非法令牌,

就可以直接给前端响应未登录的错误信息。


下面我们通过快速入门程序,来学习下拦截器的基本使用。拦截器的使用步骤和过滤器类似,也分为两步:

  • 定义拦截器

  • 注册配置拦截器


  • 1). 自定义拦截器
  • 实现 HandlerInterceptor 接口,并重写其所有方法
java
//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行。 返回true:放行    返回false:不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle .... ");
        
        return true; //true表示放行
    }

    //目标资源方法执行后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle ... ");
    }

    //视图渲染完毕后执行,最后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion .... ");
    }
}
  • 注意:
    • preHandle 方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
    • postHandle 方法:目标资源方法执行后执行
    • afterCompletion 方法:视图渲染完毕后执行,最后执行

  • 2). 注册配置拦截器

com.itheima下创建一个包 config,然后创建一个配置类 WebConfig

实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法

java
@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //自定义的拦截器对象
    @Autowired
    private DemoInterceptor demoInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
    }
}

  • 重新启动 SpringBoot 服务,打开 Apifox 测试:
3ed79f47-8acb-4137-81a9-072d56837241

可以看到控制台输出的日志:

b5647d9a-429e-421e-9f14-58f2265df58a

接下来我们再来做一个测试:将拦截器中返回值改为 false

使用 Apifox,再次点击 send 发送请求后,没有响应数据,说明请求被拦截了没有放行

97df0eff-c6bf-4151-b083-e4cb25ad0e0b
16.2.5.2 登录校验

  • 登录校验的业务逻辑以及操作步骤我们前面已经分析过了,和登录校验 Filter 过滤器当中的逻辑是

    完全一致的。现在我们只需要把这个技术方案由原来的过滤器换成拦截器 interceptor 就可以了。


  • 1). TokenInterceptor

com.itheima.interceptor 包下创建 TokenInterceptor

java
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取请求url。
        String url = request.getRequestURL().toString();

        //2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
        if(url.contains("login")){ //登录请求
            log.info("登录请求 , 直接放行");
            return true;
        }

        //3. 获取请求头中的令牌(token)。
        String jwt = request.getHeader("token");

        //4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
        if(!StringUtils.hasLength(jwt)){ //jwt为空
            log.info("获取到jwt令牌为空, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }
        
        //5. 解析token,如果解析失败,返回错误结果(未登录)。
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析令牌失败, 返回错误结果");
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            return false;
        }
        
        //6. 放行。
        log.info("令牌合法, 放行");
        return true;
    }
}

  • 2). 配置拦截器
java
@Configuration  
public class WebConfig implements WebMvcConfigurer {
    //拦截器对象
    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //注册自定义拦截器对象
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}

登录校验的拦截器编写完成后,接下来我们就可以重新启动服务来做一个测试: (关闭登录校验Filter过滤器


  • 测试1:未登录是否可以访问部门管理页面

首先关闭浏览器,重新打开浏览器,在地址栏中输入:http://localhost:90

由于用户没有登录,校验机制返回错误信息,前端页面根据返回的错误信息结果,自动跳转到登录页面了

549c3470-4877-4cfe-a654-b0d4435e8960
  • 测试2:先进行登录操作,再访问部门管理页面

登录校验成功之后,可以正常访问相关业务操作页面

1817ea2f-fd71-42bd-9b2e-6f0e1c193881
  • 到此我们也就验证了所开发的登录校验的拦截器也是没问题的。

    登录校验的过滤器和拦截器,我们只需要使用其中的一种就可以了。


16.2.5.3 Interceptor 详解

16.2.5.3.1 拦截路径

  • 首先我们先来看拦截器的拦截路径的配置,在注册配置拦截器的时候,我们要指定拦截器的拦截路径,

    通过 addPathPatterns("要拦截路径") 方法,就可以指定要拦截哪些资源。

  • 在入门程序中我们配置的是 /**,表示拦截所有资源,而在配置拦截器时,

    不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,

    只需要调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

java
@Configuration  
public class WebConfig implements WebMvcConfigurer {

    //拦截器对象
    @Autowired
    private DemoInterceptor demoInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器对象
        registry.addInterceptor(demoInterceptor)
                .addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
                .excludePathPatterns("/login");//设置不拦截的请求路径
    }
}

  • 在拦截器中除了可以设置 /** 拦截所有资源外,还有一些常见拦截路径设置:
拦截路径含义举例
/*一级路径能匹配/depts, /emps, /login, 不能匹配 /depts/1
/**任意级路径能匹配/depts, /depts/1, /depts/1/2
/depts/*/depts 下的一级路径能匹配 /depts/1, 不能匹配 /depts/1/2, /depts
/depts/**/depts 下的任意级路径能匹配 /depts, /depts/1, /depts/1/2, 不能匹配 /emps/1

16.2.5.3.2 执行流程

  • 介绍完拦截路径的配置之后,接下来我们再来介绍拦截器的执行流程。

    通过执行流程,大家就能够清晰的知道过滤器与拦截器的执行时机。

af256dbd-1c14-4f43-9588-3d0060add47f

📌 区别​:

  • 执行时机不同:请求先到达的是 Web服务器 , 那么会先被过滤器拦截到,然后才到 Spring 里面的 Interceptor 拦截到

  • 接口规范不同:过滤器要实现 Filter 接口,而拦截器需要实现 HandlerInterceptor 接口。

  • 拦截范围不同:过滤器 Filter 会拦截所有的资源,而 Interceptor 只会拦截 Spring 环境中的资源。


十七、AOP


  • 接下来进入 AOP 的学习,AOP 也是 spring 框架的第二大核心
  • AOPAspect Oriented Programming(面向切面编程、面向方面编程)

17.1 AOP 基础


17.1.1 什么是 AOP


  • 比如,我们这里有一个项目,项目中开发了很多的业务功能。然而有一些业务功能执行效率比较低,

    执行耗时较长,我们需要针对于这些业务方法进行优化。 那首先第一步就需要定位出执行耗时

    比较长的业务方法,再针对于业务方法再来进行优化。

945a5b25-99c9-460c-aff4-8b47168b0ea9

此时我们就需要统计当前这个项目当中每一个业务方法的执行耗时。

那么统计每一个业务方法的执行耗时该怎么实现?

  • 一般我们会想到的方式如下:
ec801b7b-82fa-48cd-9bb0-658c765af816
  • 而这个功能如果通过 AOP 来实现,我们只需要单独定义下面这一小段代码即可,

    不需要修改原始的任何业务方法即可记录每一个业务方法的执行耗时。

e45347dc-cc7a-45cf-a5b7-5f7c0c5231c9
  • 所以,AOP 的优势主要体现在以下四个方面:

    • 1). 减少重复代码:不需要在业务方法中定义大量的重复性的代码,

      只需要将重复性的代码抽取到 AOP 程序中即可。

    • 2). 代码无侵入:在基于 AOP 实现这些业务功能时,

      对原有的业务代码是没有任何侵入的,不需要修改任何的业务代码。

    • 3). 提高开发效率

    • 4). 维护方便