Spring Boot 跨域(CORS)问题

一、什么是跨域问题?

1.1 同源策略

浏览器出于安全考虑,实施了同源策略(Same-Origin Policy),限制网页向不同源的服务器发送请求。

同源的定义:协议 + 域名 + 端口 完全相同

✅ 同源示例:
http://localhost:8080/api/user
http://localhost:8080/api/order

❌ 不同源示例:
http://localhost:5173  →  http://localhost:8080  (端口不同)
http://example.com     →  https://example.com    (协议不同)
http://www.a.com       →  http://www.b.com       (域名不同)

1.2 跨域场景

┌─────────────────────┐         ┌─────────────────────┐
│   前端 Vue 应用      │  HTTP   │   后端 Spring Boot   │
│  localhost:5173     │ ──────▶ │   localhost:8080    │
└─────────────────────┘  跨域!  └─────────────────────┘

当前端(如 Vue)和后端(如 Spring Boot)部署在不同端口/域名时,浏览器会阻止请求。


二、CORS 机制解析

2.1 什么是 CORS?

CORS(Cross-Origin Resource Sharing,跨域资源共享)是 W3C 标准,通过服务器设置 HTTP 响应头,告诉浏览器允许哪些跨域请求。

2.2 CORS 响应头说明

响应头 说明 示例值
Access-Control-Allow-Origin 允许的来源域名 http://localhost:5173*
Access-Control-Allow-Methods 允许的 HTTP 方法 GET, POST, PUT, DELETE
Access-Control-Allow-Headers 允许的请求头 Content-Type, Authorization
Access-Control-Allow-Credentials 是否允许携带 Cookie true / false
Access-Control-Max-Age 预检请求缓存时间(秒) 3600

2.3 简单请求 vs 预检请求

简单请求(直接发送):
┌────────┐  GET/POST  ┌────────┐
│ 浏览器  │ ─────────▶ │ 服务器  │
└────────┘            └────────┘

预检请求(先 OPTIONS 探测):
┌────────┐  OPTIONS   ┌────────┐
│ 浏览器  │ ─────────▶ │ 服务器  │  ← 预检请求
│        │ ◀───────── │        │  ← 返回 CORS 头
│        │  PUT/DELETE│        │
│        │ ─────────▶ │        │  ← 实际请求
└────────┘            └────────┘

触发预检请求的条件

  • 使用 PUTDELETEPATCH 方法
  • 自定义请求头(如 Authorization
  • Content-Type 不是 application/x-www-form-urlencodedmultipart/form-datatext/plain

三、解决方案演进

方案一:WebMvcConfigurer(基础硬编码)

最简单的方式,直接在代码中写死允许的域名。

package com.example.config;

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("/**")                    // 所有路径
                .allowedOrigins(                      // 允许的域名(硬编码)
                    "http://localhost:5173",
                    "http://127.0.0.1:5173"
                )
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)               // 允许携带 Cookie
                .maxAge(3600);                        // 预检缓存 1 小时
    }
}

优点:简单直接 缺点:修改域名需要改代码并重新部署


方案二:Filter 过滤器(精细控制)

通过过滤器手动设置响应头,适合需要更精细控制的场景。

package com.example.config;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)  // 最高优先级,确保最先执行
public class CorsFilter implements Filter {

    @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");
        if (origin != null) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        }

        // 设置其他 CORS 头
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Max-Age", "3600");

        // 动态获取请求头
        String headers = request.getHeader("Access-Control-Request-Headers");
        if (headers != null) {
            response.setHeader("Access-Control-Allow-Headers", headers);
        }

        // 预检请求直接返回
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        chain.doFilter(request, response);
    }
}

优点:可以动态处理、精细控制 缺点:允许所有来源可能有安全风险


方案三:从配置文件读取(推荐)

将允许的域名配置到 application.yml 中,无需改代码即可调整。

3.1 配置文件

# application.yml
cors:
  allowed-origins: ${APP_CORS_ORIGINS:http://localhost:5173,http://localhost:3000}

3.2 配置属性类

package com.example.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {

    /**
     * 允许的跨域来源列表
     */
    private List<String> allowedOrigins;
}

3.3 CORS 配置类

package com.example.config;

import com.example.config.properties.CorsProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {

    private final CorsProperties corsProperties;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 从配置文件读取允许的域名
        String[] origins = corsProperties.getAllowedOrigins()
                .toArray(new String[0]);

        registry.addMapping("/**")
                .allowedOrigins(origins)              // 动态配置
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

3.4 环境变量文件

# .env.dev - 开发环境
APP_CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173

# .env.prod - 生产环境
APP_CORS_ORIGINS=https://www.example.com,https://admin.example.com

优点

  • 不同环境配置不同域名
  • 无需改代码,改配置即可
  • 敏感信息不提交 Git

四、其他跨域解决方案

4.1 前端代理(开发环境)

在前端开发服务器配置代理,将请求转发到后端。

// vite.config.js (Vue 3 + Vite)
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
}
// vue.config.js (Vue 2 + Webpack)
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
}

注意:仅适用于开发环境,生产环境需要后端配置 CORS。

4.2 Nginx 反向代理(生产环境)

server {
    listen 80;
    server_name www.example.com;

    # 前端静态资源
    location / {
        root /var/www/html;
        try_files $uri $uri/ /index.html;
    }

    # 后端 API 代理
    location /api {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

优点:前后端同域,无需配置 CORS

4.3 JSONP(已过时)

通过 `` 标签加载,绕过同源策略。

// 仅支持 GET 请求,有安全风险,不推荐使用
function jsonp(url, callback) {
    const script = document.createElement('script');
    script.src = `${url}?callback=${callback}`;
    document.body.appendChild(script);
}

五、方案对比

方案 适用场景 优点 缺点
WebMvcConfigurer 硬编码 简单项目 代码简单 改域名要改代码
Filter 过滤器 需要精细控制 灵活度高 代码较多
配置文件读取 生产项目(推荐) 灵活、安全 需要配置类
前端代理 开发环境 前端独立配置 仅开发环境
Nginx 代理 生产环境 彻底解决跨域 需要运维配置

六、常见问题

6.1 allowCredentials 与 allowedOrigins

// ❌ 错误:allowCredentials=true 时不能用 *
.allowedOrigins("*")
.allowCredentials(true)

// ✅ 正确:必须指定具体域名
.allowedOrigins("http://localhost:5173")
.allowCredentials(true)

// ✅ 或者使用 allowedOriginPatterns
.allowedOriginPatterns("*")
.allowCredentials(true)

6.2 预检请求 401/403

如果使用了 Security 或拦截器,需要放行 OPTIONS 请求:

// Spring Security 配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors()  // 启用 CORS
        .and()
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()  // 放行预检
            .anyRequest().authenticated()
        );
    return http.build();
}

6.3 多个 CORS 配置冲突

确保只有一个 CORS 配置生效:

// ❌ 不要同时使用 @CrossOrigin 和全局配置
@CrossOrigin(origins = "http://localhost:5173")
@RestController
public class UserController { }

// ✅ 统一使用全局配置

七、推荐配置(完整示例)

项目结构

src/main/java/com/example/
├── config/
│   ├── CorsConfig.java           # CORS 配置
│   └── properties/
│       └── CorsProperties.java   # CORS 属性
└── resources/
    ├── application.yml           # 主配置
    ├── application-dev.yml       # 开发环境
    └── application-prod.yml      # 生产环境

项目根目录/
├── .env.dev                      # 开发环境变量
└── .env.prod                     # 生产环境变量

完整代码

CorsProperties.java

@Data
@Component
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
    private List<String> allowedOrigins = new ArrayList<>();
}

CorsConfig.java

@Configuration
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {

    private final CorsProperties corsProperties;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0]))
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

也可以不需要CorsProperties.java,直接CorsConfig.java

不需要为了"规范"而过度设计。等项目真正需要更多 CORS 配置项时,再重构成 Properties 类也不迟。

CorsConfig.java

package com.zwnsyw.zwwwspringbootbasetemplate.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 跨域资源共享(CORS)配置
 * <p>
 * 解决前后端分离项目中的跨域访问问题
 * </p>
 *
 * @author your-name
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * 允许的前端域名列表(从配置文件读取)
     * <p>
     * 默认值为 "*"(允许所有域名)
     * 注意:使用 allowedOriginPatterns 而非 allowedOrigins,
     *       因此即使设置 allowCredentials=true 也支持通配符
     * </p>
     */
    @Value("${cors.allowed-origins:*}")
    private String[] allowedOrigins;

    /**
     * 预检请求缓存时间(秒)
     * <p>
     * 浏览器会缓存 OPTIONS 预检请求的结果,避免频繁发送预检请求
     * </p>
     */
    private static final long MAX_AGE_SECONDS = 3600;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                // 对所有路径生效
                .addMapping("/**")

                // 允许携带 Cookie 和认证信息
                // 注意:设为 true 时,allowedOrigins 不能为 "*"
                .allowCredentials(true)

                // 允许的请求来源
                // 方式1:使用 allowedOrigins 精确匹配
                // .allowedOrigins("http://localhost:5173", "https://example.com")

                // 方式2:使用 allowedOriginPatterns 支持通配符
                .allowedOriginPatterns(allowedOrigins)

                // 允许的 HTTP 方法
                .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")

                // 允许的请求头
                .allowedHeaders("*")
                // 或者精确指定:
                // .allowedHeaders("Content-Type", "Authorization", "X-Requested-With")

                // 允许前端访问的响应头
                .exposedHeaders("X-Total-Count", "Content-Disposition")

                // 预检请求缓存时间
                .maxAge(MAX_AGE_SECONDS);
    }
}

application.yml

cors:
  allowed-origins: ${APP_CORS_ORIGINS:http://localhost:5173}

.env.dev

APP_CORS_ORIGINS=http://localhost:5173,http://localhost:3000

八、总结

  1. 开发环境:前端代理 + 后端 CORS 配置
  2. 生产环境:Nginx 反向代理 或 后端 CORS 配置
  3. 推荐方案:从配置文件/环境变量读取允许的域名
  4. 核心原则:跨域由后端通过响应头控制,前端无法绕过

项目分区导航:⬅️ 01-缓存使用最佳实践指南 | 02-Spring Boot 跨域(CORS)问题 | ➡️ 01-登录鉴权