3-2 拦截器与过滤器

3 阅读4分钟

3-2 拦截器与过滤器

概念解析

Filter vs Interceptor vs AOP

组件执行位置作用范围实现方式
FilterServlet 容器容器层面Servlet API
InterceptorSpring MVCController 层Spring 框架
AOPSpring IoCService/RepositorySpring AOP

执行顺序

请求 → Filter1 → Filter2 → DispatcherServlet → Interceptor1 → Interceptor2 → Controller
                        ↓
                    执行 Controller
                        ↓
响应 ← Filter1 ← Filter2 ← DispatcherServlet ← Interceptor1 ← Interceptor2 ←

代码示例

1. Filter(过滤器)

// 方式一:注解方式(Spring Boot 3.x)
@WebFilter(urlPatterns = "/api/*", filterName = "myFilter")
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter 初始化");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        // 前置处理
        long start = System.currentTimeMillis();

        // 放行
        chain.doFilter(request, response);

        // 后置处理
        long cost = System.currentTimeMillis() - start;
        log.info("请求 {} 耗时 {}ms", req.getRequestURI(), cost);
    }

    @Override
    public void destroy() {
        log.info("MyFilter 销毁");
    }
}

// 方式二:配置类方式(Spring Boot 3.x 推荐)
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
        FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new MyFilter());
        registration.addUrlPatterns("/api/*");
        registration.setOrder(1);  // 执行顺序,数字越小越先执行
        return registration;
    }
}

2. HandlerInterceptor(拦截器)

@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws Exception {
        // 前置处理:返回 true 放行,false 拦截
        String token = request.getHeader("Authorization");

        if (token == null || !validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
            return false;
        }

        // 将用户信息存入请求属性
        request.setAttribute("userId", getUserId(token));
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                          HttpServletResponse response,
                          Object handler,
                          ModelAndView modelAndView) throws Exception {
        // 后置处理:Controller 执行后,视图渲染前
        log.info("postHandle: {}", request.getRequestURI());
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        // 完成后处理:请求完成后(无论成功失败)
        if (ex != null) {
            log.error("请求异常", ex);
        }
    }
}

3. 注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Autowired
    private LoggingInterceptor loggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 日志拦截器
        registry.addInterceptor(loggingInterceptor)
            .addPathPatterns("/**")  // 拦截所有
            .excludePathPatterns("/static/**", "/error");  // 排除静态资源

        // 认证拦截器
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/**")  // 只拦截 /api/*
            .excludePathPatterns(
                "/api/auth/login",
                "/api/auth/register",
                "/api/public/**"
            );
    }
}

4. 字符编码过滤器

@Configuration
public class EncodingConfig {

    @Bean
    public FilterRegistrationBean<CharacterEncodingFilter> encodingFilter() {
        FilterRegistrationBean<CharacterEncodingFilter> registration =
            new FilterRegistrationBean<>();

        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding("UTF-8");
        filter.setForceRequestEncoding(true);
        filter.setForceResponseEncoding(true);

        registration.setFilter(filter);
        registration.addUrlPatterns("/*");
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE);

        return registration;
    }
}

5. CORS 跨域过滤器

// 方式一:CorsFilter(Spring Boot 3.x)
@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return new CorsFilter(source);
    }
}

// 方式二:WebMvcConfigurer
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOriginPatterns("*")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

6. Filter 解决 XSS 注入

@Component
@Slf4j
public class XssFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new XssHttpServletRequestWrapper(
            (HttpServletRequest) request), response);
    }
}

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        return XssUtil.clean(super.getParameter(name));
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) return null;
        return Arrays.stream(values)
            .map(XssUtil::clean)
            .toArray(String[]::new);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> params = super.getParameterMap();
        Map<String, String[]> cleanParams = new HashMap<>();
        params.forEach((key, value) ->
            cleanParams.put(key, Arrays.stream(value)
                .map(XssUtil::clean)
                .toArray(String[]::new)));
        return cleanParams;
    }
}

源码解读

Filter 执行链

// ApplicationFilterChain 源码简化
public final class ApplicationFilterChain implements FilterChain {

    private ApplicationFilterConfig[] filters;
    private int pos = 0;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {

        if (pos < filters.length) {
            // 执行下一个 Filter
            ApplicationFilterConfig filterConfig = filters[pos++];
            Filter filter = filterConfig.getFilter();
            filter.doFilter(request, response, this);
        } else {
            // 所有 Filter 执行完毕,调用 Servlet
            servlet.service(request, response);
        }
    }
}

常见坑点

⚠️ 坑 1:拦截器中获取请求 body

// ❌ 请求 body 只能读取一次
@PostMapping("/api/test")
public Result test(HttpServletRequest request) throws IOException {
    // 读取 body 后,流已关闭
    BufferedReader reader = request.getReader();
    // Controller 层无法再次读取
}

// ✅ 使用 ContentCachingRequestWrapper
public class CachedBodyFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {

        ContentCachingRequestWrapper wrappedRequest =
            new ContentCachingRequestWrapper(request);
        chain.doFilter(wrappedRequest, response);

        // 可以在 Filter 中读取 body
        byte[] body = wrappedRequest.getContentAsByteArray();
    }
}

⚠️ 坑 2:拦截器执行顺序

// ❌ 错误的顺序配置
registry.addInterceptor(interceptorA).addPathPatterns("/**");
registry.addInterceptor(interceptorB).addPathPatterns("/**");
// 实际执行:B 先执行(后添加)

// ✅ 正确做法
registry.addInterceptor(interceptorA)
    .addPathPatterns("/**")
    .order(1);  // 明确指定顺序
registry.addInterceptor(interceptorB)
    .addPathPatterns("/**")
    .order(2);

⚠️ 坑 3:Filter 中注入 Bean

// ❌ 直接注入无效
@Component
public class MyFilter implements Filter {
    @Autowired
    private UserService userService;  // 可能为 null
}

// ✅ 使用延迟获取
@Component
public class MyFilter implements Filter {

    @Autowired
    private ApplicationContext context;

    @Override
    public void doFilter(...) {
        UserService userService = context.getBean(UserService.class);
        // 使用
    }
}

面试题

Q1:Filter、Interceptor、AOP 的区别?

参考答案

维度FilterInterceptorAOP
位置Servlet 容器Spring MVCSpring 容器
接口javax.servlet.FilterHandlerInterceptor@Aspect
作用范围所有请求Controller 层Service/Repository
能否获取 Spring Bean
能否拦截 Filter
能否拦截静态资源
典型场景字符编码、CORS认证、日志事务、性能监控

Q2:Filter 如何处理中文乱码?

参考答案

@Component
public class EncodingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                        FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        // 设置请求编码
        req.setCharacterEncoding("UTF-8");

        // 设置响应编码和 Content-Type
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=UTF-8");

        chain.doFilter(req, resp);
    }
}

Q3:拦截器如何实现登录校验?

参考答案

  1. 定义拦截器

    @Component
    public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler) {
            String token = request.getHeader("Authorization");
            if (token == null || !JwtUtil.validate(token)) {
                response.setStatus(401);
                return false;
            }
            // 验证通过,解析用户信息存 request
            request.setAttribute("userId", JwtUtil.getUserId(token));
            return true;
        }
    }
    
  2. 注册拦截器

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns(
                    "/api/auth/**",
                    "/api/public/**"
                );
        }
    }