本文将从零开始详细讲解Spring Boot拦截器的原理和实现,通过逐行代码解析和生动比喻,让您彻底掌握如何使用拦截器实现统一的JWT令牌校验。
一、拦截器在请求处理中的位置与作用
请求处理的完整流程
要理解拦截器的作用,我们首先需要了解一个HTTP请求在Spring Boot应用中的完整处理流程:
客户端请求 → 过滤器(Filter) → 拦截器(Interceptor) → 控制器(Controller) → 服务层(Service) → 数据库
拦截器的位置:位于过滤器和控制器之间,是Spring MVC框架的一部分。
拦截器的作用场景
想象一下公司前台的工作流程:
-
过滤器(Filter):像大楼的保安,检查每个进入大楼的人(检查请求的基本安全性)
-
拦截器(Interceptor):像公司前台,专门处理公司内部事务(检查登录状态、权限等)
-
控制器(Controller):像各个部门,处理具体的业务请求
拦截器的典型应用:
-
身份认证:检查用户是否登录
-
日志记录:记录请求信息和执行时间
-
权限验证:检查用户是否有权限访问某个接口
-
跨域处理:设置跨域相关的响应头
拦截器的工作时机
拦截器有三个重要的执行时机:
-
preHandle:在控制器方法执行之前调用
-
postHandle:在控制器方法执行之后,视图渲染之前调用
-
afterCompletion:在整个请求完成之后调用
对于JWT令牌校验,我们主要使用preHandle方法。
二、JWT拦截器实现详解
创建JWT拦截器类
让我们一步一步创建一个完整的JWT拦截器:
java
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtils jwtUtils;
/**
* 在控制器方法执行前进行拦截
* @return true表示放行,false表示拦截
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 拦截器的具体逻辑将在这里实现
}
}
代码解释:
-
@Component:将这个类声明为Spring组件,让Spring能够自动扫描并管理它 -
@Slf4j:使用Lombok注解,自动提供日志记录功能 -
HandlerInterceptor:Spring提供的拦截器接口,我们需要实现其方法 -
preHandle:最重要的方法,在请求到达控制器前执行
步骤1:获取请求URL
在preHandle方法中,我们首先需要获取当前请求的URL:
java
// 步骤1:获取请求URL
String requestURL = request.getRequestURL().toString();
log.info("拦截到请求: {}", requestURL);
为什么要获取URL?
因为我们需要判断当前请求是否需要JWT校验。比如登录接口就不需要校验令牌。
代码解释:
-
request.getRequestURL():获取完整的请求URL -
toString():将StringBuffer转换为字符串 -
log.info():记录日志,便于调试和监控
步骤2:判断是否为登录请求
接下来,我们需要检查当前请求是否是登录请求:
python
// 步骤2:判断请求URL中是否包含login,如果包含,说明是登录操作,放行
if (requestURL.contains("/login")) {
log.info("登录请求,直接放行");
return true; // true表示放行,不再执行后续拦截逻辑
}
为什么登录请求要特殊处理?
因为用户还没有登录,自然没有JWT令牌。如果对登录请求也要求令牌,就会陷入"先有鸡还是先有蛋"的死循环。
代码解释:
-
contains("/login"):检查URL中是否包含"/login"路径 -
return true:放行请求,让请求继续到达控制器
步骤3:获取请求头中的令牌
对于非登录请求,我们需要从请求头中获取JWT令牌:
python
// 步骤3:获取请求头中的令牌(token)
String token = request.getHeader("Authorization");
log.info("获取到的令牌: {}", token);
为什么令牌放在请求头中?
-
符合HTTP标准,专门用于身份认证
-
避免在URL中暴露敏感信息
-
支持各种类型的HTTP请求(GET、POST等)
代码解释:
-
request.getHeader("Authorization"):从请求头中获取名为"Authorization"的值 -
通常令牌的格式是:"Bearer 实际的令牌字符串",这里我们先获取整个值
步骤4:检查令牌是否存在
获取到令牌后,我们需要检查它是否存在:
java
// 步骤4:判断令牌是否存在,如果不存在,响应401
if (token == null || token.isEmpty()) {
log.warn("令牌为空,用户未登录,响应401状态码");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("请先登录");
return false; // false表示拦截,不再继续执行
}
为什么返回401状态码?
401状态码表示"未授权",准确地表达了"用户需要登录但未提供有效凭证"的情况。
代码解释:
-
token == null || token.isEmpty():检查令牌是否为null或空字符串 -
response.setStatus(401):设置HTTP响应状态码为401 -
response.getWriter().write():返回具体的错误信息给客户端 -
return false:拦截请求,不再继续向下执行
步骤5:解析和验证令牌
如果令牌存在,我们需要验证它的有效性:
java
// 步骤5:如果token存在,校验令牌
try {
// 提取真正的令牌(去掉"Bearer "前缀)
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 使用JWT工具类解析令牌
Claims claims = JwtUtils.parseToken(token);
log.info("令牌解析成功,用户信息: {}", claims);
} catch (Exception e) {
log.warn("令牌解析失败: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌无效或已过期");
return false; // 拦截请求
}
为什么要去掉"Bearer "前缀?
这是标准的JWT使用规范,"Bearer "表示这是一个持有者令牌。
代码解释:
-
token.startsWith("Bearer "):检查令牌是否以"Bearer "开头 -
token.substring(7):去掉前7个字符("Bearer "的长度) -
JwtUtils.parseToken(token):调用之前编写的JWT工具类解析令牌 -
如果解析失败会抛出异常,我们在catch块中处理这种错误情况
步骤6:将用户信息存入请求
令牌验证通过后,我们可以将用户信息存储起来,供控制器使用:
java
// 步骤6:将用户信息存入请求属性,供Controller使用
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("username", claims.get("username"));
log.info("用户信息已存入请求属性,用户ID: {}", claims.get("userId"));
为什么要存储用户信息?
这样在控制器中就可以直接获取当前登录用户的信息,无需重复解析令牌。
代码解释:
-
request.setAttribute():将数据存储在请求属性中 -
这些数据在当前请求的整个生命周期内都可用
-
控制器可以通过
@RequestAttribute注解获取这些值
步骤7:放行请求
所有检查都通过后,最后一步是放行请求:
java
// 步骤7:所有校验通过,放行
log.info("令牌校验通过,放行请求");
return true; // true表示放行,继续执行后续流程
三、完整的JWT拦截器代码
现在让我们把所有的代码片段整合起来,形成完整的拦截器:
java
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 步骤1:获取请求URL
String requestURL = request.getRequestURL().toString();
log.info("拦截到请求: {}", requestURL);
// 步骤2:判断是否为登录请求
if (requestURL.contains("/login")) {
log.info("登录请求,直接放行");
return true;
}
// 步骤3:获取请求头中的令牌
String token = request.getHeader("Authorization");
log.info("获取到的令牌: {}", token);
// 步骤4:检查令牌是否存在
if (token == null || token.isEmpty()) {
log.warn("令牌为空,用户未登录,响应401状态码");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("请先登录");
return false;
}
// 步骤5:解析和验证令牌
try {
// 提取真正的令牌
if (token.startsWith("Bearer ")) {
token = token.substring(7);
}
Claims claims = JwtUtils.parseToken(token);
log.info("令牌解析成功,用户信息: {}", claims);
// 步骤6:将用户信息存入请求
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("username", claims.get("username"));
} catch (Exception e) {
log.warn("令牌解析失败: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("令牌无效或已过期");
return false;
}
// 步骤7:放行请求
log.info("令牌校验通过,放行请求");
return true;
}
}
四、配置拦截器
创建好拦截器后,我们需要告诉Spring Boot在什么情况下使用这个拦截器。
创建配置类
python
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置将在这里编写
}
}
代码解释:
-
@Configuration:声明这是一个配置类 -
WebMvcConfigurer:Spring MVC配置接口,可以自定义MVC相关配置 -
@Autowired:自动注入我们之前创建的JWT拦截器
配置拦截规则
在addInterceptors方法中配置具体的拦截规则:
java
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login"); // 排除登录请求
}
配置解释:
拦截所有请求 (addPathPatterns("/**")):
-
/**是Ant风格的路径模式,表示匹配所有路径 -
这意味着除了明确排除的路径,所有请求都会经过拦截器
排除登录请求 (excludePathPatterns("/login")):
-
登录接口不需要JWT令牌验证
-
可以排除多个路径,用逗号分隔:
excludePathPatterns("/login", "/register")
完整的配置类代码
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login"); // 排除登录请求
}
}
五、在控制器中使用用户信息
拦截器验证通过后,我们可以在控制器中直接使用存储的用户信息:
java
@RestController
public class UserController {
@GetMapping("/user/profile")
public ResponseEntity<UserProfile> getUserProfile(
@RequestAttribute("userId") Long userId,
@RequestAttribute("username") String username) {
// 直接使用拦截器解析的用户信息
UserProfile profile = userService.getUserProfile(userId);
return ResponseEntity.ok(profile);
}
}
代码解释:
-
@RequestAttribute("userId"):从请求属性中获取userId -
@RequestAttribute("username"):从请求属性中获取username -
这样就不需要在每个控制器方法中重复解析JWT令牌了
六、拦截器的工作流程总结
让我们通过一个具体的请求例子来理解整个流程:
场景1:用户登录请求
java
请求:POST http://localhost:8080/login
拦截器处理流程:
1. 获取URL:http://localhost:8080/login
2. 判断包含"/login" → 是登录请求
3. 直接放行,不检查令牌
4. 请求到达登录控制器,验证用户名密码
5. 登录成功,生成并返回JWT令牌
场景2:用户访问个人资料
java
请求:GET http://localhost:8080/user/profile
请求头:Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
拦截器处理流程:
1. 获取URL:http://localhost:8080/user/profile
2. 判断不包含"/login" → 不是登录请求
3. 从请求头获取令牌
4. 令牌存在,进行解析验证
5. 验证通过,将用户信息存入请求属性
6. 放行请求,到达用户资料控制器
7. 控制器从请求属性获取用户信息,返回用户资料
场景3:未登录用户访问受保护接口
java
请求:GET http://localhost:8080/user/profile
请求头:无Authorization头
拦截器处理流程:
1. 获取URL:http://localhost:8080/user/profile
2. 判断不是登录请求
3. 从请求头获取令牌 → 令牌为null
4. 返回401状态码,拦截请求
5. 请求不会到达控制器
七、总结
拦截器的核心点
通过本文的介绍,我们可以看到拦截器在Spring Boot项目中的重要作用:
-
统一处理:在单个位置处理所有请求的认证逻辑
-
代码复用:避免在每个控制器中重复编写认证代码
-
职责分离:认证逻辑与业务逻辑分离,代码更清晰
-
灵活配置:可以精确控制哪些请求需要拦截,哪些需要放行
关键技术点回顾
-
preHandle方法:在控制器执行前进行拦截认证
-
JWT令牌验证:统一验证用户身份
-
请求属性:在拦截器和控制器间传递数据
-
配置注册:通过WebMvcConfigurer配置拦截规则
若您对JWT令牌的生成与解析尚不熟悉,可通过此链接查阅JWT令牌的详细解析说明。
若您对前端Vue项目中使用Axios拦截器实现JWT令牌认证有疑惑,可通过此链接查阅详细解析说明。
觉得本文有帮助?点赞收藏支持一下!有任何异常处理的问题,欢迎在评论区留言讨论~