## Day12 后端Web实战：登录认证

---

<figure><img src="/AiJavaWeb/imgs/jwai12-01.png"><figcaption>图1 登录需求</figcaption></figure>


##### 目录
* 登录功能
* 登录校验

  

### 1. 登录功能

#### 思路分析

<figure><img src="/AiJavaWeb/imgs/jwai12-02.png"><figcaption>图2 思路分析</figcaption></figure>

怎么样才算登录成功了呢?

* 用户名和密码都输入正确，登录成功
* 否则，登录失败

登录功能的本质是什么?

* 查询
* 根据用户名和密码查询员工信息

#### 员工登录-思路
<figure><img src="/AiJavaWeb/imgs/jwai12-03.png"><figcaption>图3 员工登录-思路</figcaption></figure>

##### cn/dzj/pojo/LoginInfo.java

```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
    private Integer id;
    private String username;
    private String name;
    private String token;
}
```

##### cn/dzj/controller/LoginController.java

```java
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;
    /**
     * 登录
     */
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("登录：{}",emp);
        LoginInfo info= empService.login(emp);
        if (info != null) {
            return Result.success(info);
        }
        return Result.error("用户名或密码错误");
    }
}
```

##### cn/dzj/service/EmpService.java

```java
    LoginInfo login(Emp emp);
```

##### cn/dzj/service/impl/EmpServiceImpl.java

```java
    @Override
    public LoginInfo login(Emp emp) {
        //1.调用mapper接口，根据用户名和密码查询员工信息
        Emp e=empMapper.selectByUsernameAndPassword(emp);

        //2，判断：判断是否存在这个员工，如果存在，组装登录成功信息
        if(e != null) {
            log.info("登录成功，员工信息：{}", e);
            return new LoginInfo(e.getId(), e.getUsername(), e.getName(), "");
        }

        //3．不存在，返回null
        return null;
    }
```

##### cn/dzj/mapper/EmpMapper.java

```java
    @Select("select id,name,username from emp where username=#{username} and password=#{password}")
    Emp selectByUsernameAndPassword(Emp emp);
```



<figure><img src="/AiJavaWeb/imgs/jwai12-04.png"><figcaption>图4 <mark>登录验证1</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai12-05.png"><figcaption>图5 <mark>登录验证2</mark></figcaption></figure>



#### 登录校验需求

* 问题：在未登录情况下，我们也可以直接访问部门管理、员工管理等功能。
* 需求：只有员工登录成功，才可以访问后台系统中的数据。

<figure><img src="/AiJavaWeb/imgs/jwai12-06.png"><figcaption>图6 登录校验需求</figcaption></figure>

### 2. 登录校验



#### 登录校验思路

* 登录标记：用户登录成功之后，在后续的每一次请求中，都可以获取到该标记。【会话技术】
* 统一拦截：过滤器Filter、拦截器interceptor

<figure><img src="/AiJavaWeb/imgs/jwai12-07.png"><figcaption>图7 登录校验思路</figcaption></figure>

* 会话技术
* JWT令牌
* 过滤器Filter
* 拦截器Interceptor


#### 2.1 会话技术

* 会话：用户打开浏览器，访问web服务器的资源，会话建立，直到有一方断开连接，会话结束。在一次会话中可以包含多次请求和响应。

<figure><img src="/AiJavaWeb/imgs/jwai12-08.png"><figcaption>图8 会话</figcaption></figure>

* 会话跟踪：一种维护浏览器状态的方法，服务器需要识别多次请求是否来自于同一浏览器，以便在<mark>同一次会话的多次请求间共享数据</mark>。
* 会话跟踪方案：
  * 客户端会话跟踪技术：Cookie
  * 服务端会话跟踪技术：Session
  * 令牌技术

<figure><img src="/AiJavaWeb/imgs/jwai12-09.png"><figcaption>图9 会话跟踪</figcaption></figure>



#### 会话跟踪方案对比

#### ①客户端存储会话跟踪技术：Cookie

<figure><img src="/AiJavaWeb/imgs/jwai12-10.png"><figcaption>图10 客户端存储会话跟踪技术：Cookie</figcaption></figure>

##### cn/dzj/controller/SessionController.java

```java
package cn.dzj.controller;

import cn.dzj.pojo.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * HttpSession演示
 */
@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","DuLaoshi")); //设置Cookie/响应Cookie
        return Result.success();
    }

    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }

}
```



<figure><img src="/AiJavaWeb/imgs/jwai12-11.png"><figcaption>图11 <mark>cookie测试验证</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai12-12.png"><figcaption>图12 cookie小结</figcaption></figure>

#### ②服务器端存储会话跟踪技术：session

<figure><img src="/AiJavaWeb/imgs/jwai12-13.png "><figcaption>图13 客户端存储会话跟踪技术：session</figcaption></figure>

##### cn/dzj/controller/SessionController.java

```java

    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1: {}", session.hashCode());

        session.setAttribute("loginUser", "seesion 杜老师"); //往session中存储数据
        return Result.success();
    }

    @GetMapping("/s2")
    public Result session2(HttpSession session){
        log.info("HttpSession-s2: {}", session.hashCode());

        Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
        log.info("loginUser: {}", loginUser);
        return Result.success(loginUser);
    }
```



<figure><img src="/AiJavaWeb/imgs/jwai12-14.png"><figcaption>图14 <mark>session测试结果验证</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai12-15.png"><figcaption>图15 Session小结</figcaption></figure>

#### ③服务器端存储会话跟踪技术：令牌方案

令牌（Token）临时身份钥匙----临时身份证(工牌)，具有唯一性、临时性、防篡改性

#### http会话中的令牌

HTTP 是**无状态协议**（每次请求都是独立的，服务器记不住上一次的交互），而 “令牌（Token）” 就是解决这个问题的核心工具 —— 它像一把 “临时身份钥匙”，让服务器能识别重复访问的客户端，维持会话（比如登录后保持登录状态）。

#### 令牌的核心逻辑（3 步看懂）：

1. **发令牌**：用户首次登录（或需要验证时），客户端提交账号密码等信息；服务器验证通过后，生成一个**唯一的随机字符串（即令牌）**，并关联用户身份（比如 “令牌 A 对应用户小明”），然后把令牌返回给客户端。
2. **存令牌**：客户端收到令牌后，会存在本地（常见方式：Cookie、LocalStorage 等）。
3. **用令牌**：之后客户端再发请求（比如逛个人主页、下单），会自动带上这个令牌（比如放在请求头、参数里）；服务器收到后，查 “令牌 - 用户” 关联表，确认令牌有效→识别出用户身份，直接处理请求（不用再让用户重复登录）。

#### 关键特点：

- 唯一性：每个令牌对应一个用户 / 会话，避免冒充。
- 时效性：通常有有效期（比如 2 小时），过期后需要重新获取，更安全。
- 轻量化：比传统的 “Session ID + 服务器存会话数据” 更灵活（尤其适合 APP、跨域场景）。



<figure><img src="/AiJavaWeb/imgs/jwai12-16.png"><figcaption>图16 http会话中的令牌</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai12-17.png"><figcaption>图17 令牌方案小结</figcaption></figure>



#### 2.2 JWT令牌

<figure><img src="/AiJavaWeb/imgs/jwai12-18.png"><figcaption>图18 JWT令牌</figcaption></figure>

#### JWT令牌-介绍

* 全称：JSON Web Token （https://jwt.io/）
* 定义了一种简洁的、自包含的格式，用于在通信双方以json数据格式安全的传输信息。
* 组成：
  * 第一部分：Header(头），记录令牌类型、签名算法等。 例如：{"alg":"HS256","type":"JWT"}
  * 第二部分：Payload(有效载荷），携带一些自定义信息、默认信息等。 例如：{"id":"1","username":"Tom"}
  * 第三部分：Signature(签名），防止Token被篡改、确保安全性。将header、payload融入，并加入指定秘钥，通过指定签名算法计算而来。
* Base64：是一种基于64个可打印字符（A-Z a-z 0-9 + /）来表示二进制数据的编码方式。

<figure><img src="/AiJavaWeb/imgs/jwai12-19.png"><figcaption>图19 JWT令牌-介绍</figcaption></figure>

#### JWT令牌-生成/解析

* 引入jjwt的依赖。
* 调用官方提供的工具类 Jwts 来生成或解析jwt令牌。

```xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
```

##### src/test/java/cn/dzj/JwtTest.java 主要代码

```java
/**
* 生成JwT令牌-Jwts.builder()
*/
@Test
public void testGenJwt() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 10);
    claims.put("username", "杜老师");
    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "SVRIRUlNQQ==")
        //"SVRIRUlNQQ==" 是itheima的base64编码 密钥
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) //设置1小时后过期
        .compact();
    System.out.println(jwt);
}

@Test
public void testParseJwt() throws Exception {
    String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..换成上面生成的字符串.";
    Claims claims = Jwts.parser()
        .setSigningKey("SVRIRUlNQQ==")  //指定密钥
        .parseClaimsJws(jwtToken)  //解析令牌
        .getBody();  //获取自定义信息 -- 载荷
    System.out.println(claims);
}

```

http://base64.us

<figure><img src="/AiJavaWeb/imgs/jwai12-20.png"><figcaption>图20 <mark>jwt测试1</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai12-21.png"><figcaption>图21 <mark>Jwt测试2</mark></figcaption></figure>



#### 小结

JWT令牌由哪几个部分组成 ，每个部分都存储什么内容？

* header（头），记录令牌类型、签名算法
* payload（载荷），携带一些自定义的信息
* signature（签名），访问被篡改，保证安全性

JWT令牌生成及校验？

* Jwts.builder()...
* Jwts.parser()...

JWT令牌解析（校验）时什么情况会报错？

* JWT令牌被篡改 或 过期失效了

注意事项：JWT校验时使用的签名秘钥，必须和生成JWT令牌时使用的秘钥是配套的

<figure><img src="/AiJavaWeb/imgs/jwai12-22.png"><figcaption>图22 小结</figcaption></figure>

#### 案例：登录成功后-生成令牌

##### 通义千文提问词

```
请帮我基于如下单元测试方法，改造成一个JWT令牌操作的工具类，类名：JwtUtils，具体要求如下：
1.工具类中有两个方法，一个方法生成令牌，另一个是解析令牌。
2.生成令牌时使用的秘钥，和测试类中的一致即可。
3.令牌的过期时间设置12小时。
原始的测试类的代码如下：

 @Test
    public void testGenJwt() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 10);
        claims.put("username", "杜老师");
        String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "SVRIRUlNQQ==")
                //"SVRIRUlNQQ==" 是itheima的base64编码 密钥
                .addClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + 100*1000)) //设置100秒后过期
                .compact();
        System.out.println(jwt);
    }
    @Test
    public void testParseJwt() throws Exception {
        String jwtToken = "eyJhbGciOiJIUzI1NiJ9"
                +".eyJpZCI6MTAsInVzZXJuYW1lIjoi5p2c6ICB5biIIiwiZXhwIjoxNzU3MTYyODc2fQ"
                +".aFy7pbnynULwxaILyOygUspkZjRENmMSod_S8d-RL9o";
        Claims claims = Jwts.parser()
                .setSigningKey("SVRIRUlNQQ==")
                .parseClaimsJws(jwtToken)
                .getBody();
        System.out.println(claims);
    }

tongyi.aliyun.com/qianwen
```

##### cn/dzj/utils/JwtUtils.java

```java
package cn.dzj.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

/**
 * JWT 工具类，用于生成和解析 JWT 令牌
 */
public class JwtUtils {

    // 密钥：使用与测试类中一致的 "SVRIRUlNQQ=="（"itheima" 的 Base64 编码）
    private static final String SECRET_KEY = "SVRIRUlNQQ==";

    // 令牌过期时间：1 小时（单位：毫秒）
    private static final long EXPIRATION = 60 * 60 * 1000; // 1小时

    /**
     * 生成 JWT 令牌
     *
     * @param claims 要包含在令牌中的自定义声明（如用户ID、用户名等）
     * @return JWT 令牌字符串
     */
    public static String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims) // 添加自定义声明
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用 HS256 算法签名
                .compact();
    }

    /**
     * 解析 JWT 令牌
     *
     * @param token JWT 令牌字符串
     * @return 解析后的 Claims 对象，包含令牌中的信息
     * @throws io.jsonwebtoken.JwtException 如果令牌无效或签名不匹配
     */
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY) // 使用相同的密钥解析
                .parseClaimsJws(token)    // 解析并验证 JWT
                .getBody();               // 获取 payload 中的 Claims
    }
}
```

##### cn/dzj/service/impl/EmpServiceImpl.java   修改public LoginInfo login(Emp emp)方法 if代码

```java
		if(e != null) {
            log.info("登录成功，员工信息：{}", e);
            
            //生成JWT令牌
            Map<String,Object> claims =new HashMap<>();
            claims.put("id", e.getId());
            claims.put("username",e.getUsername());
            claims.put("name",e.getName());
            String jwt= JwtUtils.generateToken(claims);
            return new LoginInfo(e.getId(),e.getUsername(), e.getName(),jwt);
        }
```

<figure><img src="/AiJavaWeb/imgs/jwai12-23.png"><figcaption>图23 <mark>登录成功后-生成令牌验证</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai12-24.png"><figcaption>图24 <mark>登录成功后-生成令牌验证2</mark></figcaption></figure>

#### 2.3 过滤器Filter

<figure><img src="/AiJavaWeb/imgs/jwai12-25.png"><figcaption>图25 校验：统一拦截</figcaption></figure>

* 快速入门
* 令牌校验Filter
* 详解

#### ①快速入门

* 概念：<mark>Filter过滤器</mark>，是JavaWeb三大组件(Servlet、Filter、Listener)之一。
* 过滤器可以把对资源的请求<mark>拦截</mark>下来，从而实现一些特殊的功能。
* 过滤器一般完成一些<mark>通用</mark>的操作，比如：登录校验、统一编码处理、敏感字符处理等。

<figure><img src="/AiJavaWeb/imgs/jwai12-26.png"><figcaption>图26 过滤器Filter </figcaption></figure>

#### Filter快速入门

* 定义Filter：定义一个类，实现 Filter 接口，并实现其所有方法。
* 配置Filter：Filter类上加@WebFilter注解，配置拦截路径。
* 引导类上加 @ServletComponentScan 开启Servlet组件支持。

##### cn/dzj/filter/DemoFilter.java 

```java
package cn.dzj.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("初始化方法 init ...");
    }

    @Override
    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
        log.info("拦截到了请求... 放行前...");
        chain.doFilter(servletRequest, servletResponse);
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        log.info("销毁方法 destroy ... ");
    }
}
```

##### cn/dzj/TliasWebManagementApplication.java 添加注解

```java
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
...
```



<figure><img src="/AiJavaWeb/imgs/jwai12-27.png"><figcaption>图27 <mark>Filter入门程序验证1</mark>mark></figcaption></figure>


<figure><img src="/AiJavaWeb/imgs/jwai12-28.png"><figcaption>图28 <mark>Filter入门程序验证2</mark>mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai12-29.png"><figcaption>图29 Filter小结</figcaption></figure>

#### ②令牌校验Filter

#### 问题提出

<figure><img src="/AiJavaWeb/imgs/jwai12-30.png"><figcaption>图30 问题提出</figcaption></figure>

#### 令牌校验Filter--流程

* 1.获取到请求路径
* 2.判断是否是登录请求，如果路径中包含/1ogin，说明是登录操作，放行
* 3.获取请求头中的token
* 4.判断token是否存在，如果不存在，说明用户没有登录，返回错误信息（响应401状态码）
* 5.如果token存在，校验令牌，如果校验失败->返回错误信息（响应401状态码）
* 6.校验通过，放行

<figure><img src="/AiJavaWeb/imgs/jwai12-31.png"><figcaption>图31 令牌校验Filter--流程</figcaption></figure>

##### cn/dzj/filter/TokenFilter.java

```java
package cn.dzj.filter;
import cn.dzj.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("/*")
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1.获取到请求路径
        String path = request.getRequestURI();

        //2.判断是否是登录请求，如果路径中末尾/login，说明是登录操作，放行
        if(path.endsWith("/login")) {
            log.info("登录操作，放行");
            filterChain.doFilter(request,response);
            return;
        }

        //3.获取请求头中的token
        String token = request.getHeader("token");

        //4.判断token是否存在，如果不存在，说明用户没有登录，返回错误信息（响应401状态码）
        if(token == null || token.isEmpty()) {
            log.info("令牌为空，响应401");
            response.setStatus(401);
            return;
        }

        //5.如果token存在，校验令牌，如果校验失败->返回错误信息（响应401状态码）
        try {
            JwtUtils.parseToken( token);
        } catch (Exception e) {
            log.info("令牌非法，响应401");
            response.setStatus(401);
            return;
        }

        //6.校验通过，放行
        log.info("令牌合法，放行");
        filterChain.doFilter(request,response);

    }
}
```

#### 测试

1.登录并获取令牌

2.不带令牌查询性别统计

3.带篡改令牌查询性别统计

4.带正确令牌查询性别统计

<figure><img src="/AiJavaWeb/imgs/jwai12-32.png"><figcaption>图32 <mark>令牌校验Filter测试</mark></figcaption></figure>



#### ③详解(执行流程、拦截路径、过滤器链)

#### 执行流程

<figure><img src="/AiJavaWeb/imgs/jwai12-33.png"><figcaption>图33 Filter执行流程</figcaption></figure>

问题1：放行后访问对应资源，资源访问完成后，还会回到Filter中吗？会

问题2：如果回到Filter中，是重新执行还是执行放行后的逻辑呢？执行放行后逻辑



```java
// 注释TokenFilter的@WebFilter，放开DemoFilter的@WebFilter并修改其doFilter方法
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
        log.info("拦截到了请求... 放行前...");
        chain.doFilter(servletRequest, servletResponse);
        log.info("拦截到了请求... 放行后...");
    }
```

<figure><img src="/AiJavaWeb/imgs/jwai12-34.png"><figcaption>图34 <mark>执行流程验证</mark></figcaption></figure>



#### 拦截路径

Filter 可以根据需求，配置不同的拦截资源路径：```@WebFilter("/*")```

| 拦截路径     | urlPatterns值 | 含义                               |
| ------------ | ------------- | ---------------------------------- |
| 拦截具体路径 | /login        | 只有访问 /login 路径时，才会被拦截 |
| 目录拦截     | /emps/*       | 访问/emps下的所有资源，都会被拦截  |
| 拦截所有     | /*            | 访问所有资源，都会被拦截           |



#### 过滤器链

* 介绍：一个web应用中，可以配置多个过滤器，这多个过滤器就形成了一个<mark>过滤器链</mark>。
* 顺序：注解配置的Filter，优先级是按照<mark>过滤器类名（字符串）</mark>的自然排序。

<figure><img src="/AiJavaWeb/imgs/jwai12-35.png"><figcaption>图35 过滤器链</figcaption></figure>

##### 将DemoFilter.java复制为AbcFilter.java；将AbcFilter改为XyzFilter测试

<figure><img src="/AiJavaWeb/imgs/jwai12-36.png"><figcaption>图36 <mark>过滤器链验证</mark></figcaption></figure>



#### 小结

过滤器的执行流程 ?

* 放行前 -> 放行 -> 资源 -> 放行后

配置的过滤器的拦截路径/* 与 /emps/* 分别代表什么意思 ?

* ```/*```：表示拦截所有
* ```/emps/*```：表示目录拦截，拦截/emps下的所有资源

什么是过滤器链 ?

* 项目中的多个过滤器就形成了一个过滤器链

<figure><img src="/AiJavaWeb/imgs/jwai12-37.png"><figcaption>图37 Filter小结</figcaption></figure>



#### 2.4 拦截器Interceptor

* 快速入门
* 令牌校验Interceptor
* 详解

#### ①快速入门

* 概念：是一种动态拦截方法调用的机制，类似于过滤器。Spring框架中提供的，主要用来动态拦截控制器方法的执行。
* 作用：拦截请求，在指定的方法调用前后，根据业务需要执行预先设定的代码。

<figure><img src="/AiJavaWeb/imgs/jwai12-38.png"><figcaption>图38 拦截器</figcaption></figure>



#### Interceptor快速入门

* 定义拦截器，实现HandlerInterceptor接口，并实现其所有方法。
* 注册拦截器

<figure><img src="/AiJavaWeb/imgs/jwai12-39.png"><figcaption>图39 拦截器定义与注册</figcaption></figure>

##### cn/dzj/interceptor/DemoInterceptor.java

```java
@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行，返回true：放行，返回false：不放行
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        log.info("preHandle... 目标资源方法执行前执行");
        return true;
    }
    
    //目标资源方法执行后执行
    public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object handler, ModelAndView mv) throws Exception {
        log.info("postHandle... 目标资源方法执行后执行");
    }
    
    // 视图渲染完毕后执行，最后执行
    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion... 视图渲染完毕后执行，最后执行");
    }
}

```

##### cn/dzj/config/WebConfig.java

```java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private DemoInterceptor demoInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
    }
}
```



在ApiFox中执行：根据ID查询员工信息，控制台显示如下。

<figure><img src="/AiJavaWeb/imgs/jwai12-40.png"><figcaption>图40 <mark>根据ID查询员工信息验证</mark></figcaption></figure>



#### 小结

拦截器Interceptor的使用步骤 ?

* 定义：实现HandlerInterceptor接口
  * preHandle  目标资源方法执行前执行
  * postHandle  目标资源方法执行后执行
  * afterCompletion  视图渲染完毕后执行，最后执行
* 配置：定义一个配置类实现WebMvcConfigurer接口，注册拦截器(/**)



<figure><img src="/AiJavaWeb/imgs/jwai12-41.png"><figcaption>图41 小结</figcaption></figure>



#### ②令牌校验Interceptor

* 1.获取请求url。
* 2.判断请求url中是否包含login，如果包含，说明是登录操作，放行。
* 3.获取请求头中的令牌（token）。
* 4.判断令牌是否存在，如果不存在，响应401。
* 5.解析token，如果解析失败，响应401 。
* 6.放行。

<figure><img src="/AiJavaWeb/imgs/jwai12-31.png"><figcaption>图 令牌校验流程</figcaption></figure>

##### cn/dzj/interceptor/TokenInterceptor.java

```java
package cn.dzj.interceptor;

import cn.dzj.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
    //目标资源方法执行前执行，返回true：放行，返回false：不放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取到请求路径
        String path = request.getRequestURI();

        //2.判断是否是登录请求，如果路径中末尾/login，说明是登录操作，放行
        if(path.endsWith("/login")) {
            log.info("登录操作，放行");
            return true;
        }

        //3.获取请求头中的token
        String token = request.getHeader("token");

        //4.判断token是否存在，如果不存在，说明用户没有登录，返回错误信息（响应401状态码）
        if(token == null || token.isEmpty()) {
            log.info("令牌为空，响应401");
            response.setStatus(401);
            return false;
        }

        //5.如果token存在，校验令牌，如果校验失败->返回错误信息（响应401状态码）
        try {
            JwtUtils.parseToken( token);
        } catch (Exception e) {
            log.info("令牌非法，响应401");
            response.setStatus(401);
            return false;
        }

        //6.校验通过，放行
        log.info("令牌合法，放行");
        return true;
    }
}

```

##### cn/dzj/config/WebConfig.java

```java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    //private DemoInterceptor demoInterceptor;
    private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
        //registry.addInterceptor(demoInterceptor)
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**");  // 拦截所有请求
                //.excludePathPatterns("/login");  // 排除登录接口
    }
}
```

#### 测试

1.登录并获取令牌

2.不带令牌查询

3.带篡改令牌查询

4.带正确令牌查询



<figure><img src="/AiJavaWeb/imgs/jwai12-42.png"><figcaption>图42 <mark>拦截器验证</mark></figcaption></figure>



#### ③详解（拦截路径、执行流程）

#### 拦截路径

拦截器可以根据需求，配置不同的拦截路径：

##### cn/dzj/config/WebConfig.java

```java
package cn.dzj.config;

import cn.dzj.interceptor.TokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    //private DemoInterceptor demoInterceptor;
    private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
        //registry.addInterceptor(demoInterceptor)
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")  // 拦截所有请求
                .excludePathPatterns("/login");  // 排除登录接口
    }
}
```

| 拦截路径  | 含义                 | 举例                                                         |
| --------- | -------------------- | ------------------------------------------------------------ |
| /*        | 一级路径             | 能匹配/depts，/emps，/login，<mark>不能匹配 /depts/1</mark>  |
| /**       | 任意级路径           | 能匹配/depts，/depts/1，/depts/1/2                           |
| /depts/*  | /depts下的一级路径   | 能匹配/depts/1，<mark>不能匹配/depts/1/2，/depts</mark>      |
| /depts/** | /depts下的任意级路径 | 能匹配/depts，/depts/1，/depts/1/2，<mark>不能匹配/emps/1</mark> |

<figure><img src="/AiJavaWeb/imgs/jwai12-43.png"><figcaption>图43 拦截器--拦截路径</figcaption></figure>



#### 执行流程

Filter 与 Interceptor 区别：

* 接口规范不同：过滤器需要实现Filter接口，而拦截器需要实现HandlerInterceptor接口。
* 拦截范围不同：过滤器Filter会拦截所有的资源，而Interceptor只会拦截Spring环境中的资源。

  


<figure><img src="/AiJavaWeb/imgs/jwai12-44.png"><figcaption>图44 过滤器和拦截器同时运行的执行流程</figcaption></figure>

#### 验证

打开DemoFilter的@WebFilter设置、关闭其他Filter----注释@WebFilter设置；

##### cn/dzj/config/WebConfig.java

```java
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private DemoInterceptor demoInterceptor;
    //private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor)
        //registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**");  // 拦截所有请求
               // .excludePathPatterns("/login");  // 排除登录接口
    }
}
```



#### 小结

拦截器中的拦截路径 /* 与 /** 的区别是什么 ?*

* ```/*```：只能拦截一级路径，如：/depts，/emps
* ```/**```：拦截任意级路径，如：/depts，/depts/1

过滤器与拦截器的区别 ?

* 接口规范不同：Filter是Servlet技术规范、Interceptor是Spring技术规范
* 拦截范围不同：Filter拦截范围是Servlet范围，Interceptor拦截范围是Spring范围

<figure><img src="/AiJavaWeb/imgs/jwai12-46.png"><figcaption>图46 小结</figcaption></figure>



①②③④⑤⑥⑦⑧⑨⑩

---

## HTTP 协议中，与 Cookie 三个自动

在 HTTP 协议中，与 Cookie 相关的 “三个自动” 通常指浏览器或客户端对 Cookie 的三种自动处理机制，这些机制是实现状态管理的核心基础：

### 一、自动存储（Automatic Storage）

浏览器在接收到服务器通过`Set-Cookie`响应头发送的 Cookie 时，会自动将其存储在本地缓存中。这一过程无需用户干预，存储位置根据 Cookie 的属性分为两种：

1. **内存存储**：未设置`Expires`或`Max-Age`的会话 Cookie（Session Cookie），仅在浏览器会话期间存在，关闭浏览器后自动清除。
2. **磁盘存储**：设置了过期时间的持久化 Cookie（Persistent Cookie），会被写入本地硬盘，在指定时间内有效。

例如，当用户访问电商网站时，服务器返回的`Set-Cookie: session_id=abc123`会被浏览器自动保存，用于识别用户身份。

### 二、自动携带（Automatic Inclusion）

在后续所有对同一域名的请求中，浏览器会自动在请求头中附加已存储的 Cookie，无需用户手动添加。这一机制确保服务器能够通过 Cookie 识别用户状态，例如：http

```http
GET /cart HTTP/1.1
Host: example.com
Cookie: session_id=abc123; theme=dark
```



此过程完全透明，用户无需感知，极大简化了状态管理流程。即使跨页面或跨标签页访问，浏览器也会自动同步 Cookie 信息。

### 三、自动过期（Automatic Expiration）

浏览器会根据 Cookie 的`Expires`或`Max-Age`属性自动处理过期逻辑：

1. **时间触发**：当 Cookie 的`Expires`时间到达或`Max-Age`倒计时结束，浏览器会自动删除该 Cookie。
2. **会话结束**：会话 Cookie 在浏览器关闭时自动失效，无需显式清理。

例如，某 Cookie 设置`Max-Age=86400`（24 小时），浏览器会在 24 小时后自动清除该 Cookie，确保用户数据的时效性和安全性。

### 扩展机制：自动路径匹配与域限制

除上述核心机制外，浏览器还会自动执行以下策略：

1. **路径匹配**：仅在请求路径与 Cookie 的`Path`属性匹配时携带该 Cookie。例如，`Path=/login`的 Cookie 仅在访问`/login`路径时发送。
2. **域限制**：Cookie 仅在同源请求中自动携带，跨域请求（如`example.com`与`api.example.com`）需通过`Domain`属性显式授权。

### 实际应用场景

1. **电商购物车**：用户添加商品后，服务器通过 Cookie 记录购物车内容，后续访问结算页面时，浏览器自动携带 Cookie，确保购物车数据不丢失。
2. **自动登录**：用户勾选 “记住我” 后，服务器返回长期有效的 Cookie，浏览器自动存储并在下次访问时携带，实现免密登录。
3. **个性化推荐**：服务器通过 Cookie 记录用户浏览偏好，后续请求时自动返回定制化内容。

### 技术实现原理

1. **Cookie 管理器**：浏览器内部的 Cookie 管理器（如 JMeter 中的`HTTP Cookie Manager`）负责统一管理存储、携带和过期逻辑。
2. **属性控制**：服务器通过`Secure`（仅 HTTPS 传输）、`HttpOnly`（防止 XSS 攻击）等属性进一步约束 Cookie 的自动处理行为。

### 与 HTTP 协议的协同

Cookie 的自动机制与 HTTP 的无状态性形成互补：

- **无状态性**：HTTP 默认不保留请求上下文，每次请求独立处理。
- **Cookie 补偿**：通过自动存储和携带 Cookie，服务器可在多次请求间关联用户状态，实现登录、购物车等复杂功能。

### 注意事项

1. **安全风险**：自动携带 Cookie 可能导致跨站请求伪造（CSRF）攻击，需通过`SameSite`属性限制第三方站点携带。
2. **存储容量限制**：单个域名的 Cookie 数量通常限制在 50 个以内，总大小不超过 4KB，超出时浏览器会自动丢弃旧 Cookie。

通过这三个自动机制，Cookie 在无需用户干预的情况下实现了高效的状态管理，成为现代 Web 应用的基础设施之一。理解这些机制有助于开发者优化用户体验、防范安全漏洞，并合理设计 Cookie 策略（如设置`Max-Age`平衡时效性与性能）



## 使用 jjwt:0.12.7` 依赖实现 JWT 生成与解析的测试示例

---

```xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.12.7</version>
</dependency
```

基于 JUnit 5 编写：

```java
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;

public class JwtTest {

    // 过期时间：1小时（3600000毫秒）
    private static final long EXPIRATION_TIME = 3600000;
    // 生成256位HMAC密钥（JJWT 0.12.x要求密钥长度符合算法安全标准）
    private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    /**
     * 测试生成JWT令牌
     */
    @Test
    public void testGenerateJwt() {
        // 1. 准备自定义载荷（claims）
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", 1001);
        claims.put("username", "testUser");
        claims.put("role", "admin");

        // 2. 生成JWT令牌
        String jwt = Jwts.builder()
                .setClaims(claims) // 设置自定义载荷
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 设置过期时间
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256) // 设置签名算法和密钥
                .compact();

        // 验证令牌不为空
        assertNotNull(jwt);
        System.out.println("生成的JWT令牌：" + jwt);
    }

    /**
     * 测试解析有效的JWT令牌
     */
    @Test
    public void testParseValidJwt() {
        // 1. 先生成一个令牌
        String jwt = generateTestJwt();

        // 2. 解析令牌
        try {
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY) // 设置验证密钥
                    .build()
                    .parseClaimsJws(jwt) // 解析JWT
                    .getBody();

            // 3. 验证载荷内容
            assertEquals(1001, claims.get("userId"));
            assertEquals("testUser", claims.get("username"));
            assertEquals("admin", claims.get("role"));
            assertNotNull(claims.getIssuedAt());
            assertNotNull(claims.getExpiration());

            System.out.println("解析成功，用户ID：" + claims.get("userId"));

        } catch (JwtException e) {
            fail("解析有效令牌时发生异常：" + e.getMessage());
        }
    }

    /**
     * 测试解析过期的JWT令牌
     */
    @Test
    public void testParseExpiredJwt() {
        // 1. 生成一个已过期的令牌（过期时间设为1毫秒后）
        String expiredJwt = Jwts.builder()
                .setClaims(Map.of("userId", 1001))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1)) // 1毫秒后过期
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .compact();

        // 2. 等待令牌过期
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 3. 解析过期令牌，预期抛出ExpiredJwtException
        assertThrows(ExpiredJwtException.class, () -> {
            Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(expiredJwt);
        });
    }

    /**
     * 测试解析签名错误的JWT令牌（使用错误密钥）
     */
    @Test
    public void testParseInvalidSignatureJwt() {
        // 1. 生成正常令牌
        String validJwt = generateTestJwt();

        // 2. 使用错误的密钥解析
        Key wrongKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 新的随机密钥
        assertThrows(SignatureException.class, () -> {
            Jwts.parserBuilder()
                    .setSigningKey(wrongKey) // 错误的密钥
                    .build()
                    .parseClaimsJws(validJwt);
        });
    }

    // 辅助方法：生成测试用JWT
    private String generateTestJwt() {
        return Jwts.builder()
                .setClaims(Map.of(
                        "userId", 1001,
                        "username", "testUser",
                        "role", "admin"
                ))
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .compact();
    }
}

```

#### 代码说明：

1. **依赖适配**：针对 `0.12.7` 版本，使用 `Keys.secretKeyFor()` 生成符合算法要求的密钥（HS256 需 256 位密钥），避免手动生成密钥可能导致的长度不足问题。
2. **核心功能**：
   - 生成令牌：设置自定义载荷（userId、username 等）、签发时间、过期时间，并使用 HS256 算法签名
   - 解析令牌：验证签名合法性、提取载荷信息
   - 异常测试：覆盖令牌过期（`ExpiredJwtException`）、签名错误（`SignatureException`）等场景
3. **注意事项**：
   - 密钥（`SECRET_KEY`）在实际项目中需安全存储，避免硬编码
   - 过期时间根据业务需求调整（示例为 1 小时）
   - 0.12.x 版本相比旧版本强化了安全校验，密钥长度和算法必须匹配（如 HS256 对应 256 位密钥）

运行测试前，需确保添加 JUnit 5 依赖（如 `junit-jupiter-api` 和 `junit-jupiter-engine`）。





## Spring中，多个 Interceptor 的执行顺序？

---

在 Spring 中，多个 Interceptor（拦截器）的执行顺序由它们在配置时的注册顺序决定，具体规则如下：

1. **注册顺序决定前置方法（preHandle）的执行顺序**
   当多个拦截器被注册到 Spring 容器时，它们的 `preHandle` 方法会按照**注册的先后顺序**执行（先注册的先执行）。
2. **后置方法（postHandle）和完成方法（afterCompletion）的执行顺序相反**
   - `postHandle` 方法：按照**注册的逆序**执行（后注册的先执行）。
   - `afterCompletion` 方法：同样按照**注册的逆序**执行（后注册的先执行）。
3. **特殊情况：preHandle 返回 false 时的中断机制**
   如果某个拦截器的 `preHandle` 方法返回 `false`，则后续所有拦截器的 `preHandle` 方法都不会执行，且已执行过 `preHandle` 的拦截器会按照**逆序执行 `afterCompletion` 方法**。

#### 示例说明

假设有三个拦截器 `InterceptorA`、`InterceptorB`、`InterceptorC`，注册顺序为 `A → B → C`，则执行流程如下：

```plaintext
1. A.preHandle() → 执行
2. B.preHandle() → 执行
3. C.preHandle() → 执行
4. 控制器方法执行
5. C.postHandle() → 执行
6. B.postHandle() → 执行
7. A.postHandle() → 执行
8. 视图渲染完成
9. C.afterCompletion() → 执行
10. B.afterCompletion() → 执行
11. A.afterCompletion() → 执行
```

#### 配置方式对顺序的影响

- **XML 配置**：`<mvc:interceptors>` 中拦截器的声明顺序即注册顺序。
- **Java 配置**：通过 `WebMvcConfigurer` 的 `addInterceptors` 方法添加拦截器时，添加顺序即注册顺序。

例如，Java 配置中按以下顺序添加时，执行顺序为 `InterceptorA → InterceptorB`：

```java
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new InterceptorA()); // 先注册
    registry.addInterceptor(new InterceptorB()); // 后注册
}
```

总结：拦截器的执行顺序本质上由注册顺序决定，前置方法正序执行，后置和完成方法逆序执行。



