【SpringBoot 从入门到架构师】第7章:拦截器、过滤器、跨域处理
1. 过滤器Filter:自定义过滤器、执行顺序、应用场景
一、自定义过滤器
基础实现方式
方式一:实现 Filter 接口
@Component public class CustomFilter implements Filter { @Override public void init(FilterConfig filterConfig) { // 初始化逻辑 } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 请求处理前逻辑 System.out.println("Before filter processing"); HttpServletRequest httpRequest = (HttpServletRequest) request; System.out.println("Request URI: " + httpRequest.getRequestURI()); // 执行下一个过滤器或目标资源 chain.doFilter(request, response); // 响应处理后逻辑 System.out.println("After filter processing"); } @Override public void destroy() { // 销毁逻辑 } }方式二:使用 @WebFilter 注解
@WebFilter(urlPatterns = "/*") public class AnnotationFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long startTime = System.currentTimeMillis(); chain.doFilter(request, response); long endTime = System.currentTimeMillis(); System.out.println("Request took: " + (endTime - startTime) + "ms"); } }需要在启动类添加 @ServletComponentScan:
@SpringBootApplication @ServletComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }二、过滤器执行顺序
控制执行顺序的方法
方法一:使用 @Order 注解
@Component @Order(1) // 数字越小,优先级越高 public class FirstFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("First Filter - Before"); chain.doFilter(request, response); System.out.println("First Filter - After"); } } @Component @Order(2) public class SecondFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Second Filter - Before"); chain.doFilter(request, response); System.out.println("Second Filter - After"); } }方法二:使用 FilterRegistrationBean
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<FirstFilter> firstFilter() { FilterRegistrationBean<FirstFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new FirstFilter()); registration.addUrlPatterns("/*"); registration.setOrder(1); // 设置顺序 registration.setName("firstFilter"); return registration; } @Bean public FilterRegistrationBean<SecondFilter> secondFilter() { FilterRegistrationBean<SecondFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new SecondFilter()); registration.addUrlPatterns("/*"); registration.setOrder(2); registration.setName("secondFilter"); return registration; } }执行顺序规则
- 过滤器按照
Order值从小到大执行 -
FilterRegistrationBean 优先级高于@Order注解 - 相同顺序时,按 Bean 名称字母顺序执行
三、应用场景示例
认证和授权过滤器
@Component @Order(1) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String token = httpRequest.getHeader("Authorization"); if (!isValidToken(token)) { httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpResponse.getWriter().write("Unauthorized"); return; } // 将用户信息放入请求属性 UserInfo userInfo = parseToken(token); httpRequest.setAttribute("userInfo", userInfo); chain.doFilter(request, response); } private boolean isValidToken(String token) { // 验证 token 逻辑 return token != null && token.startsWith("Bearer "); } private UserInfo parseToken(String token) { // 解析 token 逻辑 return new UserInfo(); } }日志记录过滤器
@Component public class LoggingFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // 记录请求信息 String requestURI = httpRequest.getRequestURI(); String method = httpRequest.getMethod(); String clientIP = httpRequest.getRemoteAddr(); logger.info("Request started: {} {} from {}", method, requestURI, clientIP); long startTime = System.currentTimeMillis(); try { chain.doFilter(request, response); } finally { long duration = System.currentTimeMillis() - startTime; int status = httpResponse.getStatus(); logger.info("Request completed: {} {} - Status: {} - Duration: {}ms", method, requestURI, status, duration); } } }跨域过滤器
@Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletRequest httpRequest = (HttpServletRequest) request; // 设置跨域头 httpResponse.setHeader("Access-Control-Allow-Origin", "*"); httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); httpResponse.setHeader("Access-Control-Max-Age", "3600"); httpResponse.setHeader("Access-Control-Allow-Headers", "authorization, content-type, xsrf-token"); httpResponse.setHeader("Access-Control-Expose-Headers", "xsrf-token"); // 处理预检请求 if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { httpResponse.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(request, response); } } }请求参数处理过滤器
@Component public class RequestWrapperFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 包装请求,实现请求体重复读取 if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; // 对特定请求进行处理 if (httpRequest.getContentType() != null && httpRequest.getContentType().contains("application/json")) { CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest); chain.doFilter(cachedRequest, response); return; } } chain.doFilter(request, response); } } // 自定义可重复读取的 RequestWrapper class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { private byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); InputStream requestInputStream = request.getInputStream(); this.cachedBody = StreamUtils.copyToByteArray(requestInputStream); } @Override public ServletInputStream getInputStream() { return new CachedBodyServletInputStream(this.cachedBody); } @Override public BufferedReader getReader() { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody); return new BufferedReader(new InputStreamReader(byteArrayInputStream)); } }性能监控过滤器
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class PerformanceFilter implements Filter { private static final ThreadLocal<Long> startTimeHolder = new ThreadLocal<>(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { startTimeHolder.set(System.currentTimeMillis()); try { chain.doFilter(request, response); } finally { Long startTime = startTimeHolder.get(); if (startTime != null) { long duration = System.currentTimeMillis() - startTime; // 记录慢请求 if (duration > 1000) { // 超过1秒 HttpServletRequest httpRequest = (HttpServletRequest) request; System.err.println(String.format( "Slow request: %s %s took %dms", httpRequest.getMethod(), httpRequest.getRequestURI(), duration )); } startTimeHolder.remove(); } } } }总结
Spring Boot 过滤器是处理 HTTP 请求的强大工具,适用于:
- 全局性处理 :认证、日志、跨域等
- 性能监控 :请求耗时统计
- 数据预处理 :请求/响应数据包装
- 安全控制 :XSS 防护、SQL 注入防护
合理使用过滤器可以提升代码的复用性和可维护性,但要注意避免在过滤器中实现复杂的业务逻辑,保持职责单一。
2. 拦截器Interceptor:登录拦截、权限校验、日志记录
一、拦截器基础
创建拦截器类
@Component public class AuthInterceptor implements HandlerInterceptor { // 在Controller方法执行之前调用 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 返回true继续执行,false中断请求 return true; } // 在Controller方法执行之后,视图渲染之前调用 @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 { } }配置拦截器
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/**") // 拦截所有路径 .excludePathPatterns( // 排除路径 "/api/login", "/api/register", "/swagger-ui/**", "/v3/api-docs/**" ); } }二、登录拦截实现
登录拦截器
@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 检查是否是预检请求(OPTIONS) if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } // 2. 获取token String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token)) { token = request.getParameter("token"); } // 3. 验证token if (StringUtils.isEmpty(token)) { returnUnauthorized(response, "未提供认证令牌"); return false; } // 4. 验证token有效性(这里以Redis存储为例) String userInfo = (String) redisTemplate.opsForValue().get("token:" + token); if (StringUtils.isEmpty(userInfo)) { returnUnauthorized(response, "令牌已过期或无效"); return false; } // 5. 解析用户信息并存入请求上下文 UserDTO userDTO = JSON.parseObject(userInfo, UserDTO.class); UserContext.setCurrentUser(userDTO); // 6. 刷新token有效期 redisTemplate.expire("token:" + token, 30, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 清理线程上下文,防止内存泄漏 UserContext.clear(); } private void returnUnauthorized(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> result = new HashMap<>(); result.put("code", 401); result.put("message", message); result.put("timestamp", System.currentTimeMillis()); response.getWriter().write(JSON.toJSONString(result)); } }用户上下文工具类
public class UserContext { private static final ThreadLocal<UserDTO> userHolder = new ThreadLocal<>(); public static void setCurrentUser(UserDTO user) { userHolder.set(user); } public static UserDTO getCurrentUser() { return userHolder.get(); } public static Long getCurrentUserId() { UserDTO user = userHolder.get(); return user != null ? user.getId() : null; } public static void clear() { userHolder.remove(); } }三、权限校验实现
权限注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Permission { String[] value() default {}; Logical logical() default Logical.AND; } public enum Logical { AND, OR }权限拦截器
@Component public class PermissionInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 判断是否是HandlerMethod if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; // 2. 获取方法上的权限注解 Permission permission = handlerMethod.getMethodAnnotation(Permission.class); if (permission == null) { return true; // 没有权限注解,直接放行 } // 3. 获取当前用户 UserDTO currentUser = UserContext.getCurrentUser(); if (currentUser == null) { returnForbidden(response, "用户未登录"); return false; } // 4. 获取用户权限列表(可以从数据库或缓存获取) Set<String> userPermissions = getUserPermissions(currentUser.getId()); // 5. 校验权限 if (!checkPermission(userPermissions, permission)) { returnForbidden(response, "权限不足"); return false; } return true; } private boolean checkPermission(Set<String> userPermissions, Permission permission) { String[] requiredPermissions = permission.value(); if (requiredPermissions.length == 0) { return true; } if (permission.logical() == Logical.AND) { // 需要同时拥有所有权限 for (String required : requiredPermissions) { if (!userPermissions.contains(required)) { return false; } } return true; } else { // 只需要拥有任意一个权限 for (String required : requiredPermissions) { if (userPermissions.contains(required)) { return true; } } return false; } } private Set<String> getUserPermissions(Long userId) { // 这里可以从数据库或缓存获取用户权限 // 示例:返回模拟数据 return new HashSet<>(Arrays.asList("user:view", "user:edit")); } private void returnForbidden(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); response.setContentType("application/json;charset=UTF-8"); Map<String, Object> result = new HashMap<>(); result.put("code", 403); result.put("message", message); result.put("timestamp", System.currentTimeMillis()); response.getWriter().write(JSON.toJSONString(result)); } }使用示例
@RestController @RequestMapping("/api/user") public class UserController { @GetMapping("/list") @Permission("user:view") // 需要user:view权限 public Result listUsers() { return Result.success(userService.list()); } @PostMapping("/update") @Permission({"user:view", "user:edit"}) // 需要同时拥有这两个权限 public Result updateUser(@RequestBody User user) { return Result.success(userService.update(user)); } @DeleteMapping("/delete") @Permission(value = {"user:delete", "admin"}, logical = Logical.OR) // 拥有任意一个权限即可 public Result deleteUser(@RequestParam Long id) { return Result.success(userService.delete(id)); } }四、日志记录实现
日志拦截器
@Component public class LogInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class); private ThreadLocal<Long> startTime = new ThreadLocal<>(); private ThreadLocal<Map<String, Object>> logInfo = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 记录开始时间 startTime.set(System.currentTimeMillis()); // 收集日志信息 Map<String, Object> info = new HashMap<>(); info.put("requestTime", new Date()); info.put("requestMethod", request.getMethod()); info.put("requestURI", request.getRequestURI()); info.put("clientIP", getClientIP(request)); info.put("userAgent", request.getHeader("User-Agent")); info.put("parameters", getRequestParameters(request)); // 记录用户信息(如果已登录) UserDTO currentUser = UserContext.getCurrentUser(); if (currentUser != null) { info.put("userId", currentUser.getId()); info.put("username", currentUser.getUsername()); } logInfo.set(info); // 记录请求日志 logger.info("Request Start: {} {} from {}", request.getMethod(), request.getRequestURI(), getClientIP(request)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { Map<String, Object> info = logInfo.get(); if (info != null) { // 计算执行时间 long executionTime = System.currentTimeMillis() - startTime.get(); info.put("executionTime", executionTime); info.put("responseStatus", response.getStatus()); info.put("responseTime", new Date()); // 如果有异常,记录异常信息 if (ex != null) { info.put("exception", ex.getClass().getName()); info.put("exceptionMessage", ex.getMessage()); } // 记录完整的日志 logger.info("Request Completed: {}", JSON.toJSONString(info)); // 慢请求警告 if (executionTime > 3000) { // 超过3秒 logger.warn("Slow Request detected: {} {} took {}ms", request.getMethod(), request.getRequestURI(), executionTime); } // 这里可以将日志保存到数据库 // saveLogToDB(info); } } finally { // 清理ThreadLocal startTime.remove(); logInfo.remove(); } } 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(); } return ip; } private Map<String, String[]> getRequestParameters(HttpServletRequest request) { Map<String, String[]> params = new HashMap<>(request.getParameterMap()); // 敏感信息脱敏处理 if (params.containsKey("password")) { params.put("password", new String[]{"******"}); } if (params.containsKey("token")) { params.put("token", new String[]{"******"}); } return params; } }五、最佳实践建议
- 拦截器执行顺序 :通过
order()方法控制执行顺序,数值越小优先级越高 - 性能考虑 :
- 避免在拦截器中执行耗时操作
- 使用缓存存储频繁访问的数据
- 合理设置排除路径
- 线程安全 :
- 使用ThreadLocal存储线程相关数据
- 确保在afterCompletion中清理ThreadLocal
- 日志记录优化 :
- 使用异步方式记录日志
- 敏感信息脱敏处理
- 控制日志输出级别
- 扩展性 :
- 使用注解方式配置权限
- 支持多种认证方式(JWT、Session等)
- 可配置的权限验证逻辑
这样配置的拦截器系统可以很好地处理登录验证、权限控制和日志记录,同时保持了代码的清晰和可维护性。
3. 过滤器与拦截器区别、执行流程详解
一、核心区别对比
特性 | 过滤器 (FILTER) | 拦截器 (INTERCEPTOR) |
规范 | Servlet 规范 (J2EE 标准) | Spring MVC 框架特有 |
依赖 | 依赖 Servlet 容器 | 依赖 Spring 容器 |
作用范围 | 所有请求 (包括静态资源) | 只针对 Spring MVC 请求 |
获取对象 | ServletRequest/ServletResponse | HttpServletRequest/HttpServletResponse |
获取 Bean | 无法直接获取 Spring Bean | 可以直接获取 Spring Bean |
执行位置 | Servlet 之前/之后 | Handler 执行前后 |
异常处理 | 在 doFilter 中处理 | 在 afterCompletion 中处理 |
二、执行流程详解
完整请求处理流程
客户端请求 ↓ Servlet 容器接收 ↓ FilterChain.doFilter() 开始 ↓ 过滤器1 → 过滤器2 → ... → 过滤器N ↓ DispatcherServlet ↓ HandlerMapping 找到处理器 ↓ 拦截器1.preHandle() ↓ 拦截器2.preHandle() ↓ ... ↓ 拦截器N.preHandle() ↓ Controller 方法执行 ↓ 拦截器N.postHandle() ↓ ... ↓ 拦截器1.postHandle() ↓ 视图渲染 (如果有) ↓ 拦截器1.afterCompletion() ↓ ... ↓ 拦截器N.afterCompletion() ↓ 过滤器N → ... → 过滤器2 → 过滤器1 ↓ 响应返回客户端过滤器执行流程
// 过滤器执行顺序 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // 1. 前置处理 (在Servlet之前执行) System.out.println("Filter前置处理"); // 2. 传递给下一个过滤器或Servlet chain.doFilter(request, response); // 3. 后置处理 (在Servlet之后执行) System.out.println("Filter后置处理"); }拦截器执行流程
public class CustomInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 1. Controller方法执行前调用 // 返回true继续执行,false中断 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { // 2. Controller方法执行后,视图渲染前调用 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 3. 整个请求完成后调用 // 可用于资源清理、异常处理 } }三、代码示例
过滤器实现示例
// 方式1:使用 @Component + @Order @Component @Order(1) // 定义执行顺序 public class LogFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long start = System.currentTimeMillis(); HttpServletRequest req = (HttpServletRequest) request; // 前置处理 System.out.println("请求URI: " + req.getRequestURI()); try { chain.doFilter(request, response); } finally { // 后置处理 long duration = System.currentTimeMillis() - start; System.out.println("请求耗时: " + duration + "ms"); } } } // 方式2:使用 FilterRegistrationBean(更灵活) @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<Filter> authFilter() { FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(); registration.setFilter(new AuthFilter()); registration.addUrlPatterns("/api/*"); registration.setOrder(2); registration.setName("authFilter"); return registration; } }拦截器实现示例
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private UserService userService; // 可以注入Spring Bean @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); // 验证token if (!userService.validateToken(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("Unauthorized"); return false; // 中断请求 } // 设置用户信息到请求属性 User user = userService.getUserByToken(token); request.setAttribute("currentUser", user); return true; // 继续执行 } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { // 可以修改ModelAndView if (modelAndView != null) { modelAndView.addObject("processed", true); } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 记录异常日志 if (ex != null) { log.error("请求处理异常", ex); } } } // 注册拦截器 @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/public/**") .order(1); registry.addInterceptor(new LogInterceptor()) .addPathPatterns("/**") .order(2); } }四、使用场景建议
适合使用过滤器的场景:
- 全局跨域处理 - 所有请求都需要
- 字符编码设置 - 统一请求/响应编码
- XSS防护 - 过滤危险字符
- 请求/响应日志记录 - 记录所有请求
- 性能监控 - 统计请求耗时
- GZIP压缩 - 响应内容压缩
适合使用拦截器的场景:
- 权限验证 - 需要访问Spring Bean
- 登录检查 - 基于Session或Token
- 参数预处理 - 统一参数处理
- 接口限流 - 需要计数器等业务逻辑
- 审计日志 - 记录业务操作
- 国际化处理 - Locale解析
4. 全局跨域配置(CORS),解决前后端跨域问题
使用 @CrossOrigin 注解(控制器级别)
@RestController @RequestMapping("/api") @CrossOrigin(origins = "*") // 允许所有来源 public class MyController { // 或者可以在方法级别使用 @GetMapping("/test") @CrossOrigin(origins = "http://localhost:3000") public String test() { return "Hello"; } }实现 WebMvcConfigurer(推荐)
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 所有接口 .allowedOriginPatterns("*") // 支持通配符,Spring Boot 2.4+ // .allowedOrigins("*") // Spring Boot 2.4 之前 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); // 预检请求的有效期 } }使用 CorsFilter(过滤器方式)
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许所有域名进行跨域调用 config.addAllowedOriginPattern("*"); // 允许跨越发送cookie config.setAllowCredentials(true); // 放行全部原始头信息 config.addAllowedHeader("*"); // 允许所有请求方法跨域调用 config.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }Spring Security 中的 CORS 配置
如果使用了 Spring Security,需要额外配置:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) // 如果使用JWT等无状态认证,可以禁用CSRF .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }配置文件方式(application.yml/properties)
# application.yml spring: mvc: cors: allowed-origins: "*" allowed-methods: GET,POST,PUT,DELETE,OPTIONS allowed-headers: "*" allow-credentials: true max-age: 3600# application.properties spring.mvc.cors.allowed-origins=* spring.mvc.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS spring.mvc.cors.allowed-headers=* spring.mvc.cors.allow-credentials=true spring.mvc.cors.max-age=3600生产环境建议配置
@Configuration public class CorsConfig implements WebMvcConfigurer { @Value("${cors.allowed-origins:*}") private String[] allowedOrigins; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns(allowedOrigins) .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowedHeaders("Authorization", "Content-Type", "X-Requested-With") .exposedHeaders("Authorization") // 允许前端获取的响应头 .allowCredentials(true) .maxAge(3600); // 可以配置多个路径规则 registry.addMapping("/public/**") .allowedOriginPatterns("*") .allowedMethods("GET", "OPTIONS"); } }常见问题解决
问题1:allowCredentials 与 allowedOrigins("*") 冲突
// 错误:当 allowCredentials=true 时,不能使用 allowedOrigins("*") // 正确:使用 allowedOriginPatterns("*") 或指定具体域名 .allowedOriginPatterns("http://localhost:3000", "https://example.com") .allowCredentials(true)问题2:OPTIONS 预检请求处理
确保服务器正确处理 OPTIONS 请求,Spring Boot 默认会处理。
问题3:响应头被浏览器拦截
使用 exposedHeaders 暴露需要前端访问的响应头:
.exposedHeaders("Authorization", "X-Total-Count")建议
- 开发环境 :可以使用通配符
*简化配置 - 生产环境 :建议指定具体的域名,增强安全性
- 微服务环境 :可以在网关层统一处理 CORS
- 前后端分离项目 :推荐使用
WebMvcConfigurer方式
选择哪种方式取决于具体需求,对于大多数 Spring Boot 项目,方式2(实现 WebMvcConfigurer)是最常用且推荐的方式 。
