【JavaEE24-后端部分】 从“手动锁门”到“保安统一站岗”:Spring Boot 拦截器轻松搞定登录校验

开篇:图书系统那些年,我们手动"锁门"的日子

还记得我们之前一起做的图书管理系统吗?从登录、增删改查到分页、批量删除,咱们一步步把它搭起来了。功能齐全,页面漂亮,但有一个问题一直让人心里不踏实:只要知道网址,不登录也能访问图书列表,甚至还能添加、修改、删除图书。

于是我们赶紧给系统加上了"强制登录"功能,在每个接口里手动检查 Session:

java 复制代码
if (session.getAttribute("session_user_key") == null) {
    return Result.unlogin();
}
UserInfo userInfo = (UserInfo) session.getAttribute("session_user_key");
if (userInfo == null || userInfo.getId() < 0 || "".equals(userInfo.getUserName())) {
    return Result.unlogin();
}

结果呢?每个接口都要写这么一大段。如果有几十个接口,岂不是要复制粘贴几十遍?要是以后登录校验规则变了,又得满世界改。这就像家里的每一扇门都要单独上锁,每次出门都得一个个检查,累不累?

有没有一种办法,只在门口设一个保安,让他检查所有人的证件,有证就放行,没证就拦住?这就是我们今天要学的------Spring Boot 拦截器


一、保安来了:什么是拦截器?

拦截器是 Spring 框架提供的一个功能,它可以在请求到达 Controller 之前Controller 执行之后整个请求结束之后,执行一些预先定义的代码。

用生活例子来理解:你去银行办业务。

  • 进门前(preHandle) :保安会检查你有没有带身份证,没带?对不起,不能进去。这一步可以直接拦下请求
  • 办理业务(Controller):柜员帮你处理核心业务,比如存钱、转账。
  • 业务办完,还没给你回执单(postHandle) :柜员在给你的回执单上加盖公章、添加备注 。这一步能修改 Controller 返回的结果,然后再把最终结果给你。
  • 你彻底离开银行(afterCompletion) :大堂经理清理窗口、记录日志。无论前面是否出错,这一步最后一定执行

拦截器就是这样的"保安",它在请求处理的不同阶段介入,完成一些通用任务。三个方法各司其职:

  • preHandle:能不能进?(有权拒绝)
  • postHandle:返回前改结果(能修改返回值)
  • afterCompletion:最后扫尾(一定执行)

二、两步走:定义保安 + 签合同上岗

使用拦截器只需要做两件事:定义注册

什么意思呢?假如我学校要找保安,那么首先你得有保安呀,你得有这个人呀,这个就叫做定义,那么定义完了之后就直接不管了吗?肯定不是呀,你得签合同上岗呀,这个就叫做注册,所以拦截器其实就很简单,就两个操作:定义+注册。


1. 定义拦截器 = 写好保安的工作手册

你写一个类实现 HandlerInterceptor,重写那三个方法:

  • 进门查什么
  • 办完改什么
  • 走后清理什么

这就叫:告诉保安要干什么

2. 注册拦截器 = 给保安安排岗位

在配置类里加 addInterceptors,写:

  • 哪些路径要拦(/、/admin/
  • 哪些路径放行(/login、/static)

这就叫:告诉保安,你的技能用在谁身上


一句话总结

  • 定义:造保安、教保安干活
  • 注册:安排保安站哪个门、查哪些人

2.1 第一步:定义保安(写拦截器类)

我们通常将拦截器类(保安这个人)放在interceptor包下:

然后比如我们将他命名为:LoginInterceptor,然后让它实现 HandlerInterceptor (拦截器处理者)接口,并重写它的三个方法。这个类就是我们的"保安",我们要在这里告诉他该干什么活。

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从 Session 中获取用户信息(不创建新的 Session)
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("session_user_key") != null) {
            log.info("用户已登录,放行");
            return true;
        }
        log.warn("用户未登录,拦截请求");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 这里可以修改 Controller 返回的数据(比如给返回结果统一加个字段)
        // 但现在我们不需要,先留着
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 这里可以记录请求耗时、清理资源
        // 但现在我们不需要,先留着
    }
}

我们在拦截器中验证之后就不需要在控制层接口中验证了


注意 :我们重点关注 preHandle 方法,因为它是在请求进入 Controller 之前执行的。我们在这里检查 Session 里有没有用户信息,有就返回 true 放行,没有就返回 false 拦截,并返回 401 状态码。这就是保安的"查证"工作。

那么我们定义了保安告诉他干什么活,而这些活在谁身上干?他不知道,那么我们怎么告诉他?得签合同呀,合同上有说明。也就是说呀,你保安会对进学校的人进行拦截操作,基本流程都是刷卡检查,但是是针对每个人吗?肯定不是呀,比如校长你拦他干什么,对吧。

2.2 第二步:签合同上岗(注册拦截器)

保安定义好了,不能光站在那儿,还得让他正式上岗。怎么上岗?签合同。就像学校跟保安公司签合同,合同里写明:保安在哪儿执勤、拦截什么人、放行什么人等等。签了合同,保安才能名正言顺地干活。不同的地方合同内容不一样,但流程都是"先定义,再注册"。

在我们代码里,这个"签合同"的过程就是把写好的拦截器注册到一个配置类 中,这个配置类要实现 WebMvcConfigurer 接口(Spring 给我们规定好的"合同模板")。你只需要在 config 包下新建一个类,把下面的代码复制进去,改改拦截的路径就行了,不用纠结原理。


在里面新建一个类webconfig

java 复制代码
package com.zhongge.config;

import com.zhongge.interceptor.LoginInterceptor;
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;

/**
 * @ClassName WebConfig
 * @Description TODO 注册拦截器类
 * @Author 笨忠
 * @Date 2026-04-01 20:20
 * @Version 1.0
 */
@Configuration//将这个类交给Spring管理
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor; // 你定义的保安

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)       // 让这个保安上岗(添加拦截器)
                .addPathPatterns("/**")                 // 让他守所有门
                .excludePathPatterns("/user/login")    // 但登录这个门例外
                .excludePathPatterns("/**/*.js")       // 静态资源也不拦
                .excludePathPatterns("/**/*.css")
                .excludePathPatterns("/**/*.png")
                .excludePathPatterns("/**/*.html");  // 所有HTML页面都放行
    }
}
  • addPathPatterns("/**") 就是告诉保安:你要拦截所有请求。/** 是通配符,匹配所有路径。
  • excludePathPatterns("/user/login") 是告诉保安:这个请求不要拦截,因为登录时还没有 Session,不能拦。
  • 静态资源(JS、CSS、图片)也不需要拦截,所以也排除。

瞧,就像签了合同,保安正式上岗!我们再也不用在每个 Controller 里手动写 Session 校验了。

那么此时我们先不返回状态码,然后看拦截之后返回的状态码是什么?


使用postman请求图书列表之后 看他的响应效果:

那么我们再使用fiddler抓一下包 看他返回的状态码:


我们返回的状态码是200呀,那么此时你都被拦截了,代表你这个请求没有连接上,此时为了方便前端判断,我们应该手动将状态码设置为401:



三、请求的"闯关之旅":从进门到出门,保安都在做什么?

让我们把自己想象成一个请求,从浏览器出发,看看我们到底经历了什么。

3.1 出发:我(请求)要去找图书数据

我在浏览器里被创建,目标地址是 /book/getListByPage。我带着你的指令,飞向服务器。服务器里有个总调度员,叫 DispatcherServlet,它负责接待所有请求。

3.2 第一关:保安们排成一排,检查我的证件

我一到服务器,总调度员就把我交给了一排保安。保安们按顺序检查我,每个保安都会执行 preHandle 方法。

java 复制代码
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    HttpSession session = request.getSession(false);
    if (session != null && session.getAttribute("session_user_key") != null) {
        return true; // 有证,放行
    }
    response.setStatus(401); // 没证,返回 401 状态码
    return false; // 拦截,不让进
}

如果某个保安说"不行",我就立刻被拦下,后面的保安和 Controller 都见不到,直接被赶回浏览器。浏览器收到 401 状态码,就会跳转到登录页。

如果所有保安都放行,我才能继续往前走。

解释返回false和返回true的作用:


3.3 第二关:见到 Controller,办理业务

穿过保安队伍,我终于见到了 Controller 大哥。他帮我执行真正的业务代码,比如从数据库查图书列表,然后把数据打包好交给我。

java 复制代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
    PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
    return Result.success(pageResult);
}

Controller 执行完后,我就带着数据往回走。

3.4 第三关:返回路上,保安再次出现

往回走时,我又遇到了保安们。这次他们执行 postHandle,可以在我返回之前做点事情。最重要的是,他们可以修改我手里的数据 ,就像银行柜员在给你回执单之前,可以加盖公章、添加备注。这就是为什么 postHandle 能修改 Controller 返回的结果。

java 复制代码
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    log.info("业务处理完毕,准备返回数据");
    // 这里可以修改 modelAndView,改变最终返回的内容
}

然后,数据被发送到浏览器。

3.5 第四关:客人走后,保安打扫卫生

最后,当整个响应已经发送给浏览器后,保安们执行 afterCompletion,可以做最后的清理工作,比如关闭资源、记录总耗时。这一步无论前面是否出错,都会执行

java 复制代码
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    log.info("整个请求处理完毕,收工");
}

就这样,我的"闯关之旅"结束了。


四、保安的"执勤范围":拦截路径详解

刚才我们用了 /**,它代表拦截所有请求。Spring 支持多种路径匹配规则,你可以根据需要灵活配置。这就好比保安的合同里规定了他负责哪些区域:

路径 含义 例子
/* 一级路径 匹配 /user/book,不匹配 /user/login
/** 任意级路径 匹配 /user/user/login/book/addBook
/book/* /book 下的一级路径 匹配 /book/addBook,不匹配 /book/addBook/1
/book/** /book 下的任意级路径 匹配 /book/book/addBook/book/addBook/1

比如,你只想拦截 /book 开头的请求,可以写成 addPathPatterns("/book/**")。如果你只想拦截一级路径,可以写成 addPathPatterns("/*")

排除路径也一样灵活。你可以把登录接口、注册接口、静态资源等都排除掉,就像保安可以放行一些特殊人员。


五、改造图书系统:删除重复代码,清爽回归

现在,我们可以把之前图书系统里所有接口的 Session 校验代码删掉了。

改造前(图书列表接口):

java 复制代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {
    if (session.getAttribute(Constants.SESSION_USER_KEY) == null) {
        return Result.unlogin();
    }
    UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
    if (userInfo == null || userInfo.getId() < 0 || "".equals(userInfo.getUserName())) {
        return Result.unlogin();
    }
    PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
    return Result.success(pageResult);
}

改造后:

java 复制代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
    PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
    return Result.success(pageResult);
}

六、前端配合:统一处理 401 状态码

保安拦截时返回 401 状态码,前端可以统一处理,比如用 jQuery 的全局 AJAX 配置:

javascript 复制代码
$.ajaxSetup({
    statusCode: {
        401: function() {
            location.href = "login.html";
        }
    }
});

或者单独弄

js 复制代码
 error: function () {
	 //如果请求连接失败就会走这里
	 console.error("加载图书数据失败");
	 alert("未登录,请先登录!");
	 // 没登录,直接撵到登录页
	 location.href = "login.html";
	 return;
}

这样,任何请求被拦截,都会自动跳转到登录页,用户完全无感知。你不需要在每个 AJAX 回调里重复写跳转逻辑。


七、源码浅析:Spring 是如何调用保安的?

可能有人会好奇,Spring 到底是怎么让保安们按顺序工作的?我们来简单看一下核心类 DispatcherServletdoDispatch 方法(简化版)。看不懂也没关系,有个印象就行。

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    // 1. 拿到执行链,里面包含了拦截器列表
    HandlerExecutionChain mappedHandler = getHandler(request);

    // 2. 执行所有拦截器的 preHandle
    if (!mappedHandler.applyPreHandle(request, response)) {
        return; // 如果有一个拦截器返回 false,就停止,后面的都不执行
    }

    // 3. 执行 Controller 方法
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());

    // 4. 执行所有拦截器的 postHandle
    mappedHandler.applyPostHandle(request, response, mv);

    // 5. 处理视图渲染
    processDispatchResult(request, response, mappedHandler, mv, dispatchException);
}

applyPreHandle 中,会依次调用每个拦截器的 preHandle

java 复制代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) {
    for (int i = 0; i < this.interceptorList.size(); i++) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        if (!interceptor.preHandle(request, response, this.handler)) {
            triggerAfterCompletion(request, response, null);
            return false;
        }
    }
    return true;
}

所以,请求的路径是:拦截器 preHandle → Controller → 拦截器 postHandle → 返回。如果某个 preHandle 返回 false,后面的所有步骤都不会执行。这正是我们想要的登录校验效果。


1、 Tomcat里面可以部署多个web项目。我们访问项目的路径是: context path/servlet path

  • context path 上下文路径,它用于决定项目,比如它是用来区分你是博客项目,还是我们的图书管理系统项目?
  • servlet path他是决定当前项目中的路径的。

2、观察日志可知:context path 是 /,原因是因为当前的Tomcat它只部署了一个项目


3、访问我们的登录页面,然后再观察日志:

我们本次的主角就是dispatcherServlet


4、 看dispatcherServlet源码

看下述的继承体系






5、Servlet的生命周期

服务器启动的时候执行init的方法[只执行一次],用接口的时候走service方法[可能执行很多遍],关闭服务的时候执行destroy方法[只执行一次]。




我们的DispatcherServlet中没有Iinit方法,我的初始化方法在哪呢?在我们的父类中:

你会发现这个方法是空的,然后由它的子类来实现,这样的设计模式叫做模板方法模式。

就是说父类定义了,没有实现,由子类来实现。

然后你看他子类实现这个方法的时候,那有一些日志,这些日志不就是我们控制台上的日志吗?

阅读源码的时候ctrl+alt+←退回方法。


接下来看Service方法


DispatcherServlet中的Service方法:


接下来我们核心看doDispatch这个方法

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Determine handler adapter and invoke the handler.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new ServletException("Handler dispatch failed: " + err, err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new ServletException("Handler processing failed: " + err, err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
				asyncManager.setMultipartRequestParsed(multipartRequestParsed);
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

通过debug的方式去看

请求:

处理器的链接执行了这个就执行下一个。







执行拦截器,我们就是在下述这里执行的,由我们的适配器来执行目标方法。

这个就是我们拦截器的执行原理逻辑。


八、小彩蛋:适配器模式------保安与不同客户的沟通

接下来我们就看下面这行代码,它设计了一个的设计模式叫做适配器模式:

在看源码时,你可能注意到 HandlerAdapter 这个词。它其实是适配器模式的一个应用。简单说,就是让不同种类的 Controller 都能被统一处理。

比如,有的 Controller 是普通的 @Controller,有的可能是实现 HttpRequestHandler 接口的类,有的可能是 Servlet。它们长得不一样,但 Spring 想用统一的方式调用它们。HandlerAdapter 就像个万能转接头,把不同的接口转成统一的调用方式。

生活中,适配器也很常见:手机充电器、插头转换器、网线转接头......都是让不兼容的东西能一起工作。


九、总结:有了保安,代码更清爽

今天,我们跟随一个请求的视角,完整了解了 Spring Boot 拦截器的工作流程。我们用保安类比拦截器,用银行办业务类比请求处理过程,轻松理解了拦截器的概念、使用步骤和执行流程。

我们学会了:

  1. 拦截器可以在请求进入 Controller 之前、之后、完成后介入。
  2. 使用拦截器只需要两步:定义 (写一个类实现 HandlerInterceptor)和注册(在配置类中注册并配置拦截路径)。
  3. 配置拦截路径 addPathPatterns 和排除路径 excludePathPatterns,exclude翻译为不包括,就像给保安规定执勤范围和特殊通道。
  4. 拦截器按顺序执行,一旦 preHandle 返回 false,请求立即终止。
  5. 用拦截器改造图书系统的强制登录,删除了大量重复代码,前端用全局配置处理 401 状态码。

现在,我们的图书系统终于有了一个专业的保安,再也不用自己一个个锁门了!


十、伏笔:统一返回格式和统一异常处理

虽然拦截器解决了登录校验的统一问题,但你可能还会问:我们每个接口返回的数据格式不统一怎么办?异常处理也要每个方法写一遍吗?

别急,接下来我们就学习 Spring Boot 的统一返回格式统一异常处理,让我们的代码更规范、更优雅。敬请期待下一期!

最后,老铁们,如果你觉得这篇文章对你有帮助,别忘了👍点赞⭐ 收藏👀 关注,🦀🦀各位老铁的支持~~

相关推荐
玛卡巴卡ldf3 小时前
【Springboot5】审批流工作流引擎(业务、审批分离)排除if else
java·springboot
真心喜欢你吖3 小时前
OpenClaw安装部署Mac操作系统版 - 打造你的专属AI助理
java·人工智能·macos·ai·语言模型·智能体·openclaw
LSL666_3 小时前
JVM面试题——垃圾收集器
java·jvm·面试·垃圾收集器
Via_Neo3 小时前
今天是周六,两天后是周几?
java·数据结构·算法
星晨雪海3 小时前
Redis-逻辑查询详情讲解
java·开发语言
chools3 小时前
Java后端拥抱AI开发之个人学习路线 - - Spring AI【第二期】
java·人工智能·学习·spring·ai
uNke DEPH3 小时前
MySQL中常见函数
java
大鹏说大话3 小时前
Java线程池调优实战:从核心参数到避坑指南
java·开发语言
※DX3906※4 小时前
SpringBoot之旅5| 快速上手SpringAOP、深入刨析动态/静态两种代理模式
java·数据库·spring boot·后端·spring·java-ee·代理模式