Spring Boot 跨域解决方案详解:从原理到实战(初学者友好版)
结合我们之前学习的 Spring Boot Web 开发、拦截器等知识,本文将聚焦跨域问题—— 这是前后端分离架构中最常见的痛点之一。我们会从 “什么是跨域”“为什么会有跨域问题” 入手,详细拆解两种核心解决方案(@CrossOrigin注解、全局 CORS 配置),搭配完整的前后端实战代码,帮我们彻底搞懂跨域配置的每一个细节,避免初学者常见的 “配置了但不生效” 问题。
一、先搞懂基础:什么是跨域?为什么会有跨域问题?
在学解决方案前,我们必须先理解 “跨域” 的本质 —— 它不是后端的问题,而是浏览器的安全限制导致的。
1.1 同源策略:跨域问题的根源
浏览器为了防止恶意网站窃取数据,制定了 “同源策略”:只有当请求的 “协议、域名、端口” 三者完全相同时,才允许发送 AJAX 请求,否则就是 “跨域请求”,浏览器会拦截响应。
| 对比维度 | 示例 1(当前页面) | 示例 2(请求目标) | 是否同源 | 结论(是否跨域) |
|---|---|---|---|---|
| 协议 | http://localhost:8080 | http://localhost:8081 | 是 | 否(端口不同) |
| 域名 | http://localhost:8080 | http://127.0.0.1:8080 | 否(域名不同) | 是 |
| 端口 | https://baidu.***:80 | https://baidu.***:443 | 否(端口不同) | 是 |
| 三者相同 | http://localhost:8080 | http://localhost:8080 | 是 | 否 |
我们的实际场景:前端项目运行在http://localhost:8080(比如 Vue/React 项目),后端 Spring Boot 项目运行在http://localhost:8081,此时前端发送 AJAX 请求到后端,就会触发跨域拦截。
1.2 跨域报错:浏览器的典型提示
当没有配置 CORS 时,前端发送跨域请求,浏览器控制台会报类似错误:
A***ess to fetch at 'http://localhost:8081/api/hello' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'A***ess-Control-Allow-Origin' header is present on the requested resource.
-
错误原因:后端响应中没有包含
A***ess-Control-Allow-Origin头,浏览器认为这个跨域请求不安全,拒绝接收响应。
1.3 CORS 机制:解决跨域的标准方案
CORS(Cross-Origin Resource Sharing,跨域资源共享)是 W3C 制定的标准,允许后端通过HTTP 响应头告诉浏览器:“这个源的请求是安全的,你可以接收响应”。
核心原理:
-
前端发送跨域请求时,浏览器会先发送一个 “预检请求(OPTIONS 请求)”,询问后端 “是否允许这个源的请求”;
-
后端返回包含 CORS 头的响应(如
A***ess-Control-Allow-Origin: http://localhost:8080); -
浏览器验证 CORS 头,如果允许,则发送真正的请求(GET/POST 等),否则拦截。
二、Spring Boot 跨域解决方案一:@CrossOrigin 注解(方法级配置)
@CrossOrigin是 Spring 提供的声明式注解,可以直接加在 Controller 方法或类上,快速开启单个接口或单个 Controller 的跨域支持,适合简单场景。
2.1 注解作用与参数
| 参数名 | 作用 | 示例值 | 默认值 |
|---|---|---|---|
origins |
允许跨域的源(协议 + 域名 + 端口) | {"http://localhost:8080", "https://www.example.***"} |
*(允许所有源,不推荐生产环境) |
allowedMethods |
允许跨域的 HTTP 方法 | {"GET", "POST", "PUT", "DELETE"} |
允许请求本身的方法(如 GET 请求只允许 GET) |
allowedHeaders |
允许跨域请求携带的请求头 | {"Content-Type", "Authorization"} |
*(允许所有请求头) |
allowCredentials |
是否允许携带 Cookie(如登录态) |
true/false
|
false(不允许) |
maxAge |
预检请求的缓存时间(秒),避免重复预检 |
3600(1 小时) |
1800(30 分钟) |
2.2 实战 1:方法级配置(单个接口跨域)
我们在后端 Controller 的单个方法上添加@CrossOrigin,允许http://localhost:8080的跨域请求:
步骤 1:后端 Controller 代码
package ***.Lh.corsdemo.controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api") // 接口统一前缀
public class ApiController {
// 方法级跨域配置:只允许http://localhost:8080的GET请求
@CrossOrigin(
origins = "http://localhost:8080", // 允许的源
allowedMethods = "GET", // 允许的方法
allowCredentials = true, // 允许携带Cookie
maxAge = 3600 // 预检请求缓存1小时
)
@GetMapping("/hello")
public String sayHello() {
// 返回简单字符串,供前端跨域请求
return "Hello from Spring Boot! 这是跨域GET请求的响应";
}
}
步骤 2:前端测试代码(HTML + fetch)
创建cross-test.html,运行在http://localhost:8080(比如用另一个 Spring Boot 项目的templates目录,或用 Nginx 部署):
<!DOCTYPE html>
<html lang="zh-***">
<head>
<meta charset="UTF-8">
<title>跨域测试(方法级配置)</title>
</head>
<body>
<h1>跨域GET请求测试</h1>
<button onclick="sendGetRequest()">发送跨域GET请求</button>
<div id="result" style="margin-top: 20px; color: green;"></div>
<script>
function sendGetRequest() {
// 发送跨域请求到后端8081端口的/api/hello
fetch('http://localhost:8081/api/hello', {
method: 'GET',
credentials: 'include' // 必须加,否则Cookie不会携带(对应后端allowCredentials=true)
})
.then(response => {
if (!response.ok) {
throw new Error('跨域请求失败');
}
return response.text();
})
.then(data => {
// 显示响应结果
document.getElementById('result').innerText = '响应成功:' + data;
})
.catch(error => {
document.getElementById('result').innerText = '响应失败:' + error.message;
document.getElementById('result').style.color = 'red';
});
}
</script>
</body>
</html>
步骤 3:测试流程
-
启动后端项目,端口 8081;
-
启动前端项目(端口 8080),访问
http://localhost:8080/cross-test.html; -
点击 “发送跨域 GET 请求”,页面显示 “响应成功:Hello from Spring Boot! 这是跨域 GET 请求的响应”,证明跨域配置生效。
2.3 实战 2:类级配置(整个 Controller 跨域)
如果一个 Controller 的所有接口都需要跨域,可以把@CrossOrigin加在 Controller 类上,作用于所有方法:
// 类级跨域配置:整个ApiController的所有接口都允许跨域
@CrossOrigin(origins = "http://localhost:8080", allowCredentials = true)
@RestController
@RequestMapping("/api")
public class ApiController {
// 无需再加@CrossOrigin,继承类上的配置
@GetMapping("/hello")
public String sayHello() {
return "Hello from Class-Level CORS!";
}
// 无需再加@CrossOrigin
@PostMapping("/user")
public String createUser() {
return "User created su***essfully!";
}
}
2.4 适用场景与局限性
| 适用场景 | 局限性 |
|---|---|
| 单个接口或单个 Controller 需要跨域 | 不适合多个 Controller,需重复加注解 |
| 跨域规则简单(固定源、固定方法) | 生产环境中origins="*"不安全,且不支持携带 Cookie |
| 快速测试跨域功能 | 无法统一管理跨域规则,维护成本高 |
三、Spring Boot 跨域解决方案二:全局 CORS 配置(推荐)
对于多 Controller、复杂跨域规则的项目,推荐用全局 CORS 配置—— 通过实现WebMv***onfigurer接口的addCorsMappings方法,统一配置所有接口的跨域规则,一次配置,全局生效。
3.1 全局配置的核心参数(与 @CrossOrigin 一致)
全局配置的参数和@CrossOrigin完全对应,只是配置方式从注解变成了代码,核心参数:
-
addMapping("/**"):对所有接口生效(/**表示所有路径,也可指定/api/**只对 /api 前缀接口生效); -
allowedOrigins("http://localhost:8080"):允许的源; -
allowedMethods("GET", "POST", "PUT", "DELETE"):允许的 HTTP 方法; -
allowedHeaders("*"):允许的请求头(如Content-Type、Authorization); -
allowCredentials(true):允许携带 Cookie; -
maxAge(3600):预检请求缓存 1 小时。
3.2 实战:全局 CORS 配置(完整后端代码)
我们搭建一个完整的后端项目,包含启动类、全局配置类、Controller、实体类,实现 GET 和 POST 跨域请求。
步骤 1:项目结构
cors-demo(项目根目录) ├─ src/main/java/***/zh/corsdemo/ │ ├─ CorsDemoApplication.java(启动类) │ ├─ config/WebConfig.java(全局CORS配置) │ ├─ controller/ApiController.java(接口) │ └─ entity/User.java(实体类) └─ src/main/resources/application.yml(配置文件)
步骤 2:启动类(CorsDemoApplication.java)
package ***.lh.corsdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CorsDemoApplication {
public static void main(String[] args) {
// 启动后端项目,端口8081(在application.yml中配置)
SpringApplication.run(CorsDemoApplication.class, args);
}
}
步骤 3:全局 CORS 配置类(WebConfig.java)
package ***.lh.corsdemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMv***onfigurer;
@Configuration // 标记为配置类,Spring启动时加载
public class WebConfig implements WebMv***onfigurer {
// 重写addCorsMappings,配置全局跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 1. 对所有接口生效
.allowedOrigins("http://localhost:8080") // 2. 允许的前端源(生产环境用具体域名,如"https://www.zh.***")
.allowedMethods("GET", "POST", "PUT", "DELETE") // 3. 允许的HTTP方法
.allowedHeaders("*") // 4. 允许的请求头(如Content-Type、Authorization)
.allowCredentials(true) // 5. 允许携带Cookie(登录态需要)
.maxAge(3600); // 6. 预检请求缓存1小时,减少OPTIONS请求次数
}
}
步骤 4:实体类(User.java)
用于接收前端 POST 请求的参数:
package ***.lh.corsdemo.entity;
import lombok.Data;
// Lombok注解:自动生成Getter/Setter/toString
@Data
public class User {
private Long id; // 用户ID(后端生成)
private String name; // 用户名(前端传入)
private String email; // 邮箱(前端传入)
}
步骤 5:Controller(ApiController.java)
提供 GET 和 POST 接口,无需加@CrossOrigin(全局配置已生效):
package ***.lh.corsdemo.controller;
import ***.lh.corsdemo.entity.User;
import org.springframework.web.bind.annotation.*;
@RestController
public class ApiController {
// 1. 跨域GET接口:返回简单字符串
@GetMapping("/api/hello")
public String sayHello() {
return "Hello from Global CORS! 这是跨域GET响应";
}
// 2. 跨域POST接口:接收JSON参数,返回创建的User
@PostMapping("/user")
public User createUser(@RequestBody User user) {
// 模拟后端生成ID
user.setId(1L);
// 返回包含ID的User对象
return user;
}
}
步骤 6:配置文件(application.yml)
设置后端端口为 8081:
server: port: 8081 # 后端端口,与前端8080区分,避免冲突
3.3 前端实战:完整测试页面(GET+POST)
创建global-cors-test.html,运行在http://localhost:8080,测试两种跨域请求:
<!DOCTYPE html>
<html lang="zh-***">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>全局CORS跨域测试</title>
<style>
.section { margin: 30px 0; padding: 20px; border: 1px solid #eee; border-radius: 8px; }
button { padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #45a049; }
.result { margin-top: 15px; padding: 10px; border-left: 4px solid green; }
.error { border-left-color: red; color: red; }
.form-group { margin: 10px 0; }
input { padding: 8px; width: 300px; }
</style>
</head>
<body>
<div class="container" style="max-width: 800px; margin: 0 auto;">
<h1>全局CORS跨域测试(8080 → 8081)</h1>
<!-- 1. GET请求测试 -->
<div class="section">
<h2>1. GET请求测试</h2>
<button onclick="sendGetRequest()">发送GET请求</button>
<div id="getResult" class="result"></div>
</div>
<!-- 2. POST请求测试 -->
<div class="section">
<h2>2. POST请求测试(提交用户信息)</h2>
<div class="form-group">
<label>用户名:</label>
<input type="text" id="name" placeholder="输入用户名">
</div>
<div class="form-group">
<label>邮箱:</label>
<input type="text" id="email" placeholder="输入邮箱">
</div>
<button onclick="sendPostRequest()">发送POST请求</button>
<div id="postResult" class="result"></div>
</div>
</div>
<script>
// 1. 发送跨域GET请求
function sendGetRequest() {
fetch('http://localhost:8081/api/hello', {
method: 'GET',
credentials: 'include' // 携带Cookie,对应后端allowCredentials=true
})
.then(res => {
if (!res.ok) throw new Error('GET请求失败');
return res.text();
})
.then(data => {
const resultDiv = document.getElementById('getResult');
resultDiv.innerText = 'GET响应成功:' + data;
resultDiv.classList.remove('error');
})
.catch(err => {
const resultDiv = document.getElementById('getResult');
resultDiv.innerText = 'GET响应失败:' + err.message;
resultDiv.classList.add('error');
});
}
// 2. 发送跨域POST请求(JSON参数)
function sendPostRequest() {
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
if (!name || !email) {
alert('请填写用户名和邮箱!');
return;
}
fetch('http://localhost:8081/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉后端参数是JSON格式
},
body: JSON.stringify({ name, email }), // 把表单数据转成JSON字符串
credentials: 'include' // 携带Cookie
})
.then(res => {
if (!res.ok) throw new Error('POST请求失败');
return res.json(); // 后端返回JSON,用res.json()解析
})
.then(data => {
const resultDiv = document.getElementById('postResult');
resultDiv.innerText = `POST响应成功:\nID: ${data.id}\n用户名: ${data.name}\n邮箱: ${data.email}`;
resultDiv.classList.remove('error');
})
.catch(err => {
const resultDiv = document.getElementById('postResult');
resultDiv.innerText = 'POST响应失败:' + err.message;
resultDiv.classList.add('error');
});
}
</script>
</body>
</html>
3.4 运行与测试步骤
步骤 1:启动后端项目
-
运行
CorsDemoApplication.java; -
查看控制台,确认 “Tomcat started on port (s): 8081 (http)”,证明后端启动成功。
步骤 2:部署前端页面
前端页面需要运行在http://localhost:8080(符合后端allowedOrigins配置),推荐两种方式:
-
方式 1:用另一个 Spring Boot 项目(端口 8080),将
global-cors-test.html放在src/main/resources/templates目录,通过 Controller 跳转访问; -
方式 2:用 Nginx 部署,配置端口 8080,指向 HTML 文件所在目录。
这里以 “方式 1” 为例,新建前端 Spring Boot 项目(端口 8080):
-
引入 Thymeleaf 依赖(用于页面跳转):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> -
编写前端 Controller(跳转页面):
@Controller public class FrontController { @RequestMapping("/global-cors-test") public String toCorsTest() { return "global-cors-test"; // 对应templates/global-cors-test.html } } -
启动前端项目,访问
http://localhost:8080/global-cors-test。
步骤 3:测试跨域请求
-
测试 GET 请求:点击 “发送 GET 请求”,页面显示 “GET 响应成功:Hello from Global CORS! 这是跨域 GET 响应”;
-
测试 POST 请求:填写用户名(如 “张三”)和邮箱(如 “zhangsan@test.***”),点击 “发送 POST 请求”,页面显示
POST响应成功: ID: 1 用户名: 张三 邮箱: zhangsan@test.***
-
结论:全局 CORS 配置生效,GET 和 POST 跨域请求均成功。
3.5 全局配置的优势与适用场景
| 优势 | 适用场景 |
|---|---|
| 一次配置,所有接口生效 | 多 Controller、多接口需要跨域 |
| 统一管理跨域规则,维护成本低 | 生产环境,需要严格控制源、方法、头 |
| 支持复杂规则(如不同路径不同配置) | 部分接口需要特殊跨域规则(如/admin/**只允许特定源) |
四、跨域配置常见问题与解决方案(初学者避坑)
很多初学者配置了跨域但不生效,大多是因为忽略了这些细节:
4.1 问题 1:allowCredentials=true 但前端没加 credentials: 'include'
-
现象:后端配置
allowCredentials=true,但前端携带的 Cookie 没有传递到后端; -
原因:前端 fetch 请求必须加
credentials: 'include',否则浏览器不会携带 Cookie; -
解决方案:
fetch('http://localhost:8081/api/hello', {
method: 'GET',
credentials: 'include' // 必须加,对应后端allowCredentials=true
});
4.2 问题 2:origins 设为 "*" 但 allowCredentials=true
-
现象:后端配置
allowedOrigins("*")和allowCredentials(true),前端报错 “The value of the 'A***ess-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'”; -
原因:浏览器安全限制,当
allowCredentials=true时,allowedOrigins不能为 “*”(必须指定具体源); -
解决方案:
// 错误:origins="*" + allowCredentials=true // registry.addMapping("/**").allowedOrigins("*").allowCredentials(true); // 正确:指定具体源 registry.addMapping("/**").allowedOrigins("http://localhost:8080").allowCredentials(true);
4.3 问题 3:拦截器拦截了 OPTIONS 预检请求
-
现象:前端发送跨域请求,后端拦截器报 “未登录”,但实际是 OPTIONS 预检请求;
-
原因:跨域请求前,浏览器会先发 OPTIONS 请求询问后端是否允许跨域,拦截器误将 OPTIONS 请求当成普通请求拦截;
-
解决方案:在拦截器中放行 OPTIONS 请求:
@***ponent public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行OPTIONS预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } // 其他拦截逻辑(如登录验证)... } }
4.4 问题 4:前端用 file 协议打开 HTML(file:///C:/...)
-
现象:直接双击 HTML 文件(file 协议),发送跨域请求报错 “A***ess to fetch at 'http://localhost:8081' from origin 'null' has been blocked”;
-
原因:file 协议的 origin 是 “null”,后端
allowedOrigins没有配置 “null”,且浏览器对 file 协议的跨域限制更严格; -
解决方案:必须用 HTTP 协议访问前端页面(如
http://localhost:8080),不能用 file 协议。
五、生产环境跨域配置建议(安全第一)
在开发环境可以用localhost测试,但生产环境必须严格配置,避免安全风险:
-
禁止用 allowedOrigins ("*"):必须指定具体的前端域名(如
"https://www.zh.***"),避免恶意网站跨域请求; -
限制 allowedMethods:只允许业务需要的方法(如查询用 GET,提交用 POST,禁止 PUT/DELETE 暴露给前端);
-
allowedHeaders 按需配置:不建议用 “*”,只允许必要的头(如
"Content-Type", "Authorization"); -
maxAge 合理设置:建议设 3600 秒(1 小时),减少预检请求次数,提升性能;
-
配合 HTTPS:生产环境必须用 HTTPS,避免跨域请求中的数据被窃取;
-
对敏感接口额外验证:如
/admin/**接口,除了 CORS,还要加 Token 验证,双重保障。
六、总结
Spring Boot 跨域解决方案的核心是通过 CORS 机制,让后端告诉浏览器 “允许哪个源的请求”。我们需要根据项目场景选择合适的配置方式:
-
简单场景(单个接口 / Controller):用
@CrossOrigin注解,快速生效; -
复杂场景(多接口 / 生产环境):用全局 CORS 配置(
WebMv***onfigurer),统一管理,安全可控。
记住:跨域问题的本质是浏览器的安全限制,所有配置都围绕 “如何让浏览器认为这个跨域请求是安全的” 展开。掌握本文的实战代码和避坑指南,就能轻松应对前后端分离架构中的跨域问题。