
在 Web 应用开发中,拦截器(Interceptor)和过滤器(Filter)是处理 HTTP 请求与响应的两大核心组件。很多开发者在日常工作中会混用这两个概念,甚至在面试中被问到时只能含糊其辞。实际上,它们虽然都能对请求进行拦截处理,但在归属框架、执行机制、应用场景上存在本质区别。
本文将从 Servlet 规范与 SpringMVC 框架的底层设计出发,结合 JDK 17 与最新稳定版组件,通过可直接运行的实战案例,全面解析拦截器与过滤器的核心差异。无论你是刚入行的初级开发者,还是需要夯实基础的资深工程师,都能通过本文掌握两者的使用场景与最佳实践,轻松应对面试与工作中的实际问题。
要理解拦截器与过滤器的差异,首先需要明确它们的技术归属。这是两者所有区别的根源,也是很多开发者混淆的核心点。
过滤器是Servlet 规范的一部分,属于 Java EE(现 Jakarta EE)标准定义的组件。任何实现 Servlet 规范的容器(如 Tomcat 10、Jetty 12)都必须支持过滤器功能,它不依赖于任何第三方框架。
拦截器则是SpringMVC 框架的特有组件,属于 Spring 生态的一部分。它是 SpringMVC 为了增强控制器(Controller)层处理能力而设计的扩展点,仅在 SpringMVC 环境中生效。
下面通过 架构图直观展示两者在 Web 应用中的位置:

从架构图可以看出:
两者的核心接口直接体现了技术归属的不同,也决定了它们的功能边界。
该接口是 Servlet 规范定义的标准接口,包含三个核心方法:
init(FilterConfig filterConfig):过滤器初始化方法,容器启动时调用doFilter(ServletRequest request, ServletResponse response, FilterChain chain):核心过滤逻辑,处理请求与响应destroy():过滤器销毁方法,容器关闭时调用该接口是 SpringMVC 定义的扩展接口,包含三个核心方法:
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler):控制器执行前调用postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView):控制器执行后、视图渲染前调用afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex):视图渲染后、请求完成前调用两者的执行顺序和生命周期存在显著差异,这直接影响了它们的应用场景。下面通过流程图展示完整的请求响应链路:

从流程图可以总结出关键差异:
过滤器主要用于处理通用的请求与响应逻辑,如字符编码设置、跨域处理、请求日志记录等。下面通过三个典型场景的实战案例,展示过滤器的实现方式。
解决中文乱码问题是 Web 应用的基础需求,通过过滤器可统一设置请求与响应的字符编码。
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.5</version>
</dependency>
</dependencies>
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
/**
* 全局字符编码过滤器
* 统一设置请求与响应的字符编码,解决中文乱码问题
*
* @author ken
*/
@Slf4j
@WebFilter(urlPatterns = "/*", filterName = "encodingFilter")
public class EncodingFilter implements Filter {
/**
* 默认字符编码
*/
private static final String DEFAULT_CHARSET = "UTF-8";
/**
* 字符编码配置参数
*/
private String charset;
/**
* 过滤器初始化方法
* 从配置中获取字符编码,若未配置则使用默认值
*
* @param filterConfig 过滤器配置对象
* @throws ServletException 初始化异常
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("EncodingFilter 初始化开始");
// 获取web.xml或注解配置的字符编码
String configCharset = filterConfig.getInitParameter("charset");
this.charset = StringUtils.hasText(configCharset) ? configCharset : DEFAULT_CHARSET;
log.info("EncodingFilter 初始化完成,字符编码:{}", this.charset);
}
/**
* 核心过滤逻辑
* 设置请求与响应的字符编码,并继续执行过滤链
*
* @param request 请求对象
* @param response 响应对象
* @param chain 过滤链
* @throws IOException IO异常
* @throws ServletException Servlet异常
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
log.debug("处理请求[{}]的字符编码,当前编码:{}", httpRequest.getRequestURI(), this.charset);
// 设置请求编码
if (httpRequest.getCharacterEncoding() == null) {
httpRequest.setCharacterEncoding(this.charset);
}
// 设置响应编码
httpResponse.setCharacterEncoding(this.charset);
httpResponse.setContentType("text/html;charset=" + this.charset);
// 继续执行过滤链
chain.doFilter(httpRequest, httpResponse);
log.debug("请求[{}]字符编码处理完成", httpRequest.getRequestURI());
}
/**
* 过滤器销毁方法
* 释放资源
*/
@Override
public void destroy() {
log.info("EncodingFilter 销毁");
this.charset = null;
}
}
在 Spring Boot 应用中,可通过配置类注册过滤器,替代传统的 web.xml 配置:
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 过滤器配置类
* 注册自定义过滤器并设置优先级
*
* @author ken
*/
@Configuration
public class FilterConfig {
/**
* 注册字符编码过滤器
*
* @return 过滤器注册Bean
*/
@Bean
public FilterRegistrationBean<EncodingFilter> encodingFilterRegistration() {
FilterRegistrationBean<EncodingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new EncodingFilter());
// 设置过滤的URL模式,/*表示所有请求
registrationBean.addUrlPatterns("/*");
// 设置初始化参数
registrationBean.addInitParameter("charset", "UTF-8");
// 设置过滤器名称
registrationBean.setName("encodingFilter");
// 设置优先级,值越小优先级越高
registrationBean.setOrder(1);
return registrationBean;
}
}
编写测试控制器,验证字符编码过滤器是否生效:
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 测试控制器
* 验证过滤器功能
*
* @author ken
*/
@RestController
@Tag(name = "测试接口", description = "用于验证过滤器和拦截器功能的测试接口")
public class TestController {
/**
* 测试字符编码接口
*
* @param name 中文名称参数
* @return 包含中文的响应信息
*/
@GetMapping("/test/encoding")
@Operation(summary = "测试字符编码", description = "验证字符编码过滤器是否解决中文乱码问题")
public String testEncoding(
@Parameter(description = "中文名称", required = true)
@RequestParam String name) {
return "你好," + name + "!字符编码测试成功";
}
}
启动应用后,通过 Swagger(访问http://localhost:8080/swagger-ui.html)测试接口,传入中文参数,若响应正常显示中文,说明过滤器生效。
记录接口的访问日志是排查问题的重要手段,通过过滤器可统一记录所有请求的关键信息。
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 接口访问日志过滤器
* 记录所有接口请求的关键信息,包括请求URL、参数、响应状态等
*
* @author ken
*/
@Slf4j
@WebFilter(urlPatterns = "/api/*", filterName = "accessLogFilter")
public class AccessLogFilter implements Filter {
/**
* 日期时间格式化器
*/
private static final DateTimeFormatter DATETIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("AccessLogFilter 初始化开始");
log.info("AccessLogFilter 初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 记录请求开始时间
long startTime = System.currentTimeMillis();
String requestTime = LocalDateTime.now().format(DATETIME_FORMATTER);
// 获取请求关键信息
String requestUri = httpRequest.getRequestURI();
String httpMethod = httpRequest.getMethod();
String clientIp = getClientIp(httpRequest);
String userAgent = httpRequest.getHeader("User-Agent");
try {
// 继续执行过滤链
chain.doFilter(httpRequest, httpResponse);
} finally {
// 记录请求耗时和响应状态
long costTime = System.currentTimeMillis() - startTime;
int statusCode = httpResponse.getStatus();
// 打印访问日志
log.info("【接口访问日志】请求时间:{},客户端IP:{},请求方法:{},请求URL:{}," +
"用户代理:{},响应状态:{},耗时:{}ms",
requestTime, clientIp, httpMethod, requestUri,
userAgent, statusCode, costTime);
}
}
@Override
public void destroy() {
log.info("AccessLogFilter 销毁");
}
/**
* 获取客户端真实IP地址
* 处理反向代理场景下的IP获取
*
* @param request HTTP请求对象
* @return 客户端真实IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ObjectUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ObjectUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ObjectUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 处理多个IP的情况,取第一个非unknown的IP
if (!ObjectUtils.isEmpty(ip) && ip.contains(",")) {
String[] ipArray = ip.split(",");
for (String tempIp : ipArray) {
if (!"unknown".equalsIgnoreCase(tempIp.trim())) {
ip = tempIp.trim();
break;
}
}
}
return ip;
}
}
在 FilterConfig 类中添加注册方法:
@Bean
public FilterRegistrationBean<AccessLogFilter> accessLogFilterRegistration() {
FilterRegistrationBean<AccessLogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AccessLogFilter());
// 只过滤/api/*路径下的请求
registrationBean.addUrlPatterns("/api/*");
registrationBean.setName("accessLogFilter");
// 优先级低于编码过滤器
registrationBean.setOrder(2);
return registrationBean;
}
编写 API 接口进行测试:
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* API控制器
* 提供业务接口,用于验证访问日志过滤器
*
* @author ken
*/
@RestController
@RequestMapping("/api")
@Tag(name = "API接口", description = "业务接口,用于验证访问日志过滤器")
public class ApiController {
/**
* 测试访问日志接口
*
* @return 接口响应
*/
@PostMapping("/test/log")
@Operation(summary = "测试访问日志", description = "验证访问日志过滤器是否记录请求信息")
public String testAccessLog() {
return "API接口访问成功,日志已记录";
}
}
启动应用后调用接口,控制台会输出类似如下的日志:
【接口访问日志】请求时间:2024-05-20 15:30:45.123,客户端IP:127.0.0.1,请求方法:POST,请求URL:/api/test/log,用户代理:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36,响应状态:200,耗时:15ms
在前后端分离架构中,跨域问题是常见挑战,通过过滤器可统一配置 CORS 规则。
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;
/**
* 跨域资源共享(CORS)过滤器
* 解决前后端分离架构中的跨域访问问题
*
* @author ken
*/
@Slf4j
@WebFilter(urlPatterns = "/*", filterName = "corsFilter")
public class CorsFilter implements Filter {
/**
* 允许的源地址,生产环境应配置具体的前端域名
*/
private static final String ALLOWED_ORIGINS = "*";
/**
* 允许的请求方法
*/
private static final String ALLOWED_METHODS = "GET,POST,PUT,DELETE,OPTIONS";
/**
* 允许的请求头
*/
private static final String ALLOWED_HEADERS = "Origin,X-Requested-With,Content-Type,Accept,Authorization";
/**
* 预检请求的缓存时间(秒)
*/
private static final int MAX_AGE = 3600;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("CorsFilter 初始化开始");
log.info("CorsFilter 初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 设置CORS响应头
httpResponse.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGINS);
httpResponse.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
httpResponse.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS);
httpResponse.setHeader("Access-Control-Max-Age", String.valueOf(MAX_AGE));
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 处理OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
log.debug("处理OPTIONS预检请求:{}", httpRequest.getRequestURI());
httpResponse.setStatus(HttpServletResponse.SC_OK);
return;
}
// 继续执行过滤链
chain.doFilter(httpRequest, httpResponse);
}
@Override
public void destroy() {
log.info("CorsFilter 销毁");
}
}
在 FilterConfig 类中添加注册方法:
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CorsFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("corsFilter");
// 优先级最高,确保跨域处理先执行
registrationBean.setOrder(0);
return registrationBean;
}
使用 Postman 或前端项目测试跨域访问:
若接口能正常访问且无跨域错误,说明过滤器生效。
拦截器主要用于处理与 SpringMVC 控制器相关的业务逻辑,如身份认证、权限检查、业务日志记录等。下面通过三个典型场景的实战案例,展示拦截器的实现方式。
验证用户是否已登录是 Web 应用的常见需求,通过拦截器可统一拦截未登录用户的请求。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
/**
* 用户身份认证拦截器
* 验证用户是否已登录,未登录用户将被重定向到登录页
*
* @author ken
*/
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
/**
* 登录用户在Session中的属性名
*/
private static final String LOGIN_USER = "loginUser";
/**
* 排除拦截的URL(登录、注册、静态资源等)
*/
private static final String[] EXCLUDE_URLS = {
"/login", "/register", "/static/**", "/swagger-ui/**", "/v3/api-docs/**"
};
/**
* 控制器执行前调用
* 验证用户是否已登录,未登录则重定向到登录页
*
* @param request HTTP请求对象
* @param response HTTP响应对象
* @param handler 处理请求的处理器(通常是Controller方法)
* @return true:继续执行;false:中断执行
* @throws IOException IO异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException {
String requestUri = request.getRequestURI();
log.debug("身份认证拦截器处理请求:{}", requestUri);
// 检查是否为排除拦截的URL
if (isExcludeUrl(requestUri)) {
log.debug("请求{}属于排除拦截URL,直接放行", requestUri);
return true;
}
// 获取Session中的登录用户
HttpSession session = request.getSession(false);
Object loginUser = session != null ? session.getAttribute(LOGIN_USER) : null;
// 验证用户是否已登录
if (loginUser == null) {
log.warn("用户未登录,请求{}被拦截", requestUri);
// 重定向到登录页
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
// 用户已登录,继续执行
log.debug("用户已登录,请求{}放行", requestUri);
return true;
}
/**
* 判断请求URL是否为排除拦截的URL
*
* @param requestUri 请求URL
* @return true:排除拦截;false:需要拦截
*/
private boolean isExcludeUrl(String requestUri) {
for (String excludeUrl : EXCLUDE_URLS) {
// 处理通配符匹配(仅支持末尾的**)
if (excludeUrl.endsWith("/**")) {
String prefix = excludeUrl.substring(0, excludeUrl.length() - 2);
if (requestUri.startsWith(prefix)) {
return true;
}
} else if (excludeUrl.equals(requestUri)) {
return true;
}
}
return false;
}
}
通过 SpringMVC 配置类注册拦截器:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* SpringMVC配置类
* 注册自定义拦截器并配置拦截规则
*
* @author ken
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 注册拦截器
*
* @param registry 拦截器注册表
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册身份认证拦截器
registry.addInterceptor(new AuthInterceptor())
// 配置拦截的URL模式
.addPathPatterns("/**")
// 配置排除拦截的URL模式(优先级高于addPathPatterns)
.excludePathPatterns("/login")
.excludePathPatterns("/register")
.excludePathPatterns("/static/**")
.excludePathPatterns("/swagger-ui/**")
.excludePathPatterns("/v3/api-docs/**");
}
}
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录控制器
* 提供用户登录和注销功能,配合身份认证拦截器使用
*
* @author ken
*/
@RestController
@Slf4j
@Tag(name = "登录接口", description = "用户登录和注销接口")
public class LoginController {
/**
* 登录用户在Session中的属性名
*/
private static final String LOGIN_USER = "loginUser";
/**
* 模拟用户登录
* 实际项目中应从数据库查询用户信息并验证密码
*
* @param username 用户名
* @param password 密码
* @param session HTTP会话
* @return 登录结果
*/
@PostMapping("/login")
@Operation(summary = "用户登录", description = "验证用户信息并创建登录会话")
public String login(
@Parameter(description = "用户名", required = true)
@RequestParam String username,
@Parameter(description = "密码", required = true)
@RequestParam String password,
HttpSession session) {
// 验证参数
if (!StringUtils.hasText(username)) {
return "用户名不能为空";
}
if (!StringUtils.hasText(password)) {
return "密码不能为空";
}
// 模拟数据库验证(实际项目中应使用加密密码验证)
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功,将用户信息存入Session
session.setAttribute(LOGIN_USER, username);
log.info("用户{}登录成功", username);
return "登录成功,欢迎您:" + username;
}
// 登录失败
log.warn("用户{}登录失败,密码错误", username);
return "用户名或密码错误";
}
/**
* 用户注销
* 清除Session中的登录用户信息
*
* @param session HTTP会话
* @return 注销结果
*/
@GetMapping("/logout")
@Operation(summary = "用户注销", description = "清除登录会话并退出登录")
public String logout(HttpSession session) {
Object loginUser = session.getAttribute(LOGIN_USER);
if (loginUser != null) {
log.info("用户{}注销登录", loginUser);
// 清除Session中的登录用户信息
session.removeAttribute(LOGIN_USER);
// 可选:使Session失效
session.invalidate();
return "注销成功";
}
return "您尚未登录";
}
/**
* 测试需要登录的接口
*
* @param session HTTP会话
* @return 接口响应
*/
@GetMapping("/user/info")
@Operation(summary = "获取用户信息", description = "需要登录才能访问的接口")
public String getUserInfo(HttpSession session) {
String loginUser = (String) session.getAttribute(LOGIN_USER);
return "当前登录用户:" + loginUser + ",用户信息查询成功";
}
}
/user/info,会被重定向到/login/login接口登录(用户名 admin,密码 123456)/user/info,可正常获取用户信息/logout接口注销后,访问/user/info会再次被拦截在用户已登录的基础上,还需要验证用户是否有访问特定接口的权限,通过拦截器可实现细粒度的权限控制。
首先定义一个用于标记接口所需权限的注解:
import java.lang.annotation.*;
/**
* 权限注解
* 用于标记Controller方法所需的访问权限
*
* @author ken
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequirePermission {
/**
* 所需权限编码
*
* @return 权限编码数组
*/
String[] value();
/**
* 是否需要登录
* 默认需要登录
*
* @return true:需要登录;false:不需要登录
*/
boolean requireLogin() default true;
}
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 接口权限检查拦截器
* 验证登录用户是否有访问特定接口的权限
*
* @author ken
*/
@Slf4j
public class PermissionInterceptor implements HandlerInterceptor {
/**
* 登录用户在Session中的属性名
*/
private static final String LOGIN_USER = "loginUser";
/**
* 排除拦截的URL
*/
private static final String[] EXCLUDE_URLS = {
"/login", "/logout", "/static/**", "/swagger-ui/**", "/v3/api-docs/**"
};
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException {
String requestUri = request.getRequestURI();
log.debug("权限检查拦截器处理请求:{}", requestUri);
// 检查是否为排除拦截的URL
if (isExcludeUrl(requestUri)) {
log.debug("请求{}属于排除拦截URL,直接放行", requestUri);
return true;
}
// 检查处理器是否为Controller方法
if (!(handler instanceof HandlerMethod handlerMethod)) {
log.debug("处理器不是Controller方法,直接放行");
return true;
}
// 获取方法上的权限注解
RequirePermission requirePermission = AnnotationUtils.findAnnotation(
handlerMethod.getMethod(), RequirePermission.class);
// 如果没有权限注解,直接放行(或根据需求改为拦截)
if (requirePermission == null) {
log.debug("接口{}未配置权限要求,直接放行", requestUri);
return true;
}
// 检查是否需要登录
if (requirePermission.requireLogin()) {
HttpSession session = request.getSession(false);
Object loginUser = session != null ? session.getAttribute(LOGIN_USER) : null;
if (loginUser == null) {
log.warn("用户未登录,请求{}被拦截", requestUri);
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}
// 检查用户是否有所需权限
String[] requiredPermissions = requirePermission.value();
if (requiredPermissions.length == 0) {
log.debug("接口{}不需要特定权限,直接放行", requestUri);
return true;
}
// 获取当前用户的权限(实际项目中应从数据库或缓存查询)
String username = (String) request.getSession().getAttribute(LOGIN_USER);
List<String> userPermissions = getUserPermissions(username);
// 检查用户权限是否包含所需权限
boolean hasPermission = Arrays.stream(requiredPermissions)
.anyMatch(permission -> userPermissions.contains(permission));
if (hasPermission) {
log.debug("用户{}拥有访问接口{}的权限,放行", username, requestUri);
return true;
}
// 无权限,返回403
log.warn("用户{}没有访问接口{}的权限,被拦截", username, requestUri);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("您没有访问该接口的权限(403 Forbidden)");
return false;
}
/**
* 获取用户拥有的权限
* 实际项目中应从数据库或缓存查询用户权限
*
* @param username 用户名
* @return 用户权限列表
*/
private List<String> getUserPermissions(String username) {
// 模拟权限数据
if ("admin".equals(username)) {
// 管理员拥有所有权限
return Arrays.asList("user:view", "user:add", "user:edit", "user:delete");
} else if ("user".equals(username)) {
// 普通用户只有查看权限
return Arrays.asList("user:view");
}
// 其他用户无权限
return Collections.emptyList();
}
/**
* 判断请求URL是否为排除拦截的URL
*
* @param requestUri 请求URL
* @return true:排除拦截;false:需要拦截
*/
private boolean isExcludeUrl(String requestUri) {
for (String excludeUrl : EXCLUDE_URLS) {
if (excludeUrl.endsWith("/**")) {
String prefix = excludeUrl.substring(0, excludeUrl.length() - 2);
if (requestUri.startsWith(prefix)) {
return true;
}
} else if (excludeUrl.equals(requestUri)) {
return true;
}
}
return false;
}
}
在 WebMvcConfig 类中添加拦截器注册:
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册身份认证拦截器
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register", "/static/**", "/swagger-ui/**", "/v3/api-docs/**");
// 注册权限检查拦截器
// 注意:权限拦截器应在身份认证拦截器之后执行
registry.addInterceptor(new PermissionInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/static/**", "/swagger-ui/**", "/v3/api-docs/**");
}
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
/**
* 用户管理接口
* 用于验证权限检查拦截器功能
*
* @author ken
*/
@RestController
@RequestMapping("/api/user")
@Tag(name = "用户管理接口", description = "用户查询、添加、编辑、删除接口,包含权限控制")
public class UserController {
/**
* 查看用户列表(需要user:view权限)
*
* @return 用户列表
*/
@GetMapping("/list")
@Operation(summary = "查看用户列表", description = "需要user:view权限")
@RequirePermission("user:view")
public String getUserList() {
return "用户列表:admin、user、test";
}
/**
* 添加用户(需要user:add权限)
*
* @param username 用户名
* @return 添加结果
*/
@PostMapping("/add")
@Operation(summary = "添加用户", description = "需要user:add权限")
@RequirePermission("user:add")
public String addUser(@RequestParam String username) {
return "添加用户" + username + "成功";
}
/**
* 编辑用户(需要user:edit权限)
*
* @param username 用户名
* @return 编辑结果
*/
@PutMapping("/edit")
@Operation(summary = "编辑用户", description = "需要user:edit权限")
@RequirePermission("user:edit")
public String editUser(@RequestParam String username) {
return "编辑用户" + username + "成功";
}
/**
* 删除用户(需要user:delete权限)
*
* @param username 用户名
* @return 删除结果
*/
@DeleteMapping("/delete")
@Operation(summary = "删除用户", description = "需要user:delete权限")
@RequirePermission("user:delete")
public String deleteUser(@RequestParam String username) {
return "删除用户" + username + "成功";
}
}
/api/user/list(user:view 权限):正常访问/api/user/add(user:add 权限):返回 403 无权限记录业务操作日志是审计和排查问题的重要手段,通过拦截器可记录 Controller 方法的调用信息和业务参数。
import java.lang.annotation.*;
/**
* 业务操作日志注解
* 用于标记需要记录业务日志的Controller方法
*
* @author ken
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BusinessLog {
/**
* 操作模块
*
* @return 模块名称
*/
String module();
/**
* 操作描述
*
* @return 操作描述
*/
String description();
/**
* 是否记录请求参数
* 默认记录
*
* @return true:记录;false:不记录
*/
boolean recordParams() default true;
/**
* 是否记录响应结果
* 默认记录
*
* @return true:记录;false:不记录
*/
boolean recordResult() default true;
}
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 业务操作日志拦截器
* 记录Controller方法的业务操作日志,包括操作人、操作时间、参数、结果等
*
* @author ken
*/
@Slf4j
public class BusinessLogInterceptor implements HandlerInterceptor {
/**
* 日期时间格式化器
*/
private static final DateTimeFormatter DATETIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
/**
* 登录用户在Session中的属性名
*/
private static final String LOGIN_USER = "loginUser";
/**
* 请求开始时间(ThreadLocal确保线程安全)
*/
private final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();
/**
* 业务日志上下文(ThreadLocal确保线程安全)
*/
private final ThreadLocal<Map<String, Object>> logContextThreadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 记录请求开始时间
startTimeThreadLocal.set(System.currentTimeMillis());
// 初始化日志上下文
Map<String, Object> logContext = new HashMap<>();
logContext.put("operateTime", LocalDateTime.now().format(DATETIME_FORMATTER));
logContext.put("requestUri", request.getRequestURI());
logContext.put("httpMethod", request.getMethod());
logContext.put("clientIp", getClientIp(request));
// 获取登录用户
Object loginUser = request.getSession(false) != null
? request.getSession(false).getAttribute(LOGIN_USER)
: "anonymous";
logContext.put("operator", loginUser);
// 检查是否为Controller方法
if (handler instanceof HandlerMethod handlerMethod) {
// 获取业务日志注解
BusinessLog businessLog = AnnotationUtils.findAnnotation(
handlerMethod.getMethod(), BusinessLog.class);
if (businessLog != null) {
logContext.put("module", businessLog.module());
logContext.put("description", businessLog.description());
// 记录请求参数(如果需要)
if (businessLog.recordParams()) {
Map<String, String[]> parameterMap = request.getParameterMap();
logContext.put("requestParams", JSON.toJSONString(parameterMap));
}
}
}
logContextThreadLocal.set(logContext);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
// 在视图渲染前执行,可记录ModelAndView信息
Map<String, Object> logContext = logContextThreadLocal.get();
if (logContext != null && modelAndView != null) {
logContext.put("viewName", modelAndView.getViewName());
logContext.put("model", JSON.toJSONString(modelAndView.getModel()));
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
// 请求处理完成后执行,记录响应结果和异常信息
Map<String, Object> logContext = logContextThreadLocal.get();
if (logContext == null) {
return;
}
try {
// 记录耗时
long costTime = System.currentTimeMillis() - startTimeThreadLocal.get();
logContext.put("costTime", costTime + "ms");
// 记录响应状态
logContext.put("responseStatus", response.getStatus());
// 记录异常信息(如果有)
if (ex != null) {
logContext.put("exception", ex.getClass().getName());
logContext.put("exceptionMessage", ex.getMessage());
}
// 打印业务日志(实际项目中应存入数据库)
log.info("【业务操作日志】{}", JSON.toJSONString(logContext, true));
} finally {
// 清除ThreadLocal,避免内存泄漏
startTimeThreadLocal.remove();
logContextThreadLocal.remove();
}
}
/**
* 获取客户端真实IP地址
*
* @param request HTTP请求对象
* @return 客户端真实IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
String[] ipArray = ip.split(",");
for (String tempIp : ipArray) {
if (!"unknown".equalsIgnoreCase(tempIp.trim())) {
ip = tempIp.trim();
break;
}
}
}
return ip;
}
}
在 WebMvcConfig 类中添加拦截器注册:
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册身份认证拦截器
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register", "/static/**", "/swagger-ui/**", "/v3/api-docs/**");
// 注册权限检查拦截器
registry.addInterceptor(new PermissionInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/static/**", "/swagger-ui/**", "/v3/api-docs/**");
// 注册业务日志拦截器
// 注意:业务日志拦截器应在最后执行,确保能记录完整信息
registry.addInterceptor(new BusinessLogInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/static/**", "/swagger-ui/**", "/v3/api-docs/**");
}
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单管理接口
* 用于验证业务日志拦截器功能
*
* @author ken
*/
@RestController
@RequestMapping("/api/order")
@Tag(name = "订单管理接口", description = "订单创建、支付、取消接口,包含业务日志记录")
public class OrderController {
/**
* 创建订单
*
* @param userId 用户ID
* @param productId 商品ID
* @param amount 订单金额
* @return 订单创建结果
*/
@PostMapping("/create")
@Operation(summary = "创建订单", description = "创建新订单并返回订单号")
@BusinessLog(module = "订单管理", description = "创建订单",
recordParams = true, recordResult = true)
@RequirePermission("order:create")
public String createOrder(
@Parameter(description = "用户ID", required = true)
@RequestParam Long userId,
@Parameter(description = "商品ID", required = true)
@RequestParam Long productId,
@Parameter(description = "订单金额", required = true)
@RequestParam BigDecimal amount) {
// 模拟订单创建逻辑
String orderNo = "ORDER" + System.currentTimeMillis();
log.debug("订单创建成功,订单号:{}", orderNo);
return "订单创建成功,订单号:" + orderNo + ",金额:" + amount;
}
/**
* 支付订单
*
* @param orderNo 订单号
* @param payType 支付方式(1:支付宝;2:微信支付)
* @return 支付结果
*/
@PostMapping("/pay")
@Operation(summary = "支付订单", description = "支付指定订单")
@BusinessLog(module = "订单管理", description = "支付订单",
recordParams = true, recordResult = true)
@RequirePermission("order:pay")
public String payOrder(
@Parameter(description = "订单号", required = true)
@RequestParam String orderNo,
@Parameter(description = "支付方式(1:支付宝;2:微信支付)", required = true)
@RequestParam Integer payType) {
String payTypeDesc = payType == 1 ? "支付宝" : "微信支付";
return "订单" + orderNo + "通过" + payTypeDesc + "支付成功";
}
}
使用 admin 用户登录后,调用/api/order/create接口,控制台会输出类似如下的业务日志:
【业务操作日志】{
"operateTime":"2024-05-20 16:45:30.789",
"requestUri":"/api/order/create",
"httpMethod":"POST",
"clientIp":"127.0.0.1",
"operator":"admin",
"module":"订单管理",
"description":"创建订单",
"requestParams":"{\"userId\":[\"1\"],\"productId\":[\"1001\"],\"amount\":[\"99.9\"]}",
"costTime":"25ms",
"responseStatus":200
}
通过前面的底层分析和实战案例,我们已经全面了解了拦截器与过滤器的实现方式。下面从多个维度对两者进行深度对比,帮助你在实际项目中正确选择使用。
对比维度 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
技术归属 | Servlet 规范(Jakarta EE) | SpringMVC 框架 |
依赖环境 | 任何 Servlet 容器(Tomcat、Jetty 等) | 必须在 SpringMVC 环境中 |
处理范围 | 所有 HTTP 请求(包括静态资源) | 仅 SpringMVC 管理的 Controller 请求 |
执行时机 | 请求进入 Servlet 容器后,DispatcherServlet 前 | DispatcherServlet 后,Controller 前 |
核心接口 | javax.servlet.Filter | org.springframework.web.servlet.HandlerInterceptor |
方法数量 | 3 个(init、doFilter、destroy) | 3 个(preHandle、postHandle、afterCompletion) |
参数访问 | 只能访问 ServletRequest 和 ServletResponse | 可访问 Controller 方法、ModelAndView 等 |
配置方式 | web.xml 或 @WebFilter+FilterRegistrationBean | SpringMVC 配置类(WebMvcConfigurer) |
执行顺序 | 由 FilterRegistrationBean 的 order 属性决定 | 由 InterceptorRegistry 的注册顺序决定 |
异常处理 | 需手动捕获异常 | 可在 afterCompletion 中处理异常 |
性能影响 | 较低(Servlet 容器级别的处理) | 略高(Spring 框架级别的处理) |
在实际项目中,如何选择使用过滤器还是拦截器?以下是基于场景的选择指南:
掌握了前面的知识后,我们来看看面试中关于拦截器与过滤器的高频问题,以及如何给出全面准确的回答。
参考答案:拦截器和过滤器的核心区别在于技术归属和处理范围,主要体现在三个方面:
此外,拦截器能访问 Controller 方法和 ModelAndView,支持更细粒度的控制;过滤器只能访问 ServletRequest 和 ServletResponse,适合通用功能处理。
参考答案:实现过滤器的步骤:
实现拦截器的步骤:
参考答案:多个过滤器的执行顺序由 FilterRegistrationBean 的 order 属性决定,order 值越小优先级越高。如果使用 @WebFilter 注解,默认按类名首字母顺序执行,建议显式设置 order。
多个拦截器的执行顺序由 InterceptorRegistry 的注册顺序决定,先注册的拦截器 preHandle 先执行。而 postHandle 和 afterCompletion 则按相反顺序执行,即后注册的先执行。
例如,注册了拦截器 A 和 B(A 先注册):
参考答案:如果拦截器的 preHandle 返回 false,会中断请求处理流程,后续的拦截器和 Controller 都不会执行。
具体流程如下:
这种机制常用于身份认证失败或权限不足的场景,如重定向到登录页或返回 403 错误。
参考答案:建议使用拦截器实现统一响应格式处理,原因如下:
实现步骤:
过滤器不适合该场景,因为过滤器无法方便地访问 Controller 的返回结果,且会处理静态资源请求,增加不必要的开销。
参考答案:建议使用过滤器实现 XSS 攻击防护,原因如下:
实现步骤:
本文从底层逻辑、实战案例、深度对比三个维度,全面解析了拦截器与过滤器的核心差异和应用场景。通过具体的代码实现,展示了如何在实际项目中使用这两个组件解决常见问题。