Spring拦截器详解

1. Spring拦截器基础

1.1 什么是Spring拦截器

Spring拦截器(Interceptor)是Spring MVC框架中的一个重要组件,它可以在请求处理的不同阶段进行拦截和处理。拦截器提供了一种机制,让开发者可以在请求到达控制器(Controller)之前以及控制器处理完请求后执行自定义代码。

拦截器的基本思想源自AOP(面向切面编程)的思想,它允许我们在不修改原有代码的情况下,通过拦截请求来实现一些横切关注点,例如权限验证、日志记录、性能监控等。

1.2 拦截器的作用和应用场景

Spring拦截器的主要作用包括:

  1. 请求预处理:在请求到达控制器之前进行预处理,可以进行权限验证、参数验证等
  2. 请求后处理:在控制器处理完请求后,视图渲染之前进行后处理
  3. 完成处理:在整个请求完成后进行一些资源清理、日志记录等操作

常见的应用场景包括:

  • 身份认证和授权:验证用户是否已登录,是否有权限访问特定资源
  • 日志记录:记录请求信息、处理时间、异常信息等
  • 性能监控:监控请求处理时间、系统资源使用情况等
  • 数据转换:对请求数据进行转换或格式化
  • 国际化处理:根据用户区域设置切换语言
  • 主题设置:根据用户偏好设置主题
  • 异常处理:统一捕获并处理异常

1.3 拦截器与过滤器的区别

Spring拦截器(Interceptor)和Servlet过滤器(Filter)都可以用于请求的预处理和后处理,但它们之间存在一些重要区别:

特性Spring拦截器Servlet过滤器
实现技术基于Spring框架基于Servlet规范
执行顺序在DispatcherServlet之后执行在Servlet之前执行
拦截范围只拦截由DispatcherServlet处理的请求(.jsp、.html等静态资源不会被拦截)拦截所有请求,包括静态资源
获取对象可以获取SpringMVC的上下文对象,如Controller、方法的相关信息只能获取ServletRequest和ServletResponse
注入方式支持IoC容器的依赖注入不支持依赖注入
触发时机preHandle(Controller方法执行前)、postHandle(Controller方法执行后)、afterCompletion(视图渲染后)doFilter(在请求到达Servlet前和离开Servlet后)

下图展示了它们在请求处理流程中的位置:

HTTP请求 -> [Filter] -> DispatcherServlet -> [Interceptor] -> Controller

1.4 拦截器的工作原理

Spring拦截器基于Java的反射机制和Spring MVC的请求处理流程工作。它的工作原理可以简要描述为:

  1. 客户端发送HTTP请求
  2. 请求先经过Servlet过滤器链
  3. 请求到达DispatcherServlet
  4. DispatcherServlet根据请求路径查找对应的处理器(Handler)和处理器适配器(HandlerAdapter)
  5. 在调用处理器之前,DispatcherServlet会查找并执行所有匹配的拦截器的preHandle()方法
  6. 如果所有拦截器的preHandle()方法都返回true,则执行处理器方法
  7. 处理器方法执行完成后,DispatcherServlet按照与preHandle()相反的顺序执行所有拦截器的postHandle()方法
  8. 视图渲染完成后,再次按照与preHandle()相反的顺序执行所有拦截器的afterCompletion()方法

2. Spring拦截器的实现

2.1 拦截器接口与抽象类

Spring提供了以下核心接口和抽象类用于实现拦截器:

2.1.1 HandlerInterceptor接口

HandlerInterceptor是Spring拦截器最基本的接口,它定义了三个方法:

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
    
    // 在控制器方法执行之前调用
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                              Object handler) throws Exception {
        return true;  // 返回true表示继续执行,返回false表示中断执行
    }
    
    // 在控制器方法执行之后,视图渲染之前调用
    default void postHandle(HttpServletRequest request, HttpServletResponse response, 
                           Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
    
    // 在整个请求完成之后调用(包括视图渲染完成)
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, @Nullable Exception ex) throws Exception {
    }
}
  • preHandle:在控制器方法执行前调用。如果返回false,则中断执行,不会执行后续的拦截器和控制器方法。通常用于权限验证等预处理工作。
  • postHandle:在控制器方法执行后,视图渲染前调用。可以对ModelAndView进行修改。如果控制器方法抛出异常,则不会被调用。
  • afterCompletion:在整个请求处理完成后调用,无论控制器方法是否抛出异常都会被调用。通常用于资源清理、记录日志等操作。
2.1.2 AsyncHandlerInterceptor接口

对于异步请求处理,Spring提供了AsyncHandlerInterceptor接口,它扩展了HandlerInterceptor:

public interface AsyncHandlerInterceptor extends HandlerInterceptor {
    
    // 在异步请求开始时调用
    default void afterConcurrentHandlingStarted(HttpServletRequest request, 
                                             HttpServletResponse response, 
                                             Object handler) throws Exception {
    }
}
  • afterConcurrentHandlingStarted:在异步请求处理开始后调用。此时preHandle已被调用,但postHandle和afterCompletion将不会被调用。
2.1.3 HandlerInterceptorAdapter抽象类(已过时)

在Spring 5.3之前,Spring提供了HandlerInterceptorAdapter抽象类,它实现了HandlerInterceptor接口,开发者可以通过继承它来简化拦截器的开发。但在Spring 5.3中,这个类已被标记为过时,推荐直接实现HandlerInterceptor接口(因为Java 8已引入接口默认方法)。

// 在Spring 5.3之前的用法
public class MyInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 实现逻辑
        return true;
    }
}

// Spring 5.3及以后的推荐用法
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 实现逻辑
        return true;
    }
}

2.2 实现一个简单的拦截器

下面通过一个简单的日志拦截器示例,展示如何实现一个基本的Spring拦截器:

package com.example.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class LogInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 记录请求开始时间
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        
        // 记录请求信息
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        String clientIP = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        
        logger.info("请求开始 - URI: {}, 方法: {}, 客户端IP: {}, User-Agent: {}", 
                    requestURI, method, clientIP, userAgent);
        
        return true; // 继续执行
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                          ModelAndView modelAndView) throws Exception {
        // 在控制器处理后,视图渲染前执行
        logger.info("控制器处理完成 - URI: {}, 视图: {}", 
                   request.getRequestURI(), modelAndView != null ? modelAndView.getViewName() : "无");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) throws Exception {
        // 获取请求开始时间
        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        
        // 记录请求处理时间和可能的异常
        String requestURI = request.getRequestURI();
        if (ex != null) {
            logger.error("请求完成 - URI: {}, 处理时间: {}ms, 异常: {}", 
                        requestURI, executionTime, ex.getMessage());
        } else {
            logger.info("请求完成 - URI: {}, 处理时间: {}ms", requestURI, executionTime);
        }
    }
}

这个拦截器实现了以下功能:

  1. preHandle:记录请求开始时间和请求信息(URI、方法、客户端IP、User-Agent)
  2. postHandle:记录控制器处理完成的信息和即将渲染的视图名称
  3. afterCompletion:计算请求处理时间,记录请求完成信息和可能的异常

2.3 拦截器的配置方式

Spring MVC提供了多种方式配置拦截器,包括XML配置和Java代码配置。

2.3.1 XML配置方式

在Spring MVC的XML配置文件中,可以通过<mvc:interceptors>标签配置拦截器:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 配置拦截器 -->
    <mvc:interceptors>
        <!-- 全局拦截器,拦截所有请求 -->
        <bean class="com.example.interceptor.LogInterceptor"/>
        
        <!-- 特定路径拦截器 -->
        <mvc:interceptor>
            <mvc:mapping path="/api/**"/>
            <mvc:exclude-mapping path="/api/public/**"/>
            <bean class="com.example.interceptor.AuthInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
    
</beans>

在这个配置中:

  • LogInterceptor将拦截所有的请求
  • AuthInterceptor只拦截以/api/开头的请求,但不包括以/api/public/开头的请求
2.3.2 Java代码配置方式

在Spring Boot或使用Java配置的Spring MVC应用中,可以通过实现WebMvcConfigurer接口来配置拦截器:

package com.example.config;

import com.example.interceptor.LogInterceptor;
import com.example.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加日志拦截器,拦截所有请求
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/**");
        
        // 添加权限拦截器,只拦截/api/**路径,但排除/api/public/**
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**");
    }
}

在这个配置中:

  • addInterceptor方法用于添加拦截器
  • addPathPatterns方法用于指定拦截的路径模式
  • excludePathPatterns方法用于指定排除的路径模式

也可以通过依赖注入来获取拦截器实例:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    private final LogInterceptor logInterceptor;
    private final AuthInterceptor authInterceptor;
    
    // 构造器注入
    public WebConfig(LogInterceptor logInterceptor, AuthInterceptor authInterceptor) {
        this.logInterceptor = logInterceptor;
        this.authInterceptor = authInterceptor;
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor).addPathPatterns("/**");
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/public/**");
    }
}

在这种情况下,拦截器需要被定义为Spring Bean:

@Component
public class LogInterceptor implements HandlerInterceptor {
    // 实现代码...
}

@Component
public class AuthInterceptor implements HandlerInterceptor {
    // 实现代码...
}

3. Spring拦截器的实际应用

3.1 权限验证拦截器

权限验证是拦截器最常见的应用之一,它可以在用户访问受保护资源前验证用户的身份和权限。

3.1.1 基本的权限验证拦截器
package com.example.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 获取会话
        HttpSession session = request.getSession(false);
        
        // 检查用户是否已登录
        if (session == null || session.getAttribute("user") == null) {
            // 用户未登录,重定向到登录页面
            response.sendRedirect("/login?redirectUrl=" + request.getRequestURI());
            return false; // 中断请求处理
        }
        
        // 用户已登录,继续处理
        return true;
    }
}
3.1.2 基于角色的权限验证拦截器
package com.example.interceptor;

import com.example.model.User;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.method.HandlerMethod;
import com.example.annotation.RequireRole;

@Component
public class RoleBasedAuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 只对控制器方法进行拦截
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 检查方法是否有@RequireRole注解
        RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
        
        // 如果没有@RequireRole注解,则不需要角色验证
        if (requireRole == null) {
            return true;
        }
        
        // 获取会话
        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("user") == null) {
            // 用户未登录,重定向到登录页面
            response.sendRedirect("/login?redirectUrl=" + request.getRequestURI());
            return false;
        }
        
        // 获取用户信息和所需角色
        User user = (User) session.getAttribute("user");
        String[] requiredRoles = requireRole.value();
        
        // 检查用户是否有所需角色
        for (String role : requiredRoles) {
            if (user.hasRole(role)) {
                return true; // 用户有所需角色,允许访问
            }
        }
        
        // 用户没有所需角色,返回403 Forbidden
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "您没有访问此资源的权限");
        return false;
    }
}

这个拦截器需要配合自定义注解@RequireRole使用:

package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String[] value();
}

在控制器中使用@RequireRole注解:

@RestController
@RequestMapping("/api/admin")
public class AdminController {
    
    @GetMapping("/users")
    @RequireRole("ADMIN")
    public List<User> getAllUsers() {
        // 获取所有用户的逻辑
        return userService.findAll();
    }
    
    @GetMapping("/reports")
    @RequireRole({"ADMIN", "MANAGER"})
    public List<Report> getReports() {
        // 获取报表的逻辑
        return reportService.generateReports();
    }
}

3.2 性能监控拦截器

性能监控拦截器可以帮助我们了解应用的性能状况,发现潜在的性能问题。

package com.example.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Component
public class PerformanceInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(PerformanceInterceptor.class);
    
    // 存储URI的性能统计信息
    private final Map<String, RequestStats> statsMap = new ConcurrentHashMap<>();
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                          ModelAndView modelAndView) throws Exception {
        // 不在这里进行操作,因为如果发生异常,postHandle不会被调用
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        
        // 对于控制器方法,记录详细信息
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            String controllerName = handlerMethod.getBeanType().getSimpleName();
            String methodName = handlerMethod.getMethod().getName();
            String key = controllerName + "." + methodName;
            
            // 更新统计信息
            RequestStats stats = statsMap.computeIfAbsent(key, k -> new RequestStats());
            stats.addRequest(executionTime);
            
            // 记录本次请求的执行时间
            logger.info("性能统计 - {}.{}: {}ms", controllerName, methodName, executionTime);
            
            // 如果执行时间超过阈值,记录警告
            if (executionTime > 500) {
                logger.warn("性能警告 - {}.{} 执行时间超过500ms: {}ms", 
                           controllerName, methodName, executionTime);
            }
            
            // 定期打印统计信息(例如,每1000次请求)
            if (stats.getRequestCount() % 1000 == 0) {
                logger.info("性能统计汇总 - {}: 请求数: {}, 平均时间: {}ms, 最大时间: {}ms", 
                           key, stats.getRequestCount(), stats.getAverageTime(), stats.getMaxTime());
            }
        }
    }
    
    // 请求统计类
    private static class RequestStats {
        private final AtomicLong requestCount = new AtomicLong(0);
        private final AtomicLong totalTime = new AtomicLong(0);
        private long maxTime = 0;
        
        public void addRequest(long executionTime) {
            requestCount.incrementAndGet();
            totalTime.addAndGet(executionTime);
            // 更新最大执行时间
            synchronized (this) {
                if (executionTime > maxTime) {
                    maxTime = executionTime;
                }
            }
        }
        
        public long getRequestCount() {
            return requestCount.get();
        }
        
        public long getAverageTime() {
            long count = requestCount.get();
            return count > 0 ? totalTime.get() / count : 0;
        }
        
        public long getMaxTime() {
            return maxTime;
        }
    }
}

3.3 国际化拦截器

国际化拦截器可以根据用户的区域设置或偏好自动设置应用的语言环境。

package com.example.interceptor;

import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.support.RequestContextUtils;

@Component
public class LocaleInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 获取语言参数
        String language = request.getParameter("lang");
        
        if (language != null) {
            // 根据参数设置新的Locale
            Locale locale = parseLocale(language);
            
            // 获取LocaleResolver
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver != null) {
                // 设置Locale
                localeResolver.setLocale(request, response, locale);
                
                // 如果使用的是SessionLocaleResolver,可以将Locale保存到会话中
                HttpSession session = request.getSession();
                session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
            }
        }
        
        return true;
    }
    
    // 解析语言参数为Locale对象
    private Locale parseLocale(String language) {
        String[] parts = language.split("_");
        if (parts.length == 1) {
            // 只有语言代码,如"zh"
            return new Locale(parts[0]);
        } else if (parts.length >= 2) {
            // 有语言和国家代码,如"zh_CN"
            return new Locale(parts[0], parts[1]);
        }
        // 默认返回英语
        return Locale.ENGLISH;
    }
}

3.4 跨站请求伪造(CSRF)防护拦截器

CSRF拦截器可以通过验证Token来防止跨站请求伪造攻击。

package com.example.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.UUID;

@Component
public class CsrfInterceptor implements HandlerInterceptor {
    
    private static final String CSRF_TOKEN_NAME = "_csrf_token";
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        HttpSession session = request.getSession();
        
        // 如果是GET请求,生成新的CSRF Token
        if ("GET".equals(request.getMethod())) {
            String token = UUID.randomUUID().toString();
            session.setAttribute(CSRF_TOKEN_NAME, token);
            return true;
        }
        
        // 如果是POST、PUT、DELETE等修改请求,验证CSRF Token
        if ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod()) || 
            "DELETE".equals(request.getMethod())) {
            
            // 获取会话中的Token
            String sessionToken = (String) session.getAttribute(CSRF_TOKEN_NAME);
            
            // 获取请求中的Token(可以从请求头或参数中获取)
            String requestToken = request.getHeader(CSRF_TOKEN_NAME);
            if (requestToken == null) {
                requestToken = request.getParameter(CSRF_TOKEN_NAME);
            }
            
            // 验证Token
            if (sessionToken == null || requestToken == null || !sessionToken.equals(requestToken)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF验证失败");
                return false;
            }
        }
        
        return true;
    }
}

在JSP或Thymeleaf模板中,可以这样使用CSRF Token:

JSP示例:

<form action="/submit" method="post">
    <input type="hidden" name="_csrf_token" value="${sessionScope._csrf_token}">
    <!-- 其他表单字段 -->
    <button type="submit">提交</button>
</form>

Thymeleaf示例:

<form action="/submit" method="post">
    <input type="hidden" name="_csrf_token" th:value="${session._csrf_token}">
    <!-- 其他表单字段 -->
    <button type="submit">提交</button>
</form>

3.5 API请求限流拦截器

API请求限流拦截器可以防止API被滥用,保护系统资源。

package com.example.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    
    // 限流配置:每个IP每分钟允许的最大请求数
    private static final int MAX_REQUESTS_PER_MINUTE = 60;
    
    // 存储IP地址及其请求计数和上次重置时间
    private final Map<String, RequestCount> requestCounts = new ConcurrentHashMap<>();
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 获取客户端IP
        String clientIP = getClientIP(request);
        
        // 获取或创建请求计数器
        RequestCount count = requestCounts.computeIfAbsent(clientIP, k -> new RequestCount());
        
        // 检查是否需要重置计数(如果已经过了一分钟)
        long currentTime = System.currentTimeMillis();
        if (currentTime - count.getLastResetTime() > 60000) {
            count.reset(currentTime);
        }
        
        // 增加请求计数
        int requestCount = count.incrementAndGet();
        
        // 检查是否超过限制
        if (requestCount > MAX_REQUESTS_PER_MINUTE) {
            // 返回 429 Too Many Requests
            response.setStatus(429);
            response.setHeader("Retry-After", "60");
            response.getWriter().write("请求频率过高,请稍后再试");
            return false;
        }
        
        return true;
    }
    
    // 获取客户端IP地址
    private String getClientIP(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            // 获取第一个IP(客户端真实IP)
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
    
    // 请求计数类
    private static class RequestCount {
        private final AtomicInteger count = new AtomicInteger(0);
        private long lastResetTime;
        
        public RequestCount() {
            this.lastResetTime = System.currentTimeMillis();
        }
        
        public int incrementAndGet() {
            return count.incrementAndGet();
        }
        
        public long getLastResetTime() {
            return lastResetTime;
        }
        
        public void reset(long currentTime) {
            count.set(0);
            lastResetTime = currentTime;
        }
    }
}

4. 拦截器链和执行顺序

4.1 拦截器链的概念

当配置多个拦截器时,它们会按照一定的顺序组成一个拦截器链(Interceptor Chain)。拦截器链的执行遵循以下规则:

  1. preHandle方法:按照拦截器的配置顺序从前往后依次执行
  2. postHandle方法:按照拦截器的配置顺序从后往前依次执行
  3. afterCompletion方法:按照拦截器的配置顺序从后往前依次执行

如果任何拦截器的preHandle方法返回false,则后续的拦截器和控制器方法都不会执行。

4.2 拦截器执行顺序的示意图

假设我们配置了三个拦截器:InterceptorA、InterceptorB和InterceptorC,它们的执行顺序如下:

请求 -> [ InterceptorA.preHandle ] -> [ InterceptorB.preHandle ] -> [ InterceptorC.preHandle ] -> Controller方法
                                                                                                      |
响应 <- [ InterceptorA.afterCompletion ] <- [ InterceptorB.afterCompletion ] <- [ InterceptorC.afterCompletion ] <- [ InterceptorC.postHandle ] <- [ InterceptorB.postHandle ] <- [ InterceptorA.postHandle ] 

以下情况值得注意:

  • 如果InterceptorB的preHandle返回false,则InterceptorC的preHandle和Controller方法不会执行
  • 如果InterceptorB的preHandle返回false,则InterceptorA和InterceptorB的afterCompletion会执行,但InterceptorC的afterCompletion不会执行
  • 如果Controller方法抛出异常,则所有拦截器的postHandle方法都不会执行,但afterCompletion方法会执行

4.3 控制拦截器的执行顺序

在Spring中,可以通过以下方式控制拦截器的执行顺序:

4.3.1 使用XML配置控制顺序

在XML配置中,拦截器的声明顺序决定了它们的执行顺序:

<mvc:interceptors>
    <!-- 先执行 -->
    <bean class="com.example.interceptor.LogInterceptor"/>
    
    <!-- 其次执行 -->
    <mvc:interceptor>
        <mvc:mapping path="/api/**"/>
        <bean class="com.example.interceptor.AuthInterceptor"/>
    </mvc:interceptor>
    
    <!-- 最后执行 -->
    <bean class="com.example.interceptor.PerformanceInterceptor"/>
</mvc:interceptors>
4.3.2 使用Java配置控制顺序

在Java配置中,拦截器的注册顺序决定了它们的执行顺序:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 先执行
        registry.addInterceptor(new LogInterceptor());
        
        // 其次执行
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**");
        
        // 最后执行
        registry.addInterceptor(new PerformanceInterceptor());
    }
}
4.3.3 使用Order注解或Ordered接口

如果拦截器是Spring Bean,还可以通过@Order注解或实现Ordered接口来明确指定执行顺序:

使用@Order注解:

@Component
@Order(1) // 数字越小,优先级越高
public class LogInterceptor implements HandlerInterceptor {
    // 实现代码...
}

@Component
@Order(2)
public class AuthInterceptor implements HandlerInterceptor {
    // 实现代码...
}

@Component
@Order(3)
public class PerformanceInterceptor implements HandlerInterceptor {
    // 实现代码...
}

实现Ordered接口:

@Component
public class LogInterceptor implements HandlerInterceptor, Ordered {
    
    @Override
    public int getOrder() {
        return 1; // 数字越小,优先级越高
    }
    
    // 其他HandlerInterceptor方法的实现...
}

5. 最佳实践与注意事项

5.1 设计原则

  1. 单一职责原则:一个拦截器应该只负责一个功能,如日志记录、权限验证等
  2. 拦截器链设计:按照责任链模式设计拦截器链,明确每个拦截器的职责和执行顺序
  3. 性能考虑:拦截器中的代码应该尽量简洁高效,避免执行耗时操作
  4. 异常处理:妥善处理拦截器中可能发生的异常,避免影响正常请求处理

5.2 实现建议

  1. 避免重复执行:确保拦截器中的逻辑不会与过滤器、AOP通知等其他组件重复
  2. 使用依赖注入:将拦截器定义为Spring Bean,利用Spring的依赖注入功能
  3. 路径匹配规则:合理设置拦截器的路径匹配规则,避免不必要的拦截
  4. 请求参数验证:在拦截器中进行基本的请求参数验证,减轻控制器的负担
  5. 日志记录:在拦截器中记录必要的日志信息,方便问题排查

5.3 常见错误和问题

  1. 拦截器配置不当

    • 问题:拦截器未生效或对特定路径不生效
    • 解决:检查拦截器的配置,包括路径匹配模式和排除路径
  2. 拦截器顺序混乱

    • 问题:拦截器执行顺序不符合预期
    • 解决:明确指定拦截器的执行顺序,使用@Order注解或调整配置顺序
  3. 资源未正确释放

    • 问题:拦截器中打开的资源未正确关闭
    • 解决:在afterCompletion方法中确保所有资源都被正确释放
  4. 拦截器性能问题

    • 问题:拦截器中执行耗时操作导致整体性能下降
    • 解决:优化拦截器代码,将耗时操作移至后台执行或使用缓存
  5. 异常处理不当

    • 问题:拦截器中的异常未被正确处理,导致请求处理中断
    • 解决:在拦截器方法中使用try-catch捕获异常,并记录日志

5.4 安全注意事项

  1. 敏感信息处理

    • 不要在拦截器中记录敏感信息(如密码、令牌)
    • 对于需要处理的敏感信息,应该进行脱敏处理
  2. 权限控制

    • 确保权限验证逻辑严谨,避免绕过验证的可能
    • 对于不同的API路径,可能需要不同级别的权限验证
  3. 异常信息暴露

    • 避免将详细的异常信息直接返回给客户端
    • 使用统一的错误处理机制,确保安全的错误响应

5.5 测试建议

  1. 单元测试

    • 为每个拦截器编写单元测试,验证其核心逻辑
    • 模拟HttpServletRequest和HttpServletResponse对象进行测试
  2. 集成测试

    • 编写集成测试,验证拦截器链的正确执行
    • 测试拦截器与其他组件(如控制器)的交互
  3. 性能测试

    • 对拦截器进行性能测试,确保其不会成为性能瓶颈
    • 测试高并发场景下拦截器的表现

6. 拦截器的高级应用

6.1 结合注解实现更灵活的拦截

除了前面介绍的基于角色的权限验证外,我们还可以使用更多自定义注解与拦截器结合,实现更灵活的功能。

6.1.1 限流注解
package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 允许的请求次数
    int value() default 10;
    
    // 时间窗口
    long time() default 1;
    
    // 时间单位
    TimeUnit unit() default TimeUnit.MINUTES;
    
    // 限流类型(IP、用户、接口等)
    LimitType type() default LimitType.IP;
    
    // 限流类型枚举
    enum LimitType {
        IP, USER, INTERFACE
    }
}

对应的拦截器实现:

package com.example.interceptor;

import com.example.annotation.RateLimit;
import com.example.annotation.RateLimit.LimitType;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    
    private final RedisTemplate<String, Integer> redisTemplate;
    
    public RateLimitInterceptor(RedisTemplate<String, Integer> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                            Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
        
        if (rateLimit == null) {
            return true;
        }
        
        String key = generateKey(request, rateLimit, handlerMethod);
        Integer count = redisTemplate.opsForValue().get(key);
        
        if (count == null) {
            // 第一次访问,设置初始值为1,并设置过期时间
            redisTemplate.opsForValue().set(key, 1, rateLimit.time(), rateLimit.unit());
            return true;
        }
        
        if (count < rateLimit.value()) {
            // 未超过限制,请求计数加1
            redisTemplate.opsForValue().increment(key);
            return true;
        }
        
        // 超过限制,返回429 Too Many Requests
        response.setStatus(429);
        response.setHeader("Retry-After", String.valueOf(rateLimit.time()));
        response.getWriter().write("请求频率过高,请稍后再试");
        return false;
    }
    
    private String generateKey(HttpServletRequest request, RateLimit rateLimit, HandlerMethod handlerMethod) {
        StringBuilder key = new StringBuilder("rate_limit:");
        
        // 根据限流类型生成不同的key
        if (rateLimit.type() == LimitType.IP) {
            key.append("ip:").append(getClientIP(request));
        } else if (rateLimit.type() == LimitType.USER) {
            // 从会话或请求中获取用户标识
            String userId = getUserId(request);
            key.append("user:").append(userId != null ? userId : "anonymous");
        } else if (rateLimit.type() == LimitType.INTERFACE) {
            // 使用方法完整名称作为标识
            key.append("interface:").append(handlerMethod.getBeanType().getName())
               .append(".").append(handlerMethod.getMethod().getName());
        }
        
        return key.toString();
    }
    
    private String getClientIP(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
    
    private String getUserId(HttpServletRequest request) {
        // 从会话或请求中获取用户ID
        // 具体实现取决于你的认证机制
        return (String) request.getSession().getAttribute("userId");
    }
}

在控制器中使用:

@RestController
@RequestMapping("/api")
public class ApiController {
    
    @GetMapping("/public/data")
    public ResponseEntity<?> getPublicData() {
        // 公共接口,不需要限流
        return ResponseEntity.ok("Public data");
    }
    
    @GetMapping("/data")
    @RateLimit(value = 5, time = 1, unit = TimeUnit.MINUTES, type = LimitType.IP)
    public ResponseEntity<?> getData() {
        // 每个IP每分钟最多访问5次
        return ResponseEntity.ok("Data");
    }
    
    @GetMapping("/user/profile")
    @RateLimit(value = 20, time = 1, unit = TimeUnit.HOURS, type = LimitType.USER)
    public ResponseEntity<?> getUserProfile() {
        // 每个用户每小时最多访问20次
        return ResponseEntity.ok("User profile");
    }
    
    @GetMapping("/heavy-operation")
    @RateLimit(value = 100, time = 1, unit = TimeUnit.DAYS, type = LimitType.INTERFACE)
    public ResponseEntity<?> performHeavyOperation() {
        // 所有用户每天最多调用该接口100次
        return ResponseEntity.ok("Heavy operation completed");
    }
}
6.1.2 接口审计注解
package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
    // 操作类型
    String operation() default "";
    
    // 资源类型
    String resource() default "";
    
    // 是否记录请求参数
    boolean logParams() default true;
    
    // 是否记录响应结果
    boolean logResponse() default false;
}

对应的拦截器实现:

package com.example.interceptor;

import com.example.annotation.Audit;
import com.example.service.AuditLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Component
public class AuditInterceptor implements HandlerInterceptor {
    
    private final AuditLogService auditLogService;
    private final ObjectMapper objectMapper;
    
    public AuditInterceptor(AuditLogService auditLogService, ObjectMapper objectMapper) {
        this.auditLogService = auditLogService;
        this.objectMapper = objectMapper;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                            Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Audit audit = handlerMethod.getMethodAnnotation(Audit.class);
        
        if (audit == null) {
            // 检查类级别的注解
            audit = handlerMethod.getBeanType().getAnnotation(Audit.class);
        }
        
        if (audit != null) {
            // 生成一个唯一的请求ID
            String requestId = UUID.randomUUID().toString();
            request.setAttribute("requestId", requestId);
            request.setAttribute("auditAnnotation", audit);
            request.setAttribute("startTime", System.currentTimeMillis());
            
            // 如果需要记录请求参数,确保请求是ContentCachingRequestWrapper类型
            if (audit.logParams() && !(request instanceof ContentCachingRequestWrapper)) {
                return true; // 在Filter中已包装请求
            }
        }
        
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
                          Object handler, ModelAndView modelAndView) throws Exception {
        // 不在这里执行审计日志记录
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return;
        }
        
        Audit audit = (Audit) request.getAttribute("auditAnnotation");
        if (audit == null) {
            return;
        }
        
        String requestId = (String) request.getAttribute("requestId");
        long startTime = (Long) request.getAttribute("startTime");
        long executionTime = System.currentTimeMillis() - startTime;
        
        // 获取处理器方法信息
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        String className = handlerMethod.getBeanType().getName();
        String methodName = handlerMethod.getMethod().getName();
        
        // 获取用户信息
        String username = getUsernameFromRequest(request);
        
        // 构建审计日志记录
        AuditLog auditLog = new AuditLog();
        auditLog.setRequestId(requestId);
        auditLog.setUsername(username);
        auditLog.setOperation(audit.operation());
        auditLog.setResource(audit.resource());
        auditLog.setMethod(className + "." + methodName);
        auditLog.setExecutionTime(executionTime);
        auditLog.setStatus(ex == null ? "SUCCESS" : "FAILURE");
        auditLog.setErrorMessage(ex != null ? ex.getMessage() : null);
        
        // 记录请求参数
        if (audit.logParams() && request instanceof ContentCachingRequestWrapper) {
            ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
            String requestBody = new String(cachingRequest.getContentAsByteArray());
            if (!requestBody.isEmpty()) {
                auditLog.setRequestBody(requestBody);
            }
        }
        
        // 记录响应结果
        if (audit.logResponse() && response instanceof ContentCachingResponseWrapper) {
            ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response;
            String responseBody = new String(cachingResponse.getContentAsByteArray());
            if (!responseBody.isEmpty()) {
                auditLog.setResponseBody(responseBody);
                cachingResponse.copyBodyToResponse(); // 重要:复制响应体内容
            }
        }
        
        // 保存审计日志
        auditLogService.saveAuditLog(auditLog);
    }
    
    private String getUsernameFromRequest(HttpServletRequest request) {
        // 从会话或认证上下文中获取用户名
        // 实际实现取决于你的认证机制
        Object user = request.getSession().getAttribute("user");
        if (user != null) {
            return user.toString();
        }
        return "anonymous";
    }
    
    // 审计日志实体类
    public static class AuditLog {
        private String requestId;
        private String username;
        private String operation;
        private String resource;
        private String method;
        private long executionTime;
        private String status;
        private String errorMessage;
        private String requestBody;
        private String responseBody;
        
        // getter和setter方法省略
    }
}

注意:为了能够读取请求和响应体,需要将请求和响应包装为可缓存的包装器。这通常在Filter中完成:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContentCachingFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        
        try {
            filterChain.doFilter(requestWrapper, responseWrapper);
        } finally {
            responseWrapper.copyBodyToResponse();
        }
    }
}

在控制器中使用:

@RestController
@RequestMapping("/api/users")
@Audit(resource = "用户管理") // 类级别注解
public class UserController {
    
    @GetMapping
    @Audit(operation = "查询所有用户")
    public List<User> getAllUsers() {
        // 查询用户列表
        return userService.findAll();
    }
    
    @GetMapping("/{id}")
    @Audit(operation = "查询单个用户")
    public User getUser(@PathVariable Long id) {
        // 查询单个用户
        return userService.findById(id);
    }
    
    @PostMapping
    @Audit(operation = "创建用户", logParams = true, logResponse = true)
    public User createUser(@RequestBody User user) {
        // 创建新用户
        return userService.save(user);
    }
    
    @PutMapping("/{id}")
    @Audit(operation = "更新用户", logParams = true)
    public User updateUser(@PathVariable Long id, @RequestBody User user) {
        // 更新用户信息
        return userService.update(id, user);
    }
    
    @DeleteMapping("/{id}")
    @Audit(operation = "删除用户")
    public void deleteUser(@PathVariable Long id) {
        // 删除用户
        userService.delete(id);
    }
}

6.2 异步请求处理中的拦截器

在Spring MVC中,可以使用@Async注解或返回Callable/DeferredResult来实现异步请求处理。对于异步处理的请求,普通的拦截器可能不会按预期工作,因为请求线程与处理线程可能不同。

在这种情况下,我们需要使用AsyncHandlerInterceptor接口,它提供了一个额外的方法afterConcurrentHandlingStarted,该方法在异步处理开始后调用:

package com.example.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AsyncRequestInterceptor implements AsyncHandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(AsyncRequestInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        logger.info("AsyncRequestInterceptor.preHandle 在请求线程中执行");
        // 记录请求开始时间
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                          ModelAndView modelAndView) throws Exception {
        logger.info("AsyncRequestInterceptor.postHandle 在异步处理完成后执行");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                               Object handler, Exception ex) throws Exception {
        logger.info("AsyncRequestInterceptor.afterCompletion 在异步处理完成后执行");
        
        // 计算请求处理时间
        long startTime = (Long) request.getAttribute("startTime");
        long executionTime = System.currentTimeMillis() - startTime;
        logger.info("异步请求处理时间: {}ms", executionTime);
    }
    
    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, 
                                             HttpServletResponse response, 
                                             Object handler) throws Exception {
        logger.info("AsyncRequestInterceptor.afterConcurrentHandlingStarted 在请求线程中、异步处理开始时执行");
        // 注意:这个方法在异步处理开始时执行,此时preHandle已被调用,但postHandle和afterCompletion还没有被调用
        // 可以在这里执行一些清理操作或记录日志
    }
}

异步控制器示例:

@RestController
@RequestMapping("/api/async")
public class AsyncController {
    
    @GetMapping("/callable")
    public Callable<String> handleCallable() {
        return () -> {
            // 模拟长时间处理
            Thread.sleep(2000);
            return "Async response using Callable";
        };
    }
    
    @GetMapping("/deferred")
    public DeferredResult<String> handleDeferredResult() {
        DeferredResult<String> result = new DeferredResult<>();
        
        // 在另一个线程中异步处理请求
        CompletableFuture.runAsync(() -> {
            try {
                // 模拟长时间处理
                Thread.sleep(2000);
                result.setResult("Async response using DeferredResult");
            } catch (InterruptedException e) {
                result.setErrorResult("处理异常: " + e.getMessage());
            }
        });
        
        return result;
    }
}

6.3 拦截器与WebFlux

Spring 5引入了响应式编程模型WebFlux,它使用不同的拦截机制。在WebFlux中,不使用HandlerInterceptor,而是使用WebFilter进行请求拦截:

package com.example.filter;

import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Component
public class ReactiveLoggingFilter implements WebFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        long startTime = System.currentTimeMillis();
        
        // 记录请求信息
        String path = exchange.getRequest().getPath().value();
        String method = exchange.getRequest().getMethod().name();
        
        System.out.println("收到请求: " + method + " " + path);
        
        // 使用Mono.fromRunnable在请求处理完成后执行
        return chain.filter(exchange)
                .doFinally(signalType -> {
                    long endTime = System.currentTimeMillis();
                    long executionTime = endTime - startTime;
                    
                    System.out.println("请求处理完成: " + method + " " + path + 
                                     ", 处理时间: " + executionTime + "ms");
                });
    }
}

如果你需要更复杂的拦截功能,可以实现WebHandler或使用Spring的WebFilterWebHandler扩展机制。

6.4 拦截器中的依赖注入

当拦截器被定义为Spring Bean时,可以利用Spring的依赖注入机制注入其他组件,例如服务、数据源等:

package com.example.interceptor;

import com.example.service.UserService;
import com.example.service.SecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AdvancedAuthInterceptor implements HandlerInterceptor {
    
    private final UserService userService;
    private final SecurityService securityService;
    
    @Autowired
    public AdvancedAuthInterceptor(UserService userService, SecurityService securityService) {
        this.userService = userService;
        this.securityService = securityService;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        // 获取认证令牌
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "未提供认证令牌");
            return false;
        }
        
        // 验证令牌并获取用户信息
        boolean isValid = securityService.validateToken(token);
        if (!isValid) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效的认证令牌");
            return false;
        }
        
        // 获取用户信息并设置到请求属性中
        Long userId = securityService.getUserIdFromToken(token);
        request.setAttribute("userId", userId);
        
        // 加载用户详细信息
        request.setAttribute("user", userService.findById(userId));
        
        return true;
    }
}

6.5 自定义拦截器链的错误处理

有时候,我们希望能够统一处理拦截器链中可能发生的异常。可以通过实现ControllerAdviceExceptionHandler来捕获和处理拦截器中抛出的异常:

package com.example.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<?> handleAuthenticationException(AuthenticationException ex) {
        return ResponseEntity.status(401)
                .body(new ErrorResponse("authentication_error", ex.getMessage()));
    }
    
    @ExceptionHandler(AuthorizationException.class)
    public ResponseEntity<?> handleAuthorizationException(AuthorizationException ex) {
        return ResponseEntity.status(403)
                .body(new ErrorResponse("authorization_error", ex.getMessage()));
    }
    
    @ExceptionHandler(RateLimitException.class)
    public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
        return ResponseEntity.status(429)
                .header("Retry-After", String.valueOf(ex.getRetryAfter()))
                .body(new ErrorResponse("rate_limit_error", ex.getMessage()));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGenericException(Exception ex) {
        return ResponseEntity.status(500)
                .body(new ErrorResponse("internal_error", "服务器内部错误"));
    }
    
    // 错误响应类
    public static class ErrorResponse {
        private final String code;
        private final String message;
        
        public ErrorResponse(String code, String message) {
            this.code = code;
            this.message = message;
        }
        
        public String getCode() {
            return code;
        }
        
        public String getMessage() {
            return message;
        }
    }
}

然后在拦截器中可以直接抛出相应的异常:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
        throws Exception {
    String token = request.getHeader("Authorization");
    if (token == null || token.isEmpty()) {
        throw new AuthenticationException("未提供认证令牌");
    }
    
    boolean isValid = securityService.validateToken(token);
    if (!isValid) {
        throw new AuthenticationException("无效的认证令牌");
    }
    
    Long userId = securityService.getUserIdFromToken(token);
    if (!securityService.hasPermission(userId, request.getRequestURI())) {
        throw new AuthorizationException("没有访问权限");
    }
    
    return true;
}

7. 总结

7.1 Spring拦截器的优势和特点

Spring拦截器是Spring MVC框架中非常强大的组件,它具有以下优势和特点:

  1. 集成Spring生态系统:拦截器与Spring框架紧密集成,可以利用Spring的依赖注入、AOP等特性
  2. 精确控制请求处理流程:可以在请求处理的不同阶段(前、中、后)执行自定义逻辑
  3. 灵活的配置方式:支持XML配置和Java代码配置,可以对拦截路径进行精确控制
  4. 责任链模式:多个拦截器组成拦截器链,按照特定顺序执行
  5. 获取Spring上下文:可以访问Controller、方法和参数的详细信息,实现更精细的控制

7.2 常见应用场景回顾

Spring拦截器的常见应用场景包括:

  1. 权限验证:验证用户身份和权限
  2. 日志记录:记录请求信息、执行时间和异常
  3. 性能监控:监控请求处理时间和系统资源使用情况
  4. 参数验证:验证请求参数的有效性
  5. 国际化处理:根据用户区域设置切换语言
  6. 主题设置:根据用户偏好设置主题
  7. 请求限流:限制单位时间内的请求次数
  8. 跨站请求伪造防护:通过Token验证防止CSRF攻击
  9. 审计日志:记录用户操作和系统活动
  10. 异常处理:统一捕获并处理异常

7.3 与其他技术的对比

Spring拦截器与其他类似技术的对比:

特性Spring拦截器Servlet过滤器AOP切面
实现技术基于Spring MVC基于Servlet规范基于Spring AOP
执行位置在DispatcherServlet之后在所有Servlet之前可拦截任何Spring管理的Bean
拦截范围仅拦截Controller请求拦截所有HTTP请求可拦截任何Spring管理的Bean
获取控制器信息可以获取不可以获取可以获取
获取请求/响应可以获取可以获取通过参数获取
依赖注入支持不直接支持支持
执行顺序控制明确定义通过Filter顺序控制通过@Order控制
主要用途请求处理流程控制请求/响应过滤横切关注点

7.4 最佳实践总结

基于前面的讨论,我们可以总结出以下最佳实践:

  1. 合理规划拦截器职责

    • 遵循单一职责原则,一个拦截器只负责一个功能
    • 将通用的预处理逻辑放在拦截器中,减轻控制器的负担
  2. 合理组织拦截器链

    • 按照责任链模式设计拦截器链
    • 明确定义拦截器的执行顺序
    • 将日志记录等操作放在链的最前面,将权限验证放在前面
  3. 高效实现拦截器

    • 避免在拦截器中执行耗时操作
    • 使用缓存减少重复计算
    • 合理处理异步和并发场景
  4. 安全性考虑

    • 验证用户身份和权限
    • 防止敏感信息泄露
    • 实现请求限流和防止CSRF攻击
  5. 与其他技术协同使用

    • 使用过滤器处理编码、跨域等请求级问题
    • 使用拦截器处理业务级的横切关注点
    • 使用AOP处理更精细的横切关注点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值