首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >拦截器与过滤器:从底层逻辑到实战案例,一篇讲透 Web 请求处理核心

拦截器与过滤器:从底层逻辑到实战案例,一篇讲透 Web 请求处理核心

作者头像
果酱带你啃java
发布2026-04-14 10:39:49
发布2026-04-14 10:39:49
530
举报

在 Web 应用开发中,拦截器(Interceptor)和过滤器(Filter)是处理 HTTP 请求与响应的两大核心组件。很多开发者在日常工作中会混用这两个概念,甚至在面试中被问到时只能含糊其辞。实际上,它们虽然都能对请求进行拦截处理,但在归属框架、执行机制、应用场景上存在本质区别。

本文将从 Servlet 规范与 SpringMVC 框架的底层设计出发,结合 JDK 17 与最新稳定版组件,通过可直接运行的实战案例,全面解析拦截器与过滤器的核心差异。无论你是刚入行的初级开发者,还是需要夯实基础的资深工程师,都能通过本文掌握两者的使用场景与最佳实践,轻松应对面试与工作中的实际问题。

一、底层逻辑:从规范与框架看两者本质区别

要理解拦截器与过滤器的差异,首先需要明确它们的技术归属。这是两者所有区别的根源,也是很多开发者混淆的核心点。

1.1 技术归属:Servlet 规范 vs SpringMVC 框架

过滤器是Servlet 规范的一部分,属于 Java EE(现 Jakarta EE)标准定义的组件。任何实现 Servlet 规范的容器(如 Tomcat 10、Jetty 12)都必须支持过滤器功能,它不依赖于任何第三方框架。

拦截器则是SpringMVC 框架的特有组件,属于 Spring 生态的一部分。它是 SpringMVC 为了增强控制器(Controller)层处理能力而设计的扩展点,仅在 SpringMVC 环境中生效。

下面通过 架构图直观展示两者在 Web 应用中的位置:

从架构图可以看出:

  • 过滤器先于拦截器执行,处于 Servlet 容器层面
  • 拦截器处于 SpringMVC 内部,围绕控制器展开处理
  • 响应处理时,拦截器先于过滤器执行(与请求方向相反)

1.2 核心接口:规范定义的本质差异

两者的核心接口直接体现了技术归属的不同,也决定了它们的功能边界。

过滤器核心接口:javax.servlet.Filter

该接口是 Servlet 规范定义的标准接口,包含三个核心方法:

  • init(FilterConfig filterConfig):过滤器初始化方法,容器启动时调用
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain):核心过滤逻辑,处理请求与响应
  • destroy():过滤器销毁方法,容器关闭时调用
拦截器核心接口:org.springframework.web.servlet.HandlerInterceptor

该接口是 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):视图渲染后、请求完成前调用

1.3 执行生命周期:请求响应链路的差异

两者的执行顺序和生命周期存在显著差异,这直接影响了它们的应用场景。下面通过流程图展示完整的请求响应链路:

从流程图可以总结出关键差异:

  1. 执行顺序:请求阶段过滤器先执行,响应阶段拦截器先执行
  2. 执行粒度:过滤器基于 URL 模式匹配,拦截器可精确到 Controller 方法
  3. 生命周期:过滤器由 Servlet 容器管理,拦截器由 Spring 容器管理

二、实战案例:过滤器的实现与应用场景

过滤器主要用于处理通用的请求与响应逻辑,如字符编码设置、跨域处理、请求日志记录等。下面通过三个典型场景的实战案例,展示过滤器的实现方式。

2.1 场景一:全局字符编码过滤器

解决中文乱码问题是 Web 应用的基础需求,通过过滤器可统一设置请求与响应的字符编码。

1. 依赖配置(pom.xml)
代码语言:javascript
复制
<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>
代码语言:javascript
复制

2. 过滤器实现
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

3. 过滤器注册(Spring Boot 方式)

在 Spring Boot 应用中,可通过配置类注册过滤器,替代传统的 web.xml 配置:

代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

4. 测试验证

编写测试控制器,验证字符编码过滤器是否生效:

代码语言:javascript
复制
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 + "!字符编码测试成功";
    }
}
代码语言:javascript
复制

启动应用后,通过 Swagger(访问http://localhost:8080/swagger-ui.html)测试接口,传入中文参数,若响应正常显示中文,说明过滤器生效。

2.2 场景二:接口访问日志过滤器

记录接口的访问日志是排查问题的重要手段,通过过滤器可统一记录所有请求的关键信息。

1. 过滤器实现
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

2. 注册过滤器

在 FilterConfig 类中添加注册方法:

代码语言:javascript
复制
@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;
}
代码语言:javascript
复制

3. 测试验证

编写 API 接口进行测试:

代码语言:javascript
复制
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接口访问成功,日志已记录";
    }
}
代码语言:javascript
复制

启动应用后调用接口,控制台会输出类似如下的日志:

代码语言:javascript
复制
【接口访问日志】请求时间: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
代码语言:javascript
复制

2.3 场景三:跨域资源共享(CORS)过滤器

在前后端分离架构中,跨域问题是常见挑战,通过过滤器可统一配置 CORS 规则。

1. 过滤器实现
代码语言:javascript
复制
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 销毁");
    }
}
代码语言:javascript
复制

2. 注册过滤器

在 FilterConfig 类中添加注册方法:

代码语言:javascript
复制
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
    FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new CorsFilter());
    registrationBean.addUrlPatterns("/*");
    registrationBean.setName("corsFilter");
    // 优先级最高,确保跨域处理先执行
    registrationBean.setOrder(0);
    return registrationBean;
}
代码语言:javascript
复制

3. 测试验证

使用 Postman 或前端项目测试跨域访问:

  1. 从不同域名的前端项目调用后端接口
  2. 查看浏览器控制台是否有跨域错误
  3. 通过浏览器开发者工具的 Network 面板,查看响应头是否包含 CORS 相关字段

若接口能正常访问且无跨域错误,说明过滤器生效。

三、实战案例:拦截器的实现与应用场景

拦截器主要用于处理与 SpringMVC 控制器相关的业务逻辑,如身份认证、权限检查、业务日志记录等。下面通过三个典型场景的实战案例,展示拦截器的实现方式。

3.1 场景一:用户身份认证拦截器

验证用户是否已登录是 Web 应用的常见需求,通过拦截器可统一拦截未登录用户的请求。

1. 拦截器实现
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

2. 拦截器注册

通过 SpringMVC 配置类注册拦截器:

代码语言:javascript
复制
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/**");
    }
}
代码语言:javascript
复制

3. 配套登录功能实现
代码语言:javascript
复制
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 + ",用户信息查询成功";
    }
}
代码语言:javascript
复制

4. 测试验证
  1. 未登录状态下访问/user/info,会被重定向到/login
  2. 调用/login接口登录(用户名 admin,密码 123456)
  3. 再次访问/user/info,可正常获取用户信息
  4. 调用/logout接口注销后,访问/user/info会再次被拦截

3.2 场景二:接口权限检查拦截器

在用户已登录的基础上,还需要验证用户是否有访问特定接口的权限,通过拦截器可实现细粒度的权限控制。

1. 权限注解定义

首先定义一个用于标记接口所需权限的注解:

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

2. 拦截器实现
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

3. 注册拦截器

在 WebMvcConfig 类中添加拦截器注册:

代码语言:javascript
复制
@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/**");
}
代码语言:javascript
复制

4. 权限控制接口实现
代码语言:javascript
复制
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 + "成功";
    }
}
代码语言:javascript
复制

5. 测试验证
  1. 使用 admin 用户(拥有所有权限)登录,可正常访问所有接口
  2. 创建普通用户 user(密码 123456),登录后:
    • 访问/api/user/list(user:view 权限):正常访问
    • 访问/api/user/add(user:add 权限):返回 403 无权限
  3. 未登录状态下访问任何接口:被重定向到登录页

3.3 场景三:业务操作日志拦截器

记录业务操作日志是审计和排查问题的重要手段,通过拦截器可记录 Controller 方法的调用信息和业务参数。

1. 业务日志注解定义
代码语言:javascript
复制
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;
}
代码语言:javascript
复制

2. 拦截器实现
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制

3. 注册拦截器

在 WebMvcConfig 类中添加拦截器注册:

代码语言:javascript
复制
@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/**");
}
代码语言:javascript
复制

4. 业务接口实现
代码语言:javascript
复制
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 + "支付成功";
    }
}
代码语言:javascript
复制

5. 测试验证

使用 admin 用户登录后,调用/api/order/create接口,控制台会输出类似如下的业务日志:

代码语言:javascript
复制
【业务操作日志】{
    "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
}
代码语言:javascript
复制

四、深度对比:拦截器与过滤器的核心差异总结

通过前面的底层分析和实战案例,我们已经全面了解了拦截器与过滤器的实现方式。下面从多个维度对两者进行深度对比,帮助你在实际项目中正确选择使用。

4.1 核心差异对比表

对比维度

过滤器(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 框架级别的处理)

4.2 应用场景选择指南

在实际项目中,如何选择使用过滤器还是拦截器?以下是基于场景的选择指南:

选择过滤器的场景
  • 通用请求处理:需要对所有请求(包括静态资源)进行处理的场景,如字符编码设置、CORS 跨域处理。
  • Servlet 容器级功能:依赖 Servlet 容器特性的功能,如请求压缩、URL 重写。
  • 非业务相关功能:与具体业务逻辑无关的通用功能,如全局请求日志记录、XSS 攻击防护。
选择拦截器的场景
  • SpringMVC 业务处理:与 Controller 紧密相关的业务功能,如用户身份认证、接口权限检查。
  • 细粒度控制:需要精确控制到 Controller 方法的场景,如基于注解的权限控制。
  • 业务日志记录:需要记录业务操作详情的场景,如操作人、操作参数、业务结果。
  • 请求响应增强:需要修改 ModelAndView 或响应结果的场景,如统一响应格式处理。

4.3 常见误区与最佳实践

常见误区
  1. 认为拦截器可以替代过滤器:拦截器无法处理静态资源请求,也不能在 Servlet 容器级别工作。
  2. 在过滤器中处理业务逻辑:过滤器应专注于通用功能,业务逻辑应放在拦截器或 Controller 中。
  3. 忽视执行顺序配置:多个过滤器或拦截器的执行顺序会影响功能正确性,需合理设置 order。
  4. ThreadLocal 未清理:在拦截器中使用 ThreadLocal 时,必须在 afterCompletion 中清理,避免内存泄漏。
最佳实践
  1. 分层处理:过滤器处理容器级通用功能,拦截器处理框架级业务功能,Controller 处理具体业务逻辑。
  2. 合理配置顺序:
    • 过滤器:CORS 过滤器(最高)→ 编码过滤器 → 日志过滤器 → 其他过滤器
    • 拦截器:身份认证拦截器 → 权限检查拦截器 → 业务日志拦截器(最低)
  3. 使用注解驱动:Spring Boot 中优先使用注解 + 配置类的方式,替代传统的 web.xml 配置。
  4. 统一异常处理:在拦截器的 afterCompletion 中处理全局异常,避免在多个地方重复处理。
  5. 性能优化:过滤器和拦截器应避免复杂计算,对高频请求可考虑缓存结果。

五、面试高频问题与参考答案

掌握了前面的知识后,我们来看看面试中关于拦截器与过滤器的高频问题,以及如何给出全面准确的回答。

5.1 基础问题

问题 1:拦截器和过滤器有什么区别?

参考答案:拦截器和过滤器的核心区别在于技术归属和处理范围,主要体现在三个方面:

  1. 技术归属不同:过滤器是 Servlet 规范的一部分,依赖 Servlet 容器;拦截器是 SpringMVC 框架的组件,依赖 Spring 环境。
  2. 处理范围不同:过滤器处理所有 HTTP 请求,包括静态资源;拦截器只处理 SpringMVC 管理的 Controller 请求。
  3. 执行时机不同:请求阶段过滤器先执行(Servlet 容器级别),拦截器后执行(SpringMVC 框架级别);响应阶段则相反。

此外,拦截器能访问 Controller 方法和 ModelAndView,支持更细粒度的控制;过滤器只能访问 ServletRequest 和 ServletResponse,适合通用功能处理。

问题 2:如何实现一个过滤器 / 拦截器?

参考答案:实现过滤器的步骤:

  1. 实现 javax.servlet.Filter 接口,重写 init、doFilter、destroy 方法。
  2. 在 doFilter 方法中编写过滤逻辑,处理请求与响应。
  3. 通过 @WebFilter 注解或 FilterRegistrationBean 注册过滤器,配置 URL 模式和执行顺序。

实现拦截器的步骤:

  1. 实现 org.springframework.web.servlet.HandlerInterceptor 接口,重写 preHandle、postHandle、afterCompletion 方法。
  2. 在 preHandle 中编写前置处理逻辑(如认证、权限检查),postHandle 中处理 Controller 执行后逻辑,afterCompletion 中处理请求完成后逻辑。
  3. 通过 WebMvcConfigurer 的 addInterceptors 方法注册拦截器,配置拦截和排除的 URL 模式。

5.2 进阶问题

问题 3:多个过滤器和拦截器同时存在时,执行顺序如何确定?

参考答案:多个过滤器的执行顺序由 FilterRegistrationBean 的 order 属性决定,order 值越小优先级越高。如果使用 @WebFilter 注解,默认按类名首字母顺序执行,建议显式设置 order。

多个拦截器的执行顺序由 InterceptorRegistry 的注册顺序决定,先注册的拦截器 preHandle 先执行。而 postHandle 和 afterCompletion 则按相反顺序执行,即后注册的先执行。

例如,注册了拦截器 A 和 B(A 先注册):

  • preHandle:A → B
  • postHandle:B → A
  • afterCompletion:B → A
问题 4:拦截器的 preHandle 返回 false 会发生什么?

参考答案:如果拦截器的 preHandle 返回 false,会中断请求处理流程,后续的拦截器和 Controller 都不会执行。

具体流程如下:

  1. 当前拦截器的 preHandle 返回 false。
  2. SpringMVC 会立即停止请求处理,不会执行后续拦截器的 preHandle。
  3. 已经执行过 preHandle 且返回 true 的拦截器,会按相反顺序执行 afterCompletion 方法。
  4. 最终直接返回响应,不会执行 Controller 和视图渲染。

这种机制常用于身份认证失败或权限不足的场景,如重定向到登录页或返回 403 错误。

5.3 实战问题

问题 5:如何统一处理接口的响应格式?用过滤器还是拦截器实现?

参考答案:建议使用拦截器实现统一响应格式处理,原因如下:

  1. 拦截器能访问 Controller 的返回结果(ModelAndView 或响应体),便于修改响应内容。
  2. 拦截器只处理 Controller 请求,不会影响静态资源等非业务请求。

实现步骤:

  1. 定义统一的响应格式类,如 ResultDTO(包含 code、message、data 字段)。
  2. 实现拦截器,在 postHandle 方法中:
    • 如果返回的是 ModelAndView,修改其模型数据。
    • 如果是 @ResponseBody 接口,通过 ResponseBodyAdvice 增强响应体,将结果包装为 ResultDTO。
  3. 注册拦截器,配置需要统一处理的 URL 模式。

过滤器不适合该场景,因为过滤器无法方便地访问 Controller 的返回结果,且会处理静态资源请求,增加不必要的开销。

问题 6:如何防止 XSS 攻击?用过滤器还是拦截器实现?

参考答案:建议使用过滤器实现 XSS 攻击防护,原因如下:

  1. XSS 攻击可能通过任何 HTTP 请求提交恶意脚本,包括静态资源请求,过滤器能覆盖所有请求。
  2. 过滤器在请求进入 SpringMVC 前处理,能更早地净化请求参数,避免恶意脚本进入业务逻辑。

实现步骤:

  1. 实现过滤器,重写 doFilter 方法。
  2. 自定义 HttpServletRequestWrapper,重写 getParameter、getParameterValues 等方法,对请求参数进行 XSS 过滤(如转义特殊字符)。
  3. 在 doFilter 中用自定义的 RequestWrapper 包装原始请求,传递给后续处理。
  4. 注册过滤器,设置 URL 模式为 /*,确保所有请求都经过过滤。

六、总结与扩展

本文从底层逻辑、实战案例、深度对比三个维度,全面解析了拦截器与过滤器的核心差异和应用场景。通过具体的代码实现,展示了如何在实际项目中使用这两个组件解决常见问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、底层逻辑:从规范与框架看两者本质区别
    • 1.1 技术归属:Servlet 规范 vs SpringMVC 框架
    • 1.2 核心接口:规范定义的本质差异
      • 过滤器核心接口:javax.servlet.Filter
      • 拦截器核心接口:org.springframework.web.servlet.HandlerInterceptor
    • 1.3 执行生命周期:请求响应链路的差异
  • 二、实战案例:过滤器的实现与应用场景
    • 2.1 场景一:全局字符编码过滤器
      • 1. 依赖配置(pom.xml)
      • 2. 过滤器实现
      • 3. 过滤器注册(Spring Boot 方式)
      • 4. 测试验证
    • 2.2 场景二:接口访问日志过滤器
      • 1. 过滤器实现
      • 2. 注册过滤器
      • 3. 测试验证
    • 2.3 场景三:跨域资源共享(CORS)过滤器
      • 1. 过滤器实现
      • 2. 注册过滤器
      • 3. 测试验证
  • 三、实战案例:拦截器的实现与应用场景
    • 3.1 场景一:用户身份认证拦截器
      • 1. 拦截器实现
      • 2. 拦截器注册
      • 3. 配套登录功能实现
      • 4. 测试验证
    • 3.2 场景二:接口权限检查拦截器
      • 1. 权限注解定义
      • 2. 拦截器实现
      • 3. 注册拦截器
      • 4. 权限控制接口实现
      • 5. 测试验证
    • 3.3 场景三:业务操作日志拦截器
      • 1. 业务日志注解定义
      • 2. 拦截器实现
      • 3. 注册拦截器
      • 4. 业务接口实现
      • 5. 测试验证
  • 四、深度对比:拦截器与过滤器的核心差异总结
    • 4.1 核心差异对比表
    • 4.2 应用场景选择指南
      • 选择过滤器的场景
      • 选择拦截器的场景
    • 4.3 常见误区与最佳实践
      • 常见误区
      • 最佳实践
  • 五、面试高频问题与参考答案
    • 5.1 基础问题
      • 问题 1:拦截器和过滤器有什么区别?
      • 问题 2:如何实现一个过滤器 / 拦截器?
    • 5.2 进阶问题
      • 问题 3:多个过滤器和拦截器同时存在时,执行顺序如何确定?
      • 问题 4:拦截器的 preHandle 返回 false 会发生什么?
    • 5.3 实战问题
      • 问题 5:如何统一处理接口的响应格式?用过滤器还是拦截器实现?
      • 问题 6:如何防止 XSS 攻击?用过滤器还是拦截器实现?
  • 六、总结与扩展
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档