Spring Boot拦截器详解:实现统一的JWT认证

本文将从零开始详细讲解Spring Boot拦截器的原理和实现,通过逐行代码解析和生动比喻,让您彻底掌握如何使用拦截器实现统一的JWT令牌校验。

一、拦截器在请求处理中的位置与作用

请求处理的完整流程

要理解拦截器的作用,我们首先需要了解一个HTTP请求在Spring Boot应用中的完整处理流程:

复制代码
客户端请求 → 过滤器(Filter) → 拦截器(Interceptor) → 控制器(Controller) → 服务层(Service) → 数据库

拦截器的位置:位于过滤器和控制器之间,是Spring MVC框架的一部分。

拦截器的作用场景

想象一下公司前台的工作流程:

  • 过滤器(Filter):像大楼的保安,检查每个进入大楼的人(检查请求的基本安全性)

  • 拦截器(Interceptor):像公司前台,专门处理公司内部事务(检查登录状态、权限等)

  • 控制器(Controller):像各个部门,处理具体的业务请求

拦截器的典型应用

  • 身份认证:检查用户是否登录

  • 日志记录:记录请求信息和执行时间

  • 权限验证:检查用户是否有权限访问某个接口

  • 跨域处理:设置跨域相关的响应头

拦截器的工作时机

拦截器有三个重要的执行时机:

  1. preHandle:在控制器方法执行之前调用

  2. postHandle:在控制器方法执行之后,视图渲染之前调用

  3. 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项目中的重要作用:

  1. 统一处理:在单个位置处理所有请求的认证逻辑

  2. 代码复用:避免在每个控制器中重复编写认证代码

  3. 职责分离:认证逻辑与业务逻辑分离,代码更清晰

  4. 灵活配置:可以精确控制哪些请求需要拦截,哪些需要放行

关键技术点回顾

  • preHandle方法:在控制器执行前进行拦截认证

  • JWT令牌验证:统一验证用户身份

  • 请求属性:在拦截器和控制器间传递数据

  • 配置注册:通过WebMvcConfigurer配置拦截规则

若您对JWT令牌的生成与解析尚不熟悉,可通过此链接查阅JWT令牌的详细解析说明。

若您对前端Vue项目中使用Axios拦截器实现JWT令牌认证有疑惑,可通过此链接查阅详细解析说明。

觉得本文有帮助?点赞收藏支持一下!有任何异常处理的问题,欢迎在评论区留言讨论~

相关推荐
Zender Han2 小时前
Flutter 新版 Google Sign-In 插件完整解析(含示例讲解)
android·flutter·ios·web
Gerardisite4 小时前
如何在微信个人号开发中有效管理API接口?
java·开发语言·python·微信·php
q***69774 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
闲人编程5 小时前
Python的导入系统:模块查找、加载和缓存机制
java·python·缓存·加载器·codecapsule·查找器
故渊ZY5 小时前
Java 代理模式:从原理到实战的全方位解析
java·开发语言·架构
匿者 衍5 小时前
POI读取 excel 嵌入式图片(支持wps 和 office)
java·excel
一个尚在学习的计算机小白5 小时前
java集合
java·开发语言
IUGEI5 小时前
synchronized的工作机制是怎样的?深入解析synchronized底层原理
java·开发语言·后端·c#
q***13615 小时前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat