什么是跨域访问问题,怎么解决?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
浏览器安全机制理解:面试官想了解你是否理解浏览器 "同源策略" 的设计初衷,以及为什么需要这个安全限制。
-
方案广度与深度:考察你是否掌握多种跨域解决方案,能否根据实际场景选择合适的方案,而不是只会 "配置 CORS" 这一种。
-
生产实践经验:是否了解跨域场景下的 Cookie 携带、预检请求优化、安全风险等实际问题。
核心答案
跨域 是指浏览器出于安全考虑(同源策略),阻止网页向不同源(协议 + 域名 + 端口)发起 AJAX 请求。
常见解决方案有 5 种:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| CORS | 服务端设置响应头 | 标准方案,功能完整 | 需要服务端配合 | 现代项目首选 |
| 代理服务器 | 同源代理转发 | 无需修改后端 | 多一层转发 | 开发环境 |
| JSONP | 利用 <script> 标签 | 兼容性好 | 只支持 GET,不安全 | 老项目兼容 |
| postMessage | 跨窗口通信 | 安全可控 | 仅限窗口间 | iframe 嵌套 |
| WebSocket | 不受同源限制 | 全双工通信 | 需要建立长连接 | 实时通信场景 |
一句话总结:生产环境优先使用 CORS,开发环境可用 代理服务器,老旧系统兼容可用 JSONP。
深度解析
一、什么是跨域?为什么会有跨域问题?
1. 同源策略
同源策略的核心要点:
-
什么是同源:URL 的协议(Protocol)、域名(Domain)、端口(Port)三者完全相同才算同源。
-
限制范围:同源策略主要限制以下操作:
- AJAX 请求无法发送(主要限制)
- Cookie、LocalStorage、IndexedDB 无法读取
- DOM 无法操作(iframe 跨域)
-
设计初衷:防止恶意网站读取其他网站的数据,保护用户隐私和安全。比如防止恶意网站通过 AJAX 读取你的银行账户信息。
2. 跨域错误示例
跨域错误的几个关键点:
-
请求是发出去了的:跨域并不是请求发不出去,而是浏览器拦截了响应。
-
后端不知道跨域:后端正常处理请求并返回数据,根本不知道发生了跨域。
-
浏览器报错:浏览器检查响应头,如果没有正确的 CORS 头,就拦截响应并报错。
二、方案一:CORS(跨域资源共享)
CORS(Cross-Origin Resource Sharing)是 W3C 标准,也是目前最主流的跨域解决方案。
1. 简单请求 vs 预检请求
CORS 请求分为两种类型:
-
简单请求:满足特定条件的请求,浏览器直接发送,在响应头中检查 CORS 头即可。
-
预检请求(Preflight):不满足简单请求条件的请求,浏览器会先发送一个
OPTIONS请求询问服务器是否允许,然后再发送实际请求。
2. 简单请求流程
简单请求的处理流程:
-
请求阶段:浏览器在请求头中添加
Origin字段,表示请求来源。 -
响应阶段:服务器在响应头中添加
Access-Control-Allow-Origin,表示允许哪些源访问。 -
验证阶段:浏览器检查响应头,如果
Origin在允许列表中,则允许读取响应。
3. 预检请求流程
预检请求的处理流程:
-
第一步(预检):浏览器先发送
OPTIONS请求,询问服务器是否允许该跨域请求(包括方法、头部等)。 -
第二步(实际请求):如果预检通过,浏览器才发送实际的请求。
-
缓存优化:
Access-Control-Max-Age可以设置预检结果的缓存时间,避免每次都发送预检请求。
4. 后端配置示例
Spring Boot 配置 CORS:
// 方式一:全局配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 允许跨域的路径
.allowedOriginPatterns("*") // 允许的源(SpringBoot 2.4+ 用 patterns)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") // 允许的请求头
.allowCredentials(true) // 允许携带 Cookie
.maxAge(3600); // 预检请求缓存时间(秒)
}
}
// 方式二:过滤器方式(更灵活)
@Component
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;
// 设置允许的源(生产环境应配置具体域名)
String origin = request.getHeader("Origin");
if (isAllowedOrigin(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
// 预检请求直接返回
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}
5. CORS 常见响应头
| 响应头 | 说明 | 示例 |
|---|---|---|
Access-Control-Allow-Origin | 允许的源 | * 或 http://localhost:3000 |
Access-Control-Allow-Methods | 允许的方法 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 允许的请求头 | Content-Type, X-Token |
Access-Control-Allow-Credentials | 是否允许 Cookie | true |
Access-Control-Max-Age | 预检缓存时间 | 3600(秒) |
Access-Control-Expose-Headers | 暴露给前端的响应头 | X-Total-Count |
三、方案二:代理服务器
代理服务器的原理是:同源策略只限制浏览器,不限制服务器。通过同源的服务器代理转发请求,绕过浏览器的跨域限制。
代理服务器的工作原理:
-
浏览器视角:请求发送到
localhost:3000,同源,无跨域问题。 -
代理服务器:前端开发服务器(如 Vite、Webpack)将
/api/*的请求代理到后端服务器。 -
服务器间通信:服务器之间通信不受同源策略限制。
Vite 配置代理示例:
// vite.config.js
export default {
server: {
proxy: {
// 将 /api 请求代理到后端
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true, // 修改 Origin 头
rewrite: (path) => path.replace(/^\/api/, '') // 路径重写
}
}
}
}
Nginx 反向代理配置:
# 生产环境可用 Nginx 做反向代理
server {
listen 80;
server_name example.com;
# 前端静态资源
location / {
root /var/www/html;
index index.html;
}
# API 代理
location /api/ {
proxy_pass http://backend-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
四、方案三:JSONP
JSONP(JSON with Padding)是早期常用的跨域方案,利用 <script> 标签不受同源限制的特性。
JSONP 的工作原理:
-
利用
<script>标签:<script>标签的src属性不受同源策略限制。 -
回调函数机制:前端通过 URL 参数传递回调函数名,后端返回调用该函数的 JS 代码。
-
自动执行:浏览器加载并执行返回的 JS 代码,触发回调函数。
JSONP 实现代码:
// 前端实现
function jsonp(url, callback) {
const callbackName = 'jsonp_' + Date.now();
// 创建全局回调函数
window[callbackName] = function(data) {
callback(data);
delete window[callbackName]; // 清理全局函数
document.body.removeChild(script); // 移除 script 标签
};
// 创建 script 标签
const script = document.createElement('script');
script.src = `${url}?callback=${callbackName}`;
document.body.appendChild(script);
}
// 使用
jsonp('http://api.example.com/user', function(data) {
console.log(data);
});
JSONP 的局限性:
-
只支持 GET 请求:因为是通过 URL 传参,无法发送 POST 等请求。
-
存在安全风险:如果后端被劫持,可以返回恶意代码执行。
-
错误处理困难:无法通过 HTTP 状态码判断请求是否成功。
五、方案对比与选择
六、携带 Cookie 的跨域问题
当需要跨域携带 Cookie 时,需要额外配置:
// 前端:fetch 需要设置 credentials
fetch('http://api.example.com/user', {
credentials: 'include' // 携带 Cookie
});
// 前端:axios 需要设置 withCredentials
axios.get('http://api.example.com/user', {
withCredentials: true
});
// 后端:CORS 配置
// 注意:allowCredentials(true) 时,allowOrigin 不能为 "*"
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); // 具体域名
response.setHeader("Access-Control-Allow-Credentials", "true");
重要注意事项:
-
当
Access-Control-Allow-Credentials: true时,Access-Control-Allow-Origin不能 设置为*,必须指定具体域名。 -
Cookie 也受同源策略限制,跨域 Cookie 需要设置
SameSite=None; Secure(仅 HTTPS)。
面试高频追问
-
追问一:为什么表单提交、图片加载不受跨域限制,但 AJAX 会受限制?
回答思路:跨域限制的目的是保护用户数据安全。表单提交会跳转页面,不会泄露数据给原页面;图片加载只能显示,无法读取内容。而 AJAX 可以读取响应内容,如果恶意网站能通过 AJAX 访问你的银行账户,就能窃取数据,所以需要限制。
-
追问二:CORS 的预检请求会对性能有影响吗?如何优化?
回答思路:预检请求会增加一次网络往返,有性能开销。优化方式:一是设置
Access-Control-Max-Age缓存预检结果;二是尽量避免使用触发预检的请求方式(如Content-Type: application/json改为text/plain,但不太推荐);三是服务端对 OPTIONS 请求快速响应,不进行业务处理。 -
追问三:WebSocket 为什么不受跨域限制?
回答思路:WebSocket 使用
ws://或wss://协议,建立连接时虽然也发送 Origin 头,但它的安全模型与 AJAX 不同。WebSocket 连接需要服务端显式验证 Origin,而不是浏览器拦截。实际上服务端应该校验 Origin,避免恶意网站建立 WebSocket 连接。
常见面试变体
- 变体一:介绍一下 CORS 的工作原理?
- 变体二:简单请求和预检请求有什么区别?
- 变体三:开发环境和生产环境分别如何解决跨域问题?
- 变体四:跨域请求能携带 Cookie 吗?需要注意什么?
记忆口诀
同源策略三要素:协议域名端口要一致。
解决方案记四招:CORS 标准、代理转发、JSONP 兼容、postMessage 通信。
CORS 流程:简单请求直接发,预检先问 OPTIONS,响应头部要配置全。
总结
跨域是浏览器 同源策略(协议 + 域名 + 端口)导致的安全限制。主流解决方案是 CORS(后端设置 Access-Control-* 响应头),开发环境可用 代理服务器 绕过,老旧系统可用 JSONP(仅 GET)。生产环境推荐 Nginx 反向代理 + CORS 组合方案,注意携带 Cookie 时 Allow-Origin 不能为 *。