首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Spring Boot 3.3.x 跨域终极指南:从原理到实战,解决 99% 的前后端分离难题

Spring Boot 3.3.x 跨域终极指南:从原理到实战,解决 99% 的前后端分离难题

作者头像
果酱带你啃java
发布2026-04-14 12:38:41
发布2026-04-14 12:38:41
420
举报

为什么升级到 Spring Boot 3.3.x 后跨域突然失效?

"我只是把 Spring Boot 从 2.7.x 升到了 3.3.1,前后端分离项目的跨域就全挂了!"—— 这是最近技术社区讨论度极高的问题。作为一名深耕 Java 领域十余年的技术专家,我在主导多个大型项目升级时也遇到了同样的情况。

跨域(CORS)问题的本质是浏览器的安全机制,但 Spring Boot 3.3.x 基于 Spring Framework 6.1 + 对跨域处理进行了底层重构。根据 Spring 官方文档(https://docs.spring.io/spring-framework/docs/6.1.x/reference/html/web.html#mvc-cors),这次调整并非简单优化,而是涉及预检请求处理、凭证支持和配置优先级的根本性变更,直接导致许多旧版本的 "标准答案" 不再适用。

本文将从 HTTP 协议底层讲起,结合 Spring Boot 3.3.x 的最新特性,提供 5 种实战解决方案,每个方案都包含可直接运行的代码实例和避坑指南。无论你是刚接触跨域的新手,还是需要解决升级难题的资深开发者,都能在这里找到答案。

一、跨域核心原理:浏览器为什么要 "拦截" 你的请求?

在解决问题前,我们必须先理解 "跨域" 到底是什么。很多开发者只知道 "配置一下 allowedOrigins 就行",但遇到复杂场景就束手无策,根源在于没有掌握底层逻辑。

1.1 同源策略:浏览器的安全红线

浏览器的同源策略(Same-Origin Policy)规定:只有当两个 URL 的协议、域名、端口完全相同时,才能自由交互。例如:

  • 允许:https://example.com/api 访问 https://example.com/user(同源)
  • 禁止:https://frontend.example.com 访问 https://api.example.com(域名不同)
  • 禁止:http://example.com 访问 https://example.com(协议不同)
  • 禁止:https://example.com:80 访问 https://example.com:8080(端口不同)

这种限制是为了防止恶意网站窃取用户数据。想象一下:如果没有同源策略,黑客的网站可以随意调用银行网站的接口,后果不堪设想。

1.2 CORS 协议:跨域请求的 "通行证" 机制

当需要跨域请求时,浏览器会通过CORS(跨域资源共享)协议与服务器协商是否允许访问。整个过程分为两种场景:

1.2.1 简单请求:直接放行,事后校验

满足以下条件的请求属于简单请求,浏览器直接发送请求,然后根据响应头判断是否允许访问:

  • 请求方法为 GET、POST、HEAD
  • 请求头仅包含 Accept、Accept-Language、Content-Language、Content-Type(且值为 application/x-www-form-urlencoded、multipart/form-data、text/plain)

流程如下:

代码语言:javascript
复制


1.2.2 预检请求:先请示,再执行

不满足简单请求条件的请求(如 PUT/DELETE 方法、带自定义头、Content-Type 为 application/json),浏览器会先发送OPTIONS 预检请求,确认服务器允许后再发送真实请求:

代码语言:javascript
复制


Spring Boot 的跨域配置,本质就是通过控制服务器返回的Access-Control-*系列响应头,告诉浏览器 "是否允许当前跨域请求"。

1.3 Spring Boot 3.3.x 的跨域处理变革

Spring Boot 3.3.x 基于 Spring Framework 6.1,对跨域处理进行了三大关键调整(官方文档明确说明):

  1. 预检请求处理收紧 旧版本中未配置allowedMethods时默认允许所有方法,3.3.x 中默认仅允许 GET、POST、HEAD。若前端发送 PUT/DELETE 请求,必须显式配置允许这些方法,否则预检失败。
  2. Credentials 与通配符冲突allowCredentials=true(允许前端携带 Cookie/Token)时,allowedOrigins不能设为*(允许所有源),必须指定具体域名。这是因为浏览器不允许 "既开放所有源,又允许携带敏感凭证" 的矛盾配置。
  3. 配置优先级重构 集成 Spring Security 时,3.3.x 中 Security 的 CORS 配置会完全覆盖WebMvc 的配置(包括WebMvcConfigurer和配置文件方式),旧版本的共存逻辑不再适用。

二、环境准备:Spring Boot 3.3.x 项目基础配置

为了让所有示例可直接运行,我们先搭建一个标准项目,包含 Web、Security、MyBatis-Plus 等常用组件。

2.1 依赖配置(pom.xml)

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.1</version>
        <relativePath/>
    </parent>
    <groupId>com.jam.cors</groupId>
    <artifactId>spring-boot-33x-cors-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-33x-cors-demo</name>
    <description>Spring Boot 3.3.x跨域处理实战示例</description>

    <properties>
        <java.version>17</java.version>
        <commons-lang3.version>3.14.0</commons-lang3.version>
        <springdoc.version>2.3.0</springdoc.version>
        <mybatis-plus.version>3.5.6</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>

        <!-- 字符串工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>

        <!-- 集合工具类 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>6.1.8</version>
        </dependency>

        <!-- Swagger3 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>

        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
代码语言:javascript
复制

2.2 配置文件(application.yml)

代码语言:javascript
复制
server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cors_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root123456

mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.jam.cors.entity
  configuration:
    map-underscore-to-camel-case: true

springdoc:
  api-docs:
    path: /api-docs
  swagger-ui:
    path: /swagger-ui.html
    operationsSorter: method
  packages-to-scan: com.jam.cors.controller
代码语言:javascript
复制

2.3 数据库表结构(MySQL 8.0)

代码语言:javascript
复制
CREATE DATABASE IF NOT EXISTS cors_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE cors_demo;

-- 用户表(用于测试接口)
CREATE TABLE IF NOT EXISTS sys_user (
    id BIGINT AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    phone VARCHAR(20) NOT NULL COMMENT '手机号',
    create_time DATETIME NOT NULL COMMENT '创建时间',
    update_time DATETIME NOT NULL COMMENT '更新时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
代码语言:javascript
复制

2.4 基础实体与 VO

代码语言:javascript
复制
package com.jam.cors.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户实体类
 *
 * @author 果酱
 */
@Data
@TableName("sys_user")
@Schema(description = "用户实体")
public class User {
    @TableId(type = IdType.AUTO)
    @Schema(description = "用户ID")
    private Long id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "手机号")
    private String phone;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
}
代码语言:javascript
复制

代码语言:javascript
复制
package com.jam.cors.vo.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 用户添加请求参数
 *
 * @author 果酱
 */
@Data
@Schema(description = "用户添加请求参数")
public class UserAddReq {
    @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
    private String username;

    @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED)
    private String phone;
}
代码语言:javascript
复制

代码语言:javascript
复制
package com.jam.cors.vo.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 用户更新请求参数
 *
 * @author 果酱
 */
@Data
@Schema(description = "用户更新请求参数")
public class UserUpdateReq {
    @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long id;

    @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED)
    private String phone;
}
代码语言:javascript
复制

代码语言:javascript
复制
package com.jam.cors.vo.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一响应结果
 *
 * @author 果酱
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "统一响应结果")
public class Result<T> {
    @Schema(description = "状态码:200成功,其他失败")
    private int code;

    @Schema(description = "提示信息")
    private String message;

    @Schema(description = "响应数据")
    private T data;

    /**
     * 成功响应
     *
     * @param data 响应数据
     * @return 响应结果
     */
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }

    /**
     * 失败响应
     *
     * @param message 失败信息
     * @return 响应结果
     */
    public static <T> Result<T> fail(String message) {
        return new Result<>(500, message, null);
    }
}
代码语言:javascript
复制

2.5 测试接口(UserController)

代码语言:javascript
复制
package com.jam.cors.controller;

import com.jam.cors.entity.User;
import com.jam.cors.service.UserService;
import com.jam.cors.vo.request.UserAddReq;
import com.jam.cors.vo.request.UserUpdateReq;
import com.jam.cors.vo.response.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * 用户管理接口(用于测试跨域)
 *
 * @author 果酱
 */
@RestController
@RequestMapping("/api/user")
@Tag(name = "用户管理接口", description = "包含GET/POST/PUT/DELETE方法,用于测试跨域")
@RequiredArgsConstructor
@Slf4j
public class UserController {
    private final UserService userService;

    @GetMapping("/{id}")
    @Operation(summary = "根据ID查询用户", description = "简单请求(GET)")
    public Result<User> getUserById(
            @Parameter(description = "用户ID", required = true) @PathVariable Long id) {
        log.info("查询用户,ID:{}", id);
        return Result.success(userService.getUserById(id));
    }

    @PostMapping
    @Operation(summary = "添加用户", description = "预检请求(Content-Type=application/json)")
    public Result<Long> addUser(
            @Parameter(description = "用户信息", required = true) @RequestBody UserAddReq addReq) {
        log.info("添加用户:{}", addReq.getUsername());
        return Result.success(userService.addUser(addReq));
    }

    @PutMapping
    @Operation(summary = "更新用户", description = "预检请求(PUT方法)")
    public Result<Boolean> updateUser(
            @Parameter(description = "更新信息", required = true) @RequestBody UserUpdateReq updateReq) {
        log.info("更新用户,ID:{}", updateReq.getId());
        return Result.success(userService.updateUser(updateReq));
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "删除用户", description = "预检请求(DELETE方法)")
    public Result<Boolean> deleteUser(
            @Parameter(description = "用户ID", required = true) @PathVariable Long id) {
        log.info("删除用户,ID:{}", id);
        return Result.success(userService.deleteUser(id));
    }
}
代码语言:javascript
复制

代码语言:javascript
复制
package com.jam.cors.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.cors.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户Mapper接口
 *
 * @author 果酱
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
代码语言:javascript
复制

代码语言:javascript
复制
package com.jam.cors.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.cors.entity.User;
import com.jam.cors.vo.request.UserAddReq;
import com.jam.cors.vo.request.UserUpdateReq;

/**
 * 用户服务接口
 *
 * @author 果酱
 */
public interface UserService extends IService<User> {
    /**
     * 根据ID查询用户
     *
     * @param id 用户ID
     * @return 用户信息
     */
    User getUserById(Long id);

    /**
     * 添加用户
     *
     * @param addReq 添加参数
     * @return 新增用户ID
     */
    Long addUser(UserAddReq addReq);

    /**
     * 更新用户
     *
     * @param updateReq 更新参数
     * @return 是否成功
     */
    boolean updateUser(UserUpdateReq updateReq);

    /**
     * 删除用户
     *
     * @param id 用户ID
     * @return 是否成功
     */
    boolean deleteUser(Long id);
}
代码语言:javascript
复制

代码语言:javascript
复制
package com.jam.cors.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.cors.entity.User;
import com.jam.cors.mapper.UserMapper;
import com.jam.cors.service.UserService;
import com.jam.cors.vo.request.UserAddReq;
import com.jam.cors.vo.request.UserUpdateReq;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Objects;

/**
 * 用户服务实现类
 *
 * @author 果酱
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    public User getUserById(Long id) {
        Objects.requireNonNull(id, "用户ID不能为空");
        return getById(id);
    }

    @Override
    public Long addUser(UserAddReq addReq) {
        Objects.requireNonNull(addReq, "添加参数不能为空");
        StringUtils.hasText(addReq.getUsername(), "用户名不能为空");
        StringUtils.hasText(addReq.getPhone(), "手机号不能为空");

        User user = new User();
        user.setUsername(addReq.getUsername());
        user.setPhone(addReq.getPhone());
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());

        save(user);
        return user.getId();
    }

    @Override
    public boolean updateUser(UserUpdateReq updateReq) {
        Objects.requireNonNull(updateReq, "更新参数不能为空");
        Objects.requireNonNull(updateReq.getId(), "用户ID不能为空");
        StringUtils.hasText(updateReq.getPhone(), "手机号不能为空");

        User user = new User();
        user.setId(updateReq.getId());
        user.setPhone(updateReq.getPhone());
        user.setUpdateTime(LocalDateTime.now());

        return updateById(user);
    }

    @Override
    public boolean deleteUser(Long id) {
        Objects.requireNonNull(id, "用户ID不能为空");
        return removeById(id);
    }
}
代码语言:javascript
复制

三、方案一:全局配置类(WebMvcConfigurer)

这是最常用的跨域处理方式,通过WebMvcConfigurer配置全局跨域规则,适合大多数前后端分离场景。

3.1 配置实现

代码语言:javascript
复制
package com.jam.cors.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Spring Boot 3.3.x 全局跨域配置
 * 基于WebMvcConfigurer实现,适用于未集成Spring Security的项目
 *
 * @author 果酱
 */
@Configuration
public class GlobalCorsConfig {

    /**
     * 配置跨域规则
     *
     * @return WebMvcConfigurer实例
     */
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // 对所有路径应用跨域配置
                registry.addMapping("/**")
                        // 允许的源(生产环境必须指定具体域名,不能用*)
                        .allowedOrigins("https://frontend.example.com", "https://admin.example.com")
                        // 允许的请求方法(必须包含OPTIONS,否则预检请求失败)
                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                        // 允许的请求头(*表示允许所有,也可指定具体头)
                        .allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
                        // 是否允许携带凭证(Cookie/Token)
                        .allowCredentials(true)
                        // 预检请求的缓存时间(秒),减少预检请求次数
                        .maxAge(3600)
                        // 允许暴露的响应头(默认只能获取基本头,如需获取自定义头需配置)
                        .exposedHeaders("X-Total-Count");

                // 可为特定路径配置不同规则(优先级高于/**)
                registry.addMapping("/api/public/**")
                        .allowedOrigins("*") // 公开接口允许所有源
                        .allowedMethods("GET") // 仅允许GET方法
                        .maxAge(1800);
            }
        };
    }
}
代码语言:javascript
复制

3.2 关键配置说明

  1. allowedOrigins:指定允许的源。生产环境禁止使用*,特别是allowCredentials=true时,*会导致跨域失败(浏览器安全限制)。
  2. allowedMethods:必须显式包含OPTIONS,否则预检请求会被拦截。Spring Boot 3.3.x 默认只允许 GET、POST、HEAD,不包含 PUT/DELETE 等方法。
  3. allowCredentials:设为true时,前端请求需设置withCredentials: true(如 Axios),否则后端不会接收 Cookie/Token。
  4. maxAge:预检请求的缓存时间,建议设置 30 分钟到 1 小时,减少不必要的预检请求。

3.3 测试验证

使用前端代码测试(以 Axios 为例):

代码语言:javascript
复制
// 简单请求(GET)
axios.get('http://localhost:8080/api/user/1')
  .then(response => console.log(response.data))
  .catch(error => console.error(error));

// 预检请求(POST+JSON)
axios.post('http://localhost:8080/api/user', {
  username: 'jam',
  phone: '13800138000'
}, {
  withCredentials: true // 允许携带凭证
})
.then(response => console.log(response.data))
.catch(error => console.error(error));
代码语言:javascript
复制

打开浏览器开发者工具的 Network 面板,查看请求头和响应头:

  • 请求头应包含Origin(前端域名)
  • 响应头应包含Access-Control-Allow-Origin(与请求 Origin 匹配)、Access-Control-Allow-Credentials: true

四、方案二:配置文件方式(属性驱动)

对于规则简单的场景,可直接在application.yml中配置跨域,无需编写 Java 代码,更适合 DevOps 场景。

4.1 配置实现

代码语言:javascript
复制
spring:
  mvc:
    cors:
      mappings:
        # 全局规则(/**)
        "/**":
          # 允许的源(多个用逗号分隔)
          allowed-origins: "https://frontend.example.com,https://admin.example.com"
          # 允许的方法
          allowed-methods: "GET,POST,PUT,DELETE,OPTIONS"
          # 允许的请求头
          allowed-headers: "Content-Type,Authorization,X-Requested-With"
          # 是否允许凭证
          allow-credentials: true
          # 预检缓存时间(秒)
          max-age: 3600
          # 允许暴露的响应头
          exposed-headers: "X-Total-Count"
        # 特定路径规则(优先级更高)
        "/api/public/**":
          allowed-origins: "*"
          allowed-methods: "GET"
          max-age: 1800
代码语言:javascript
复制

4.2 配置优先级

配置文件方式的优先级规则与 Java 配置类一致:

  • 具体路径(如/api/public/**)的配置覆盖通配符路径(/**
  • 若同时配置了 Java 配置类和配置文件,Java 配置类的规则会覆盖配置文件(Spring Boot 官方文档明确说明)

4.3 适用场景

  • 跨域规则简单且固定,无需动态调整
  • 偏好通过配置文件管理环境差异(如开发 / 测试 / 生产环境的允许源不同)
  • 团队中运维人员更熟悉配置文件而非代码

五、方案三:@CrossOrigin 注解(细粒度控制)

@CrossOrigin注解用于对单个控制器或方法进行跨域配置,适合特殊接口的定制化需求。

5.1 控制器级别配置

代码语言:javascript
复制
package com.jam.cors.controller;

import com.jam.cors.entity.User;
import com.jam.cors.service.UserService;
import com.jam.cors.vo.request.UserAddReq;
import com.jam.cors.vo.request.UserUpdateReq;
import com.jam.cors.vo.response.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.CrossOrigin;

/**
 * 带跨域注解的用户控制器
 *
 * @author 果酱
 */
@RestController
@RequestMapping("/api/user")
@Tag(name = "用户管理接口", description = "控制器级别@CrossOrigin配置")
@RequiredArgsConstructor
@Slf4j
// 控制器级别跨域配置
@CrossOrigin(
    origins = {"https://frontend.example.com", "https://admin.example.com"},
    methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS},
    allowedHeaders = {"Content-Type", "Authorization"},
    allowCredentials = true,
    maxAge = 3600,
    exposedHeaders = "X-Total-Count"
)
public class UserControllerWithCrossOrigin {
    private final UserService userService;

    // 方法实现与之前相同...
    @GetMapping("/{id}")
    @Operation(summary = "根据ID查询用户")
    public Result<User> getUserById(@PathVariable Long id) {
        log.info("查询用户,ID:{}", id);
        return Result.success(userService.getUserById(id));
    }
}
代码语言:javascript
复制

5.2 方法级别配置(覆盖控制器配置)

代码语言:javascript
复制
@DeleteMapping("/{id}")
@Operation(summary = "删除用户(特殊跨域规则)")
// 方法级别配置覆盖控制器配置
@CrossOrigin(
    origins = "https://security.example.com", // 仅允许安全域名访问删除接口
    maxAge = 1800
)
public Result<Boolean> deleteUser(@PathVariable Long id) {
    log.info("删除用户,ID:{}", id);
    return Result.success(userService.deleteUser(id));
}
代码语言:javascript
复制

5.3 注意事项

  1. 注解属性与配置类对应关系@CrossOrigin的属性与CorsRegistry的方法一一对应(如origins对应allowedOrigins)。
  2. 优先级:方法级别注解 > 控制器级别注解 > 全局配置(Java / 配置文件)。
  3. 局限性@CrossOrigin注解不能配置路径模式(如/api/**),只能对注解所在的控制器 / 方法生效。

六、方案四:集成 Spring Security 的跨域配置

当项目使用 Spring Security 时,必须在 Security 配置中处理跨域,因为 Security 的过滤器链会先于 WebMvc 拦截请求。

6.1 配置实现

代码语言:javascript
复制
package com.jam.cors.config;

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;
import java.util.Arrays;

/**
 * 集成Spring Security的跨域配置
 * 注意:Security的CORS配置会覆盖WebMvc的配置
 *
 * @author 果酱
 */
@Configuration
public class SecurityCorsConfig {

    /**
     * 配置Security过滤器链
     * 必须先配置cors(),再配置其他安全规则
     *
     * @param http HttpSecurity对象
     * @return SecurityFilterChain实例
     * @throws Exception 配置异常
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 1. 配置CORS(必须放在最前面)
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            // 2. 关闭CSRF(前后端分离项目通常关闭)
            .csrf(csrf -> csrf.disable())
            // 3. 配置权限规则
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/user/**").authenticated()
                .anyRequest().authenticated()
            );

        return http.build();
    }

    /**
     * 定义CORS配置源
     *
     * @return CorsConfigurationSource实例
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        // 创建CORS配置对象
        CorsConfiguration config = new CorsConfiguration();

        // 允许的源(生产环境指定具体域名)
        config.setAllowedOrigins(Arrays.asList(
            "https://frontend.example.com", 
            "https://admin.example.com"
        ));

        // 允许的方法(包含预检请求OPTIONS)
        config.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE", "OPTIONS"
        ));

        // 允许的请求头
        config.setAllowedHeaders(Arrays.asList(
            "Content-Type", "Authorization", "X-Requested-With"
        ));

        // 允许携带凭证
        config.setAllowCredentials(true);

        // 预检请求缓存时间
        config.setMaxAge(3600L);

        // 允许暴露的响应头
        config.setExposedHeaders(Arrays.asList("X-Total-Count"));

        // 应用到所有路径
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        // 特定路径单独配置
        CorsConfiguration publicConfig = new CorsConfiguration();
        publicConfig.setAllowedOrigins(Arrays.asList("*"));
        publicConfig.setAllowedMethods(Arrays.asList("GET"));
        publicConfig.setMaxAge(1800L);
        source.registerCorsConfiguration("/api/public/**", publicConfig);

        return source;
    }
}
代码语言:javascript
复制

6.2 关键注意点

  1. 配置顺序cors()必须在其他安全配置(如csrf()authorizeHttpRequests())之前,否则跨域配置可能被安全规则覆盖。
  2. 完全覆盖性:Spring Security 的 CORS 配置会完全覆盖WebMvc 的配置(包括 Java 配置类和配置文件),这是 3.3.x 的重要变更(https://docs.spring.io/spring-security/reference/current/features/cors.html)。
  3. OPTIONS 请求放行:Security 默认不会拦截 OPTIONS 请求,但如果自定义了拦截器,需确保 OPTIONS 请求能正常到达 CORS 过滤器。

七、方案五:自定义 CORS 过滤器(动态规则场景)

对于需要动态调整跨域规则的场景(如从数据库读取允许的源),可通过自定义过滤器实现。

7.1 过滤器实现

代码语言:javascript
复制
package com.jam.cors.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

/**
 * 动态跨域过滤器
 * 适用于需要从数据库/配置中心获取跨域规则的场景
 *
 * @author 果酱
 */
@Component
@Slf4j
@Order(1) // 确保过滤器在其他过滤器之前执行
public class DynamicCorsFilter implements Filter {

    /**
     * 允许的源列表(实际项目中可从数据库/配置中心加载)
     */
    private static final List<String> ALLOWED_ORIGINS = Arrays.asList(
        "https://frontend.example.com",
        "https://admin.example.com",
        "https://mobile.example.com"
    );

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

        // 1. 获取请求源(Origin头)
        String origin = httpRequest.getHeader("Origin");
        log.debug("请求源:{},请求方法:{}", origin, httpRequest.getMethod());

        // 2. 验证源是否允许
        if (isOriginAllowed(origin)) {
            httpResponse.setHeader("Access-Control-Allow-Origin", origin);
        }

        // 3. 设置通用跨域响应头
        httpResponse.setHeader("Access-Control-Allow-Methods", 
            "GET,POST,PUT,DELETE,OPTIONS");
        httpResponse.setHeader("Access-Control-Allow-Headers", 
            "Content-Type,Authorization,X-Requested-With");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Max-Age", "3600");
        httpResponse.setHeader("Access-Control-Expose-Headers", "X-Total-Count");

        // 4. 处理预检请求(直接返回200)
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        // 5. 继续执行过滤器链
        chain.doFilter(request, response);
    }

    /**
     * 判断请求源是否允许
     * 实际项目中可从数据库或配置中心查询
     *
     * @param origin 请求源
     * @return 是否允许
     */
    private boolean isOriginAllowed(String origin) {
        // 允许空Origin(部分场景如本地文件请求)
        if (StringUtils.isBlank(origin)) {
            return true;
        }
        // 检查是否在允许列表中
        return ALLOWED_ORIGINS.contains(origin);
    }
}
代码语言:javascript
复制

7.2 动态规则扩展

在实际项目中,可将允许的源、方法等信息存储在数据库,通过定时任务或配置中心动态更新:

代码语言:javascript
复制
/**
 * 从数据库加载允许的源
 */
private List<String> loadAllowedOriginsFromDb() {
    // 实际项目中使用MyBatis-Plus查询数据库
    log.info("从数据库加载允许的跨域源");
    // 示例:return corsMapper.selectAllAllowedOrigins();
    return Arrays.asList("https://frontend.example.com", "https://admin.example.com");
}
代码语言:javascript
复制

7.3 与 Spring Security 集成

若同时使用自定义过滤器和 Spring Security,需在 Security 配置中放行过滤器:

代码语言:javascript
复制
@Override
public void configure(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(new DynamicCorsFilter(), UsernamePasswordAuthenticationFilter.class)
        // 其他配置...
}
代码语言:javascript
复制

八、常见问题与避坑指南

8.1 问题 1:配置了 allowedOrigins: "*" 但仍跨域失败

原因:当allowCredentials=true时,浏览器不允许allowedOrigins*(安全限制)。 解决:显式指定允许的域名,如allowedOrigins: ["https://frontend.example.com"]

8.2 问题 2:PUT/DELETE 请求提示跨域错误

原因:Spring Boot 3.3.x 默认不允许 PUT/DELETE 方法,需显式配置。 解决:在allowedMethods中添加PUT,DELETE,OPTIONS

8.3 问题 3:集成 Spring Security 后跨域配置失效

原因:Security 的 CORS 配置覆盖了 WebMvc 的配置,且可能未正确配置。 解决:按方案四在 Security 配置中单独配置 CORS,且确保cors()在其他配置之前。

8.4 问题 4:前端能发送请求但无法获取自定义响应头

原因:未配置exposedHeaders,浏览器默认只允许获取基本响应头。 解决:添加exposedHeaders: "X-Total-Count"(替换为你的自定义头)。

8.5 问题 5:预检请求返回 403 Forbidden

原因

  1. 未在allowedMethods中包含OPTIONS
  2. 自定义拦截器拦截了 OPTIONS 请求
  3. Spring Security 未正确放行 OPTIONS 请求

解决

  1. 确保allowedMethods包含OPTIONS
  2. 拦截器中添加对 OPTIONS 请求的放行逻辑
  3. 检查 Security 配置,确保 OPTIONS 请求不被拦截

九、总结:如何选择合适的跨域方案?

方案

适用场景

优点

缺点

全局配置类

大多数前后端分离项目,规则固定

灵活、可扩展、全局生效

需要编写代码

配置文件

规则简单,需环境差异化配置

零代码、易维护

不支持动态规则

@CrossOrigin 注解

特殊接口的定制化需求

细粒度控制

规则分散,不适合全局

Spring Security 集成

使用 Security 的项目

与安全规则联动

配置顺序要求严格

自定义过滤器

动态规则、复杂业务

高度灵活

需处理细节(如预检请求)

参考

  1. 跨域原理:W3C CORS 规范(https://www.w3.org/TR/cors/)
  2. Spring Boot 3.3.x 跨域变更:Spring Framework 6.1 文档(https://docs.spring.io/spring-framework/docs/6.1.x/reference/html/web.html#mvc-cors)
  3. Spring Security 与 CORS 集成:Spring Security 文档(https://docs.spring.io/spring-security/reference/current/features/cors.html)

通过本文的讲解,相信你已经掌握了 Spring Boot 3.3.x 跨域处理的核心原理和实战方案。记住:跨域问题的本质是浏览器的安全机制,解决问题的关键在于理解 CORS 协议的工作流程和 Spring Boot 的配置逻辑。在实际项目中,应根据具体场景选择合适的方案,并始终遵循安全最佳实践。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么升级到 Spring Boot 3.3.x 后跨域突然失效?
  • 一、跨域核心原理:浏览器为什么要 "拦截" 你的请求?
    • 1.1 同源策略:浏览器的安全红线
    • 1.2 CORS 协议:跨域请求的 "通行证" 机制
      • 1.2.1 简单请求:直接放行,事后校验
      • 1.2.2 预检请求:先请示,再执行
    • 1.3 Spring Boot 3.3.x 的跨域处理变革
  • 二、环境准备:Spring Boot 3.3.x 项目基础配置
    • 2.1 依赖配置(pom.xml)
    • 2.2 配置文件(application.yml)
    • 2.3 数据库表结构(MySQL 8.0)
    • 2.4 基础实体与 VO
    • 2.5 测试接口(UserController)
  • 三、方案一:全局配置类(WebMvcConfigurer)
    • 3.1 配置实现
    • 3.2 关键配置说明
    • 3.3 测试验证
  • 四、方案二:配置文件方式(属性驱动)
    • 4.1 配置实现
    • 4.2 配置优先级
    • 4.3 适用场景
  • 五、方案三:@CrossOrigin 注解(细粒度控制)
    • 5.1 控制器级别配置
    • 5.2 方法级别配置(覆盖控制器配置)
    • 5.3 注意事项
  • 六、方案四:集成 Spring Security 的跨域配置
    • 6.1 配置实现
    • 6.2 关键注意点
  • 七、方案五:自定义 CORS 过滤器(动态规则场景)
    • 7.1 过滤器实现
    • 7.2 动态规则扩展
    • 7.3 与 Spring Security 集成
  • 八、常见问题与避坑指南
    • 8.1 问题 1:配置了 allowedOrigins: "*" 但仍跨域失败
    • 8.2 问题 2:PUT/DELETE 请求提示跨域错误
    • 8.3 问题 3:集成 Spring Security 后跨域配置失效
    • 8.4 问题 4:前端能发送请求但无法获取自定义响应头
    • 8.5 问题 5:预检请求返回 403 Forbidden
  • 九、总结:如何选择合适的跨域方案?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档