Spring Boot跨域问题终结者!3种配置方式让前后端联调不再头疼

Spring Boot跨域问题终结者!3种配置方式让前后端联调不再头疼

今天我们来聊聊一个让无数Java开发者头疼的问题——跨域请求(CORS)。相信很多小伙伴在前后端分离开发时都遇到过这个问题:

你是否遇到过这样的困扰:

  • 前端同学抱怨接口调用报错:"Access-Control-Allow-Origin"问题
  • 后端接口明明没问题,但浏览器就是不认
  • 网上查了一堆解决方案,试了还是不行
  • 项目上线后跨域问题又出现了

别急,今天这篇文章就带你彻底搞懂Spring Boot中的CORS配置,让你从此告别跨域烦恼!

CORS到底是什么鬼?

在深入解决方案之前,我们先来理解一下CORS到底是什么,为什么会出现这个问题。

什么是CORS?

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种浏览器安全机制,用于限制一个域下的文档或脚本如何与另一个域下的资源进行交互。

什么是"域"?

在Web开发中,"域"由以下三个部分组成:

只有这三个部分完全相同,才被认为是"同域"。比如:

  • http://localhost:8080http://localhost:8081跨域
  • http://www.example.comhttps://www.example.com跨域
  • http://www.example.comhttp://api.example.com跨域

为什么会有CORS限制?

这是浏览器的一种安全保护机制,叫做同源策略。它的目的是防止恶意网站窃取用户数据。

举个例子:
如果你在恶意网站上,它不能直接访问你的银行网站获取账户信息,因为它们是不同域的。

常见的CORS错误信息

// 前端控制台常见的错误信息
"Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present 
on the requested resource."

// 或者
"Access to XMLHttpRequest at 'http://localhost:8080/api/login' from origin 
'http://localhost:3000' has been blocked by CORS policy: Response to preflight 
request doesn't pass access control check: It does not have HTTP ok status."

CORS请求的两种类型

  1. 简单请求:满足特定条件的请求,浏览器直接发送
  2. 预检请求:复杂请求会先发送OPTIONS请求询问服务器是否允许

简单请求的条件:

  • GET、POST、HEAD方法之一
  • 只包含安全的请求头(如Accept、Accept-Language、Content-Language等)
  • Content-Type只能是:application/x-www-form-urlencoded、multipart/form-data、text/plain

预检请求的条件:

  • PUT、DELETE等方法
  • 包含自定义请求头
  • Content-Type为application/json等

理解了这些基础知识,我们就可以开始配置Spring Boot来解决CORS问题了!
]

Spring Boot CORS配置方法详解

在Spring Boot中,有多种方式可以解决CORS问题。我们来逐一介绍这些方法,并分析它们的适用场景。

方法一:全局配置CORS(推荐)

这是最常用也是最推荐的方式,通过配置类来全局设置CORS规则。

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("*") // 允许所有域名访问
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方法
                .allowedHeaders("*") // 允许的请求头
                .allowCredentials(true) // 是否允许携带cookie
                .maxAge(3600); // 预检请求的有效期(秒)
    }
}

配置说明:

  • addMapping("/**"):匹配所有接口路径
  • allowedOriginPatterns("*"):允许所有域名访问(Spring Boot 2.4+推荐使用这个)
  • allowedMethods():允许的HTTP方法
  • allowedHeaders("*"):允许的请求头
  • allowCredentials(true):允许携带认证信息(如Cookie)
  • maxAge(3600):预检请求缓存时间

方法二:使用@CrossOrigin注解

在具体的Controller或方法上使用注解,适用于局部配置。

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000") // 在类级别配置
public class UserController {
    
    @GetMapping("/users")
    @CrossOrigin(
        origins = {"http://localhost:3000", "http://localhost:4200"},
        methods = {RequestMethod.GET, RequestMethod.POST},
        allowedHeaders = "*",
        allowCredentials = "true"
    ) // 在方法级别配置,会覆盖类级别的配置
    public List<User> getUsers() {
        // 返回用户列表
        return userService.getAllUsers();
    }
    
    @PostMapping("/users")
    public User createUser(@RequestBody User user) {
        // 创建用户
        return userService.createUser(user);
    }
    
    @DeleteMapping("/users/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        // 删除用户
        userService.deleteUser(id);
        return ResponseEntity.ok().build();
    }
}

注解参数说明:

  • origins:允许的源域名
  • methods:允许的HTTP方法
  • allowedHeaders:允许的请求头
  • allowCredentials:是否允许携带认证信息
  • maxAge:预检请求缓存时间

方法三:使用Filter过滤器

通过自定义过滤器来处理CORS请求,这种方式更加灵活。

import org.springframework.stereotype.Component;
import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Order(-1) // 确保过滤器优先级最高
public class CorsFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        
        // 添加CORS响应头
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", 
                          "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", 
                          "Content-Type, Authorization, X-Requested-With");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        
        // 处理预检请求
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }
}

方法四:通过application.yml配置

在配置文件中也可以进行CORS配置:

# application.yml
spring:
  web:
    cors:
      allowed-origins: 
        - "http://localhost:3000"
        - "http://localhost:4200"
      allowed-methods:
        - GET
        - POST
        - PUT
        - DELETE
        - OPTIONS
      allowed-headers: "*"
      allow-credentials: true
      max-age: 3600

方法五:使用WebFlux配置(适用于响应式编程)

如果你使用的是Spring WebFlux,配置方式略有不同:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsWebFluxConfig {
    
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = 
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsWebFilter(source);
    }
}

各种方法的对比和选择建议

配置方式优点缺点适用场景
全局配置配置简单,统一管理灵活性较差项目统一跨域策略
@CrossOrigin注解灵活,可精确控制配置分散,维护困难特定接口需要不同策略
Filter过滤器最灵活,可自定义逻辑代码复杂度高复杂的跨域处理逻辑
配置文件非侵入式,易于修改功能相对简单简单的跨域需求
WebFlux配置适用于响应式编程仅限WebFlux项目响应式Spring Boot项目

推荐使用顺序:

  1. 首选:全局配置类(方法一)
  2. 特殊情况:@CrossOrigin注解(方法二)
  3. 复杂需求:Filter过滤器(方法三)

实际应用案例和最佳实践

让我们通过几个实际的业务场景来看看如何在项目中正确使用CORS配置。

案例一:电商项目前后端分离架构

// 电商项目CORS配置
@Configuration
public class ECommerceCorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**") // 只对API接口开启跨域
                .allowedOriginPatterns(
                    "http://localhost:3000",      // 开发环境前端地址
                    "http://localhost:4200",      // Angular开发环境
                    "https://shop.example.com",   // 生产环境前端地址
                    "https://admin.example.com"   // 管理后台地址
                )
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true) // 允许携带Cookie认证
                .maxAge(3600);
        
        // 文件上传接口可能需要更宽松的配置
        registry.addMapping("/upload/**")
                .allowedOriginPatterns("*")
                .allowedMethods("POST", "OPTIONS")
                .allowedHeaders("*")
                .maxAge(1800);
    }
}

// 用户控制器
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "https://admin.example.com") // 管理后台专用
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // 获取用户列表(管理后台专用)
    @GetMapping
    public ResponseEntity<List<User>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        List<User> users = userService.getUsers(page, size);
        return ResponseEntity.ok(users);
    }
    
    // 用户注册(所有前端都可以访问)
    @PostMapping("/register")
    public ResponseEntity<User> register(@RequestBody UserRegistrationDto dto) {
        User user = userService.register(dto);
        return ResponseEntity.ok(user);
    }
    
    // 用户登录
    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginDto dto) {
        LoginResponse response = userService.login(dto);
        return ResponseEntity.ok(response);
    }
}

// 订单控制器
@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    // 创建订单(需要用户认证)
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody CreateOrderDto dto,
                                           HttpServletRequest request) {
        // 从请求中获取用户信息
        String userId = (String) request.getAttribute("userId");
        Order order = orderService.createOrder(userId, dto);
        return ResponseEntity.ok(order);
    }
    
    // 获取用户订单(需要用户认证)
    @GetMapping("/my")
    public ResponseEntity<List<Order>> getMyOrders(HttpServletRequest request) {
        String userId = (String) request.getAttribute("userId");
        List<Order> orders = orderService.getUserOrders(userId);
        return ResponseEntity.ok(orders);
    }
}

案例二:微服务架构中的CORS处理

// 网关层统一处理CORS
@Configuration
public class GatewayCorsConfig {
    
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        // 根据不同环境设置允许的源
        if (isDevEnvironment()) {
            config.addAllowedOriginPattern("*");
        } else {
            config.addAllowedOriginPattern("https://*.example.com");
            config.addAllowedOriginPattern("https://example.com");
        }
        
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = 
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsWebFilter(source);
    }
    
    private boolean isDevEnvironment() {
        String env = System.getenv("SPRING_PROFILES_ACTIVE");
        return "dev".equals(env) || "local".equals(env);
    }
}

// 各个微服务中禁用CORS(由网关统一处理)
@Configuration
public class MicroserviceCorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 微服务之间调用不需要CORS,由API网关统一处理
        // registry.addMapping("/**").allowedOrigins(); // 留空或注释掉
    }
}

案例三:多域名支持的复杂配置

// 复杂的多域名CORS配置
@Configuration
public class ComplexCorsConfig implements WebMvcConfigurer {
    
    @Value("${app.cors.allowed-origins:}")
    private String[] allowedOrigins;
    
    @Value("${app.cors.allowed-origins-patterns:}")
    private String[] allowedOriginPatterns;
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        CorsRegistration corsRegistration = registry.addMapping("/api/**")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("X-Total-Count", "X-Pagination")
                .allowCredentials(true)
                .maxAge(3600);
        
        // 根据配置决定使用allowedOrigins还是allowedOriginPatterns
        if (allowedOrigins != null && allowedOrigins.length > 0) {
            corsRegistration.allowedOrigins(allowedOrigins);
        } else if (allowedOriginPatterns != null && allowedOriginPatterns.length > 0) {
            corsRegistration.allowedOriginPatterns(allowedOriginPatterns);
        } else {
            // 默认配置
            corsRegistration.allowedOriginPatterns(
                "http://localhost:*",
                "https://*.example.com",
                "https://example.com"
            );
        }
    }
}

// application.yml配置
/*
app:
  cors:
    allowed-origins-patterns: 
      - "http://localhost:*"
      - "https://*.example.com"
      - "https://example.com"
    allowed-origins:
      # - "http://localhost:3000"
      # - "https://shop.example.com"
*/

最佳实践建议

1. 安全性考虑

// 安全的CORS配置示例
@Configuration
public class SecureCorsConfig implements WebMvcConfigurer {
    
    @Value("${app.env:prod}")
    private String environment;
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        CorsRegistration registration = registry.addMapping("/api/**")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS")
                .allowedHeaders(
                    "Authorization",
                    "Content-Type",
                    "X-Requested-With",
                    "Accept",
                    "Origin",
                    "Access-Control-Request-Method",
                    "Access-Control-Request-Headers"
                )
                .exposedHeaders("Authorization", "X-Total-Count")
                .maxAge(3600);
        
        // 根据环境设置不同的源策略
        if ("dev".equals(environment) || "local".equals(environment)) {
            // 开发环境允许所有源
            registration.allowedOriginPatterns("*");
            registration.allowCredentials(false); // 开发环境不允 许凭证
        } else {
            // 生产环境只允许特定源
            registration.allowedOriginPatterns(
                "https://shop.example.com",
                "https://admin.example.com",
                "https://mobile.example.com"
            );
            registration.allowCredentials(true);
        }
    }
}

2. 性能优化

// 高性能CORS配置
@Configuration
public class PerformanceCorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 为不同类型的接口设置不同的CORS策略
        
        // API接口 - 标准配置
        registry.addMapping("/api/**")
                .allowedOriginPatterns("https://*.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("Authorization", "Content-Type")
                .allowCredentials(true)
                .maxAge(3600); // 长缓存时间
        
        // 公开接口 - 更宽松的配置
        registry.addMapping("/public/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET")
                .allowedHeaders("Content-Type")
                .maxAge(86400); // 24小时缓存
        
        // 文件上传接口 - 特殊配置
        registry.addMapping("/upload/**")
                .allowedOriginPatterns("https://*.example.com")
                .allowedMethods("POST")
                .allowedHeaders("Content-Type", "X-File-Name")
                .maxAge(1800);
    }
}

3. 日志记录和监控

// 带日志记录的CORS过滤器
@Component
@Order(-2)
public class LoggingCorsFilter implements Filter {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingCorsFilter.class);
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        
        String origin = request.getHeader("Origin");
        String method = request.getMethod();
        
        // 记录CORS请求
        if (origin != null) {
            logger.info("CORS request from origin: {}, method: {}", origin, method);
        }
        
        // 设置CORS头
        response.setHeader("Access-Control-Allow-Origin", 
                          getAllowedOrigin(origin));
        response.setHeader("Access-Control-Allow-Methods", 
                          "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", 
                          "Authorization, Content-Type, X-Requested-With");
        
        // 处理预检请求
        if ("OPTIONS".equalsIgnoreCase(method)) {
            response.setStatus(HttpServletResponse.SC_OK);
            logger.debug("Handled preflight request from: {}", origin);
            return;
        }
        
        chain.doFilter(req, res);
    }
    
    private String getAllowedOrigin(String origin) {
        // 根据业务逻辑判断是否允许该源
        Set<String> allowedOrigins = Set.of(
            "https://shop.example.com",
            "https://admin.example.com"
        );
        
        return allowedOrigins.contains(origin) ? origin : "null";
    }
}

常见问题排查和解决方案

兄弟们,在实际开发过程中,即使我们按照标准配置了CORS,还是可能会遇到一些让人头疼的问题。今天我就来给大家梳理一下最常见的几个坑,以及对应的解决方案。

1. 配置不生效问题

这是最常见的情况,你明明配置了CORS,但前端还是报跨域错误。这时候我们得按以下步骤来排查:

首先,检查你的配置是否真的被加载了。可以用这个小技巧:

@Component
public class CorsConfigChecker implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("CORS配置已加载!");
    }
}

如果控制台没打印这条信息,说明你的配置类可能没有被Spring扫描到。检查一下:

  • 配置类上是否有@Configuration注解
  • 是否在Spring Boot启动类的扫描路径下
  • 包名是否正确

2. 预检请求403错误

当遇到OPTIONS请求返回403时,通常是因为我们的拦截器或过滤器把预检请求给拦住了。解决方案是在拦截器中放行OPTIONS请求:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 放行OPTIONS预检请求
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
        response.setStatus(HttpServletResponse.SC_OK);
        return true;
    }
    // 其他请求继续处理
    return super.preHandle(request, response, handler);
}

3. Cookie无法传递问题

如果你需要携带Cookie进行跨域请求,光配置allowCredentials(true)还不够,前端也要配合设置:

// 前端axios配置
axios.defaults.withCredentials = true;

同时后端要注意,当设置了allowCredentials(true)时,allowedOrigins不能使用通配符"*",必须指定具体的域名:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://www.yourdomain.com") // 注意这里不能用*
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

4. 多个域名配置问题

有时候我们需要支持多个域名访问,这时候可以这样配置:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOriginPatterns("*") // 使用Pattern匹配多个域名
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

或者更精确地控制:

.allowedOrigins("https://www.domain1.com", "https://www.domain2.com")

5. 请求头限制问题

如果前端需要传递自定义请求头,比如X-Requested-With,后端也需要明确允许:

registry.addMapping("/api/**")
        .allowedOrigins("https://www.yourdomain.com")
        .allowedMethods("GET", "POST", "PUT", "DELETE")
        .allowedHeaders("*") // 允许所有请求头
        .exposedHeaders("Authorization") // 暴露特定响应头给前端
        .allowCredentials(true);

6. 调试技巧分享

最后分享一个小技巧,如何快速验证CORS配置是否正确。可以在浏览器控制台执行以下命令:

fetch('http://localhost:8080/api/test', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({test: 'data'})
})
.then(response => console.log(response))
.catch(error => console.error('Error:', error));

如果能看到正确的响应而不是跨域错误,说明配置就成功了!

好啦,常见的问题和解决方案就这些。大家在开发中遇到其他问题,欢迎在评论区留言讨论,我们一起解决!


标题:Spring Boot跨域问题终结者!3种配置方式让前后端联调不再头疼
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/21/1766304289183.html

    0 评论
avatar