JWT-过滤器-拦截器-全局异常处理重点总结

登录认证 · JWT · 过滤器 · 拦截器 · 全局异常处理 · 重点总结

一、登录认证整体流程

1.1 核心思路

步骤 操作
① 用户登录 浏览器发起 POST /login 请求,携带用户名和密码
② 服务端校验 根据用户名和密码查询数据库,校验通过则生成 JWT 令牌返回给前端
③ 前端存储令牌 前端将 JWT 令牌存入 localStorage(浏览器本地存储)
④ 后续请求携带令牌 每次请求自动在 请求头 token 中携带 JWT
⑤ 服务端拦截校验 用过滤器或拦截器统一拦截,校验令牌有效性,无效则返回 NOT_LOGIN 错误

1.2 HTTP 协议无状态特性

HTTP 是无状态协议:每次请求独立,下次请求不会自动携带上次的数据。所以需要会话跟踪技术才能识别用户登录状态。


二、会话跟踪三种技术对比

方案 数据位置 优点 缺点
Cookie 客户端浏览器 HTTP 协议原生支持,自动携带 移动端不支持、可被禁用、不能跨域、不安全
Session 服务端 数据安全(存服务端) 集群环境下 Session 不共享、底层依赖 Cookie、移动端不支持
令牌(JWT) 客户端任意位置 支持 PC/移动端、可跨域、解决集群问题、减轻服务器存储压力 需要自己实现生成、传递、校验

企业开发首选:令牌技术(JWT)

1
2
3
4
5
6
7
// Cookie
response.addCookie(new Cookie("login_username", "itheima"));
Cookie[] cookies = request.getCookies();

// Session(底层基于 Cookie 的 JSESSIONID)
session.setAttribute("loginUser", "tom");
Object user = session.getAttribute("loginUser");

三、JWT 令牌

3.1 核心概念

项目 说明
全称 JSON Web Token
官网 https://jwt.io/
本质 一个简洁的字符串(三部分用 . 分隔)
特点 简洁(字符串可在请求头/参数传递)、自包含(可存储自定义数据)

3.2 JWT 三部分组成

组成 内容 示例
Header(头) 令牌类型、签名算法 {"alg":"HS256","typ":"JWT"}
Payload(载荷) 自定义数据(用户 ID、名称、过期时间等) {"id":1,"username":"Tom","exp":1672729730}
Signature(签名) 由 Header + Payload + 密钥计算出的签名,防止令牌被篡改 加密后的字符串

Header 和 Payload 都是 Base64 编码(可直接解码出来),签名部分不是编码而是签名算法计算结果。
篡改任何一个字符都会导致校验失败 → JWT 是安全可靠的。

3.3 引入 JWT 依赖

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

3.4 生成与解析 JWT

操作 方法
生成 JWT Jwts.builder().setClaims(map).signWith(算法, 密钥).setExpiration(过期时间).compact()
解析 JWT Jwts.parser().setSigningKey(密钥).parseClaimsJws(令牌).getBody()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 生成 JWT
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "Tom");

String jwt = Jwts.builder()
.setClaims(claims) // 自定义数据
.signWith(SignatureAlgorithm.HS256, "itheima") // 签名算法 + 密钥
.setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) // 1小时过期
.compact();
// 输出:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJUb20iLCJleHAiOi4uLn0.xxx

// 解析 JWT(密钥必须与生成时一致)
Claims claims = Jwts.parser()
.setSigningKey("itheima")
.parseClaimsJws(jwt)
.getBody();
System.out.println(claims.get("id")); // 1
System.out.println(claims.get("username")); // Tom

关键规则

  • 解析时使用的密钥必须与生成时一致
  • 令牌过期或被篡改 → 解析时抛异常
  • 密钥长度建议 ≥ 32 字节(256 比特)

3.5 JWT 工具类(Spring 管理 + 配置文件)

application.yml:

1
2
3
4
# 自定义 JWT 配置
myjwt:
key: sdjfkgsdsfgyusadgfklsay78657iusfdgu98dstfoghkjsdtgf # 至少32字节
time: 86400000 # 24小时(毫秒)

JWT 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class MyJwtUtils {

@Value("${myjwt.key}")
private String signKey;

@Value("${myjwt.time}")
private Long expire;

/** 生成 JWT 令牌 */
public String generateJwt(Map<String, Object> claims) {
return Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
}

/** 解析 JWT 令牌 */
public Claims parseJWT(String jwt) {
return Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
}
}

3.6 登录接口生成令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
public class LoginController {

@Autowired
private EmpService empService;

@PostMapping("/login")
public Result login(@RequestBody LoginDto dto) {
String token = empService.login(dto);
if (token != null) {
return Result.success(token); // 登录成功,返回令牌
}
return Result.error("NOT_LOGIN"); // 登录失败
}
}

@Service
public class EmpServiceImpl implements EmpService {

@Autowired private EmpMapper empMapper;
@Autowired private MyJwtUtils myJwtUtils;

@Override
public String login(LoginDto dto) {
Emp emp = empMapper.selectEmpByName(dto.getUsername());
if (emp == null) return null;
if (emp.getPassword().equals(dto.getPassword())) {
// 登录成功,生成令牌
Map<String, Object> map = Map.of("id", emp.getId(), "name", emp.getName());
return myJwtUtils.generateJwt(map);
}
return null;
}
}

四、Filter 过滤器

4.1 核心概念

项目 说明
定义 JavaWeb 三大组件(Servlet、Filter、Listener)之一
作用 拦截浏览器请求,做统一处理(登录校验、编码处理、敏感字符过滤等)
特点 必须先经过滤器,才能访问 Web 资源

4.2 三大生命周期方法

方法 调用时机 调用次数
init() Web 服务器启动时(创建对象时) 1 次
doFilter() 每次请求到达时 多次(每次请求一次)
destroy() 服务器关闭时(销毁对象时) 1 次

4.3 定义 Filter 三步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第①步:定义类,实现 Filter 接口
@WebFilter(urlPatterns = "/*") // 第②步:配置拦截路径
public class MyFilter implements Filter {

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
System.out.println("放行前逻辑...");

chain.doFilter(req, resp); // ⚠️ 关键:放行请求!不调用此行无法访问后续资源

System.out.println("放行后逻辑...");
}
}
1
2
3
4
5
6
7
8
// 第③步:启动类加 @ServletComponentScan,开启 Servlet 组件支持
@ServletComponentScan
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

4.4 拦截路径配置

拦截路径 urlPatterns 值 含义
拦截具体路径 /login 仅访问 /login 才被拦截
目录拦截 /emps/* 访问 /emps 下所有资源被拦截
拦截所有 /* 所有请求都被拦截(最常用)

4.5 过滤器执行流程

1
2
3
浏览器请求 → Filter 放行前逻辑 → chain.doFilter() → Web 资源(Controller)

浏览器响应 ← Filter 放行后逻辑 ← ────────────────────────

4.6 过滤器链

一个项目可定义多个 Filter,形成过滤器链

规则 说明
执行顺序 类名首字母排序(A 比 X 先执行)
放行前逻辑 正向执行:1 → 2 → 3
放行后逻辑 反向执行:3 → 2 → 1

4.7 登录校验过滤器完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

@Resource
private MyJwtUtils myJwtUtils;

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

// ① 获取请求路径
String url = request.getRequestURL().toString();

// ② 登录请求直接放行
if (url.contains("/login")) {
chain.doFilter(req, resp);
return;
}

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

// ④ 令牌不存在 → 返回 NOT_LOGIN
if (!StringUtils.hasLength(token)) {
returnError(response);
return;
}

// ⑤ 解析令牌(失败抛异常)
try {
myJwtUtils.parseJWT(token);
} catch (Exception e) {
log.info("令牌解析失败");
returnError(response);
return;
}

// ⑥ 令牌有效,放行
chain.doFilter(req, resp);
}

private void returnError(HttpServletResponse response) throws IOException {
String json = JSON.toJSONString(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json);
}
}

五、Interceptor 拦截器

5.1 核心概念

项目 说明
定义 Spring 框架提供的,动态拦截 Controller 方法执行的机制
类似 类似过滤器,但只拦截 Spring 环境内的资源
接口 实现 HandlerInterceptor 接口

5.2 三大生命周期方法

方法 调用时机 返回值含义
preHandle() Controller 方法执行前 true = 放行;false = 不放行
postHandle() Controller 方法执行,视图渲染
afterCompletion() 视图渲染(最后执行)

5.3 定义 + 注册拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 第①步:自定义拦截器类,实现 HandlerInterceptor 接口
@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 获取请求头中的令牌
String token = request.getHeader("token");

// 令牌不存在 → 返回 NOT_LOGIN
if (!StringUtils.hasLength(token)) {
returnError(response);
return false; // 不放行
}

// 解析令牌
try {
JwtUtils.parseJWT(token);
} catch (Exception e) {
returnError(response);
return false;
}

return true; // 放行
}

private void returnError(HttpServletResponse response) throws IOException {
String json = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第②步:注册拦截器(实现 WebMvcConfigurer,配置类)
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login"); // 排除登录请求
}
}

5.4 拦截路径写法

拦截路径 含义 示例匹配
/* 一级路径(不含子路径) 匹配 /depts/login;不匹配 /depts/1
/** 任意级路径(含子路径) 匹配 /depts/depts/1/depts/1/2
/depts/* /depts 下的一级路径 匹配 /depts/1;不匹配 /depts/1/2
/depts/** /depts 下的任意级路径 匹配 /depts/depts/1/depts/1/2

六、Filter vs Interceptor(⭐ 常考)

对比项 Filter(过滤器) Interceptor(拦截器)
接口规范 Filter(JavaWeb 规范) HandlerInterceptor(Spring 提供)
拦截范围 所有资源(包括静态资源) 仅 Spring 环境的资源(Controller 方法)
触发顺序 在 DispatcherServlet 之前触发 在 DispatcherServlet 之后、Controller 之前触发
配置方式 @WebFilter + 启动类 @ServletComponentScan 实现 WebMvcConfigurer 注册
使用场景 通用过滤(编码、压缩、跨域) 业务相关(登录校验、权限校验)

6.1 完整执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
浏览器请求

[Filter 放行前逻辑] ←—— Filter 拦截所有资源

DispatcherServlet(前端控制器)

[Interceptor preHandle()] ←—— Interceptor 只拦截 Spring 资源

Controller 方法

[Interceptor postHandle()]

视图渲染

[Interceptor afterCompletion()]

DispatcherServlet

[Filter 放行后逻辑]

浏览器响应

七、ThreadLocal 保存用户信息

7.1 应用场景

拦截器解析 JWT 后获取的用户信息,需要在后续 Controller/Service 中使用,但不便每个方法都传参 → 使用 ThreadLocal 存储。

1
2
3
4
5
6
7
public class MyThreadLocalUtils {
private static final ThreadLocal<Emp> threadLocal = new ThreadLocal<>();

public static void set(Emp emp) { threadLocal.set(emp); }
public static Emp get() { return threadLocal.get(); }
public static void remove() { threadLocal.remove(); } // ⚠️ 必须remove,否则内存泄漏
}
1
2
3
4
5
6
7
8
// 拦截器中:解析令牌 → 存入 ThreadLocal → 放行
Claims claims = myJwtUtils.parseJWT(token);
Emp emp = new Emp(claims.get("id", Integer.class), claims.get("name", String.class));
MyThreadLocalUtils.set(emp);

chain.doFilter(req, resp); // 放行

MyThreadLocalUtils.remove(); // 业务结束后清理
1
2
// Controller/Service 中:直接获取当前登录用户
Emp currentUser = MyThreadLocalUtils.get();

八、全局异常处理

8.1 当前问题

三层架构中任意一层抛异常 → 默认抛给框架 → 返回的 JSON 不符合统一响应规范 → 前端无法解析。

8.2 解决方案对比

方案 评价
每个 Controller 方法都 try-catch ❌ 代码臃肿,不推荐
全局异常处理器 ✅ 简单优雅,推荐

8.3 全局异常处理器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestControllerAdvice  // 标识全局异常处理类(= @ControllerAdvice + @ResponseBody)
public class GlobalExceptionHandler {

// 处理运行时异常
@ExceptionHandler(RuntimeException.class)
public Result doRuntimeException(RuntimeException e) {
e.printStackTrace();
return Result.error("亲,您的操作有问题,请重试...");
}

// 处理其他所有异常(兜底)
@ExceptionHandler(Exception.class)
public Result doException(Exception e) {
e.printStackTrace();
return Result.error("亲,服务器正在升级,请稍后重试...");
}
}

8.4 关键注解

注解 作用
@RestControllerAdvice 标识当前类为全局异常处理器(= @ControllerAdvice + @ResponseBody,返回值自动转 JSON)
@ExceptionHandler(异常类型.class) 指定该方法处理哪种类型的异常

九、综合速查

核心注解一览

注解 用途
@Value("${xxx}") 在 Spring Bean 中读取配置文件的值
@Resource(name="bean名") 按名称注入 Bean(vs @Autowired 按类型注入)
@WebFilter(urlPatterns="...") 配置过滤器拦截路径
@ServletComponentScan 启动类加,开启 Servlet 组件扫描
@Configuration 配置类专用
@RestControllerAdvice 全局异常处理类
@ExceptionHandler(异常.class) 处理指定类型异常的方法

登录认证完整流程

1
2
3
4
5
6
7
8
9
10
11
1. 用户登录                  → POST /login(用户名密码)
2. 服务端校验 + 生成 JWT → 返回 JWT 给前端
3. 前端存储 JWT → localStorage
4. 后续每次请求携带 JWT → 请求头 token
5. Filter/Interceptor 拦截 → 校验 JWT
- 登录请求 → 直接放行
- 无 token → 返回 NOT_LOGIN
- 解析失败 → 返回 NOT_LOGIN
- 解析成功 → 放行(可选:存入 ThreadLocal)
6. 业务处理 → 返回数据
7. 全局异常处理 → 统一捕获异常并返回标准格式

Filter vs Interceptor 速记口诀

1
2
Filter   → JavaWeb 规范,拦截所有资源,先于 DispatcherServlet
Inter... → Spring 提供,仅拦 Controller,preHandle 返 true 才放行

三种会话技术速记

1
2
3
Cookie   → 客户端存储,不安全、不跨域、移动端不支持
Session → 服务端存储,集群下不共享、依赖 Cookie
JWT → 字符串令牌,跨平台/跨域/集群通用 ⭐