Day12 后端Web实战:登录认证

目录
- 登录功能
- 登录校验
1. 登录功能
思路分析

怎么样才算登录成功了呢?
- 用户名和密码都输入正确,登录成功
- 否则,登录失败
登录功能的本质是什么?
- 查询
- 根据用户名和密码查询员工信息
员工登录-思路

cn/zjy/pojo/LoginInfo.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private Integer id;
private String username;
private String name;
private String token;
}
cn/zjy/controller/LoginController.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/zjy/service/EmpService.java
LoginInfo login(Emp emp);
cn/zjy/service/impl/EmpServiceImpl.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/zjy/mapper/EmpMapper.java
@Select("select id,name,username from emp where username=#{username} and password=#{password}")
Emp selectByUsernameAndPassword(Emp emp);


登录校验需求
- 问题:在未登录情况下,我们也可以直接访问部门管理、员工管理等功能。
- 需求:只有员工登录成功,才可以访问后台系统中的数据。

2. 登录校验
登录校验思路
- 登录标记:用户登录成功之后,在后续的每一次请求中,都可以获取到该标记。【会话技术】
- 统一拦截:过滤器Filter、拦截器interceptor

- 会话技术
- JWT令牌
- 过滤器Filter
- 拦截器Interceptor
2.1 会话技术
- 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。

- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
- 会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术

会话跟踪方案对比
①客户端存储会话跟踪技术:Cookie

cn/zjy/controller/SessionController.java
package cn.zjy.controller;
import cn.zjy.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();
}
}


②服务器端存储会话跟踪技术:session

cn/zjy/controller/SessionController.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);
}


③服务器端存储会话跟踪技术:令牌方案
令牌(Token)临时身份钥匙----临时身份证(工牌),具有唯一性、临时性、防篡改性
http会话中的令牌
HTTP 是无状态协议(每次请求都是独立的,服务器记不住上一次的交互),而 “令牌(Token)” 就是解决这个问题的核心工具 —— 它像一把 “临时身份钥匙”,让服务器能识别重复访问的客户端,维持会话(比如登录后保持登录状态)。
令牌的核心逻辑(3 步看懂):
- 发令牌:用户首次登录(或需要验证时),客户端提交账号密码等信息;服务器验证通过后,生成一个唯一的随机字符串(即令牌),并关联用户身份(比如 “令牌 A 对应用户小明”),然后把令牌返回给客户端。
- 存令牌:客户端收到令牌后,会存在本地(常见方式:Cookie、LocalStorage 等)。
- 用令牌:之后客户端再发请求(比如逛个人主页、下单),会自动带上这个令牌(比如放在请求头、参数里);服务器收到后,查 “令牌 - 用户” 关联表,确认令牌有效→识别出用户身份,直接处理请求(不用再让用户重复登录)。
关键特点:
- 唯一性:每个令牌对应一个用户 / 会话,避免冒充。
- 时效性:通常有有效期(比如 2 小时),过期后需要重新获取,更安全。
- 轻量化:比传统的 “Session ID + 服务器存会话数据” 更灵活(尤其适合 APP、跨域场景)。


2.2 JWT令牌

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 + /)来表示二进制数据的编码方式。

JWT令牌-生成/解析
- 引入jjwt的依赖。
- 调用官方提供的工具类 Jwts 来生成或解析jwt令牌。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
src/test/java/cn/zjy/JwtTest.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);
}


小结
JWT令牌由哪几个部分组成 ,每个部分都存储什么内容?
- header(头),记录令牌类型、签名算法
- payload(载荷),携带一些自定义的信息
- signature(签名),访问被篡改,保证安全性
JWT令牌生成及校验?
- Jwts.builder()...
- Jwts.parser()...
JWT令牌解析(校验)时什么情况会报错?
- JWT令牌被篡改 或 过期失效了
注意事项:JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的

案例:登录成功后-生成令牌
通义千文提问词
请帮我基于如下单元测试方法,改造成一个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/zjy/utils/JwtUtils.java
package cn.zjy.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/zjy/service/impl/EmpServiceImpl.java 修改public LoginInfo login(Emp emp)方法 if代码
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);
}


2.3 过滤器Filter

- 快速入门
- 令牌校验Filter
- 详解
①快速入门
- 概念:Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。

Filter快速入门
- 定义Filter:定义一个类,实现 Filter 接口,并实现其所有方法。
- 配置Filter:Filter类上加@WebFilter注解,配置拦截路径。
- 引导类上加 @ServletComponentScan 开启Servlet组件支持。
cn/zjy/filter/DemoFilter.java
package cn.zjy.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/zjy/TliasWebManagementApplication.java 添加注解
@ServletComponentScan
@SpringBootApplication
public class TliasManagementApplication {
...



②令牌校验Filter
问题提出

令牌校验Filter--流程
- 1.获取到请求路径
- 2.判断是否是登录请求,如果路径中包含/1ogin,说明是登录操作,放行
- 3.获取请求头中的token
- 4.判断token是否存在,如果不存在,说明用户没有登录,返回错误信息(响应401状态码)
- 5.如果token存在,校验令牌,如果校验失败->返回错误信息(响应401状态码)
- 6.校验通过,放行

cn/zjy/filter/TokenFilter.java
package cn.zjy.filter;
import cn.zjy.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.带正确令牌查询性别统计

③详解(执行流程、拦截路径、过滤器链)
执行流程

问题1:放行后访问对应资源,资源访问完成后,还会回到Filter中吗?会
问题2:如果回到Filter中,是重新执行还是执行放行后的逻辑呢?执行放行后逻辑
// 注释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("拦截到了请求... 放行后...");
}

拦截路径
Filter 可以根据需求,配置不同的拦截资源路径:@WebFilter("/*")
| 拦截路径 | urlPatterns值 | 含义 |
|---|---|---|
| 拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
| 目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
| 拦截所有 | /* | 访问所有资源,都会被拦截 |
过滤器链
- 介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
- 顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。

将DemoFilter.java复制为AbcFilter.java;将AbcFilter改为XyzFilter测试

小结
过滤器的执行流程 ?
- 放行前 -> 放行 -> 资源 -> 放行后
配置的过滤器的拦截路径/ 与 /emps/ 分别代表什么意思 ?
/*:表示拦截所有/emps/*:表示目录拦截,拦截/emps下的所有资源
什么是过滤器链 ?
- 项目中的多个过滤器就形成了一个过滤器链

2.4 拦截器Interceptor
- 快速入门
- 令牌校验Interceptor
- 详解
①快速入门
- 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,主要用来动态拦截控制器方法的执行。
- 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

Interceptor快速入门
- 定义拦截器,实现HandlerInterceptor接口,并实现其所有方法。
- 注册拦截器

cn/zjy/interceptor/DemoInterceptor.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/zjy/config/WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
}
}
在ApiFox中执行:根据ID查询员工信息,控制台显示如下。

小结
拦截器Interceptor的使用步骤 ?
- 定义:实现HandlerInterceptor接口
- preHandle 目标资源方法执行前执行
- postHandle 目标资源方法执行后执行
- afterCompletion 视图渲染完毕后执行,最后执行
- 配置:定义一个配置类实现WebMvcConfigurer接口,注册拦截器(/**)

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

cn/zjy/interceptor/TokenInterceptor.java
package cn.zjy.interceptor;
import cn.zjy.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/zjy/config/WebConfig.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.带正确令牌查询

③详解(拦截路径、执行流程)
拦截路径
拦截器可以根据需求,配置不同的拦截路径:
cn/zjy/config/WebConfig.java
package cn.zjy.config;
import cn.zjy.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,不能匹配 /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 |

执行流程
Filter 与 Interceptor 区别:
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
- 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

验证
打开DemoFilter的@WebFilter设置、关闭其他Filter----注释@WebFilter设置;
cn/zjy/config/WebConfig.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范围

①②③④⑤⑥⑦⑧⑨⑩
HTTP 协议中,与 Cookie 三个自动
在 HTTP 协议中,与 Cookie 相关的 “三个自动” 通常指浏览器或客户端对 Cookie 的三种自动处理机制,这些机制是实现状态管理的核心基础:
一、自动存储(Automatic Storage)
浏览器在接收到服务器通过Set-Cookie响应头发送的 Cookie 时,会自动将其存储在本地缓存中。这一过程无需用户干预,存储位置根据 Cookie 的属性分为两种:
- 内存存储:未设置
Expires或Max-Age的会话 Cookie(Session Cookie),仅在浏览器会话期间存在,关闭浏览器后自动清除。 - 磁盘存储:设置了过期时间的持久化 Cookie(Persistent Cookie),会被写入本地硬盘,在指定时间内有效。
例如,当用户访问电商网站时,服务器返回的Set-Cookie: session_id=abc123会被浏览器自动保存,用于识别用户身份。
二、自动携带(Automatic Inclusion)
在后续所有对同一域名的请求中,浏览器会自动在请求头中附加已存储的 Cookie,无需用户手动添加。这一机制确保服务器能够通过 Cookie 识别用户状态,例如:http
GET /cart HTTP/1.1
Host: example.com
Cookie: session_id=abc123; theme=dark
此过程完全透明,用户无需感知,极大简化了状态管理流程。即使跨页面或跨标签页访问,浏览器也会自动同步 Cookie 信息。
三、自动过期(Automatic Expiration)
浏览器会根据 Cookie 的Expires或Max-Age属性自动处理过期逻辑:
- 时间触发:当 Cookie 的
Expires时间到达或Max-Age倒计时结束,浏览器会自动删除该 Cookie。 - 会话结束:会话 Cookie 在浏览器关闭时自动失效,无需显式清理。
例如,某 Cookie 设置Max-Age=86400(24 小时),浏览器会在 24 小时后自动清除该 Cookie,确保用户数据的时效性和安全性。
扩展机制:自动路径匹配与域限制
除上述核心机制外,浏览器还会自动执行以下策略:
- 路径匹配:仅在请求路径与 Cookie 的
Path属性匹配时携带该 Cookie。例如,Path=/login的 Cookie 仅在访问/login路径时发送。 - 域限制:Cookie 仅在同源请求中自动携带,跨域请求(如
example.com与api.example.com)需通过Domain属性显式授权。
实际应用场景
- 电商购物车:用户添加商品后,服务器通过 Cookie 记录购物车内容,后续访问结算页面时,浏览器自动携带 Cookie,确保购物车数据不丢失。
- 自动登录:用户勾选 “记住我” 后,服务器返回长期有效的 Cookie,浏览器自动存储并在下次访问时携带,实现免密登录。
- 个性化推荐:服务器通过 Cookie 记录用户浏览偏好,后续请求时自动返回定制化内容。
技术实现原理
- Cookie 管理器:浏览器内部的 Cookie 管理器(如 JMeter 中的
HTTP Cookie Manager)负责统一管理存储、携带和过期逻辑。 - 属性控制:服务器通过
Secure(仅 HTTPS 传输)、HttpOnly(防止 XSS 攻击)等属性进一步约束 Cookie 的自动处理行为。
与 HTTP 协议的协同
Cookie 的自动机制与 HTTP 的无状态性形成互补:
- 无状态性:HTTP 默认不保留请求上下文,每次请求独立处理。
- Cookie 补偿:通过自动存储和携带 Cookie,服务器可在多次请求间关联用户状态,实现登录、购物车等复杂功能。
注意事项
- 安全风险:自动携带 Cookie 可能导致跨站请求伪造(CSRF)攻击,需通过
SameSite属性限制第三方站点携带。 - 存储容量限制:单个域名的 Cookie 数量通常限制在 50 个以内,总大小不超过 4KB,超出时浏览器会自动丢弃旧 Cookie。
通过这三个自动机制,Cookie 在无需用户干预的情况下实现了高效的状态管理,成为现代 Web 应用的基础设施之一。理解这些机制有助于开发者优化用户体验、防范安全漏洞,并合理设计 Cookie 策略(如设置Max-Age平衡时效性与性能)
使用 jjwt:0.12.7` 依赖实现 JWT 生成与解析的测试示例
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.7</version>
</dependency
基于 JUnit 5 编写:
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();
}
}
代码说明:
- 依赖适配:针对
0.12.7版本,使用Keys.secretKeyFor()生成符合算法要求的密钥(HS256 需 256 位密钥),避免手动生成密钥可能导致的长度不足问题。 - 核心功能:
- 生成令牌:设置自定义载荷(userId、username 等)、签发时间、过期时间,并使用 HS256 算法签名
- 解析令牌:验证签名合法性、提取载荷信息
- 异常测试:覆盖令牌过期(
ExpiredJwtException)、签名错误(SignatureException)等场景
- 注意事项:
- 密钥(
SECRET_KEY)在实际项目中需安全存储,避免硬编码 - 过期时间根据业务需求调整(示例为 1 小时)
- 0.12.x 版本相比旧版本强化了安全校验,密钥长度和算法必须匹配(如 HS256 对应 256 位密钥)
- 密钥(
运行测试前,需确保添加 JUnit 5 依赖(如 junit-jupiter-api 和 junit-jupiter-engine)。
Spring中,多个 Interceptor 的执行顺序?
在 Spring 中,多个 Interceptor(拦截器)的执行顺序由它们在配置时的注册顺序决定,具体规则如下:
- 注册顺序决定前置方法(preHandle)的执行顺序
当多个拦截器被注册到 Spring 容器时,它们的
preHandle方法会按照注册的先后顺序执行(先注册的先执行)。 - 后置方法(postHandle)和完成方法(afterCompletion)的执行顺序相反
postHandle方法:按照注册的逆序执行(后注册的先执行)。afterCompletion方法:同样按照注册的逆序执行(后注册的先执行)。
- 特殊情况:preHandle 返回 false 时的中断机制
如果某个拦截器的
preHandle方法返回false,则后续所有拦截器的preHandle方法都不会执行,且已执行过preHandle的拦截器会按照逆序执行afterCompletion方法。
示例说明
假设有三个拦截器 InterceptorA、InterceptorB、InterceptorC,注册顺序为 A → B → C,则执行流程如下:
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:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new InterceptorA()); // 先注册
registry.addInterceptor(new InterceptorB()); // 后注册
}
总结:拦截器的执行顺序本质上由注册顺序决定,前置方法正序执行,后置和完成方法逆序执行。