跨域问题及解决方案
一、跨域的定义及解决方案
什么是跨域
简单来说,跨域(Cross-Origin)是浏览器从一个域名(源)向另一个域名(源)发起HTTP请求时,受到 同源策略(Same-Origin Policy) 限制的一种行为。
同源策略是浏览器的一项安全机制,用于限制从一个源加载的资源如何与另一个源的资源进行交互。源(Origin) 是由以下三部分组成的:
- 协议(如
http或https) - 域名(如
example.com) - 端口号(如
80或443)
如果两个URL的协议、域名和端口号完全相同,则它们属于 同源;否则,它们属于 跨域。
例如:
http://example.com/page1和http://example.com/page2—— 同源http://example.com和https://example.com—— 跨域(协议不同)http://example.com和http://sub.example.com—— 跨域(域名不同)http://example.com:80和http://example.com:8080—— 跨域(端口不同)
跨域问题一般有如下解决方案:
- CORS(跨域资源共享):是跨域问题的标准解决方案,通过在服务器设置HTTP响应头允许或拒绝跨域请求。
- JSONP(JSON with Padding):早期的跨域解决方法,只支持GET请求。通过
<script>标签不受同源策略的限制突破跨域问题,现已不推荐。 - 反向代理,如nginx等。通过代理服务器将跨域请求转为同源请求解决。
- WebSocket:不受同源策略限制
以下篇章主要介绍CORS的方案。
二、 跨域资源共享(CORS)
在了解CORS(跨域资源共享)如何解决跨域问题时,需要先了解下简单请求和复杂请求这两个概念,它们是CORS中的两个重要概念,决定了浏览器如何处理跨域请求,以及是否需要发送 预检请求(Preflight Request) 。
-
简单请求: 简单请求是指满足以下所有条件的HTTP请求:
- 请求方法为
GET、POST或HEAD。 - 请求头仅包含以下字段:
Accept、Accept-Language、Content-Language、Content-Type(仅限于application/x-www-form-urlencoded、multipart/form-data或text/plain)。 - 没有使用
XMLHttpRequest.upload事件。 - 请求中没有包含自定义头信息。
- 请求方法为
-
复杂请求: 不满足简单请求的请求既是复杂请求。复杂请求通常需要浏览器在发送实际请求之前,先发送一个预检请求(Preflight Request),以确认服务器是否允许该请求。预检请求使用
OPTIONS方法。
CORS的工作流程
对于简单请求而言,工作流程如下:
- 浏览器发送跨域请求,会自动在请求头添加
Origin字段,表明请求来源:
GET /api/data HTTP/1.1
Origin: https://frontend.com
Host: https://backend.com
- 服务器接收到请求后,需要检查下
Origin字段,决定是否允许跨域,如果允许,会在响应头添加Access-Control-Allow-Origin等字段,表示允许的源:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Content-Type: application/json
{"message": "Hello, CORS!"}
- 浏览器进行验证:浏览器检查响应头中的
Access-Control-Allow-Origin字段。如果与请求的Origin匹配,则允许访问响应数据,否则拦截响应并报错。
对于复杂请求,还需要在请求前发送一个预检请求OPTIONS,流程如下:
- 发送预检请求,会在请求头添加如下字段:
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Host: https://backend.com
2. 服务器响应预检请求
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: PUT, POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600
- 如果预检请求通过,浏览器再发送实际的复杂请求
PUT /api/data HTTP/1.1
Origin: https://frontend.com
Content-Type: application/json
Authorization: Bearer token123
Host: https://backend.com
{"message": "Hello, CORS!"}
CORS请求头与响应头
说明下上述提及的请求及响应头含义
请求头
Origin:标明请求的来源(域名、协议、端口)。Access-Control-Request-Method:标明实际请求的方法(仅用于预检请求)。Access-Control-Request-Headers:标明实际请求的头信息(仅用于预检请求)。
响应头
Access-Control-Allow-Origin:允许的源(如*表示所有来源)。Access-Control-Allow-Methods:允许的HTTP方法(如GET、POST)。Access-Control-Allow-Headers:允许的请求头(如Content-Type、Authorization)。Access-Control-Max-Age:预检请求的有效期(秒)。Access-Control-Allow-Credentials:是否允许携带凭证(如Cookies)。
三、原生Servlet处理跨域实践
理解了上述CORS解决方法,下面我们以原生serlvet为例实践下。 原生servlet更能去掉框架的影响,可以从原理上更深入理解。
服务准备
搭建两个端口不同java web应用服务,用tomcat等常见服务器就行。
服务A(调用方),在本地启动,设置端口8082,配置如下:
pom.xml添加serlvet依赖即可
<!-- Servlet API, 用于tomcat10及以上版本,tomcat10以下的用javax.servlet版本即可 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
servlet配置如下:
@WebServlet("/user")
public class UserServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 转发到HTML
RequestDispatcher dispatcher = req.getRequestDispatcher("index.html");
dispatcher.forward(req, resp);
}
}
index.html页面如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document</title>
</head>
<body>
<script>
// 发送跨域请求,访问B服务
fetch('http://localhost:8083/serverB/test', {
method: 'GET',
}).then(data => console.log('成功:', data))
.catch(err => console.error('失败:', err));
</script>
<h1> Hello World </h1>
</body>
</html>
服务B,本地启动,设置端口为8083,配置如下:
pom.xml添加serlvet依赖即可
<!-- Servlet API, 用于tomcat10及以上版本,tomcat10以下的用javax.servlet版本即可 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
servlet配置如下:
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=UTF-8");
PrintWriter out = resp.getWriter();
String name = req.getParameter("name");
out.println("<h1>你好," + (name != null ? name : "访客") + "!</h1>");
out.println("<p>当前时间:" + new java.util.Date() + "</p>");
}
}
简单请求的处理
测试简单请求,按上述配置启动服务A和服务B, 查看服务A的控制台,可以看到test的GET请求报错CORS错误。点详情查看http请求是成功的,说明跨域的GET请求服务端可以成功返回,但会被浏览器拦截。
在doGet方法设置响应头Access-Control-Allow-Origin,如下所示:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=UTF-8");
PrintWriter out = resp.getWriter();
// 设置CORS响应头
resp.setHeader("Access-Control-Allow-Origin", "*");
out.println("<p>当前时间:" + new java.util.Date() + "</p>");
}
再次请求发现跨域问题解决了
复杂请求的处理
对于复杂请求,需要处理预检请求(OPTIONS方法),并手动设置Access-Control-Allow-*头信息:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json; charset=UTF-8");
PrintWriter out = resp.getWriter();
// 读取JSON请求体
StringBuilder jsonBody = new StringBuilder();
String line;
try (BufferedReader reader = req.getReader()) {
while ((line = reader.readLine()) != null) {
jsonBody.append(line);
}
}
resp.setHeader("Access-Control-Allow-Origin", "*");
//设置响应
out.println(jsonBody);
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
可以将服务A请求服务B的test方法设置为post,如下所示:
<script>
// 发送POST跨域请求
fetch('http://localhost:8083/servletdemo_war_exploded/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'user123',
email: 'user@example.com'
})
}).then(response => response.json())
.then(data => console.log('成功:', data))
.catch(err => console.error('失败:', err));
</script>
四、框架解决跨域问题
一般的web框架都会内置对跨域的支持,通过简单配置即可解决。如下面的spring mvc
Spring MVC中的CORS支持
Spring MVC 提供了对CORS的全面支持,通过注解或全局配置可以轻松处理跨域请求,无需手动处理预检请求。
自动拦截OPTIONS请求
如上所说,当浏览器发送一个复杂请求时,会先发送一个OPTIONS方法的预检请求。Spring MVC通过DispatcherServlet和HandlerMapping自动拦截并处理这些请求,生成一个符合CORS规范的响应。
使用注解配置
在Spring MVC中,可以使用@CrossOrigin注解来为特定方法或控制器启用CORS:
@RestController
public class MyController {
@CrossOrigin(origins = "*") // 允许所有来源跨域
@GetMapping("/simple")
public String simpleRequest() {
return "Hello, this is a simple GET request!";
}
}
全局CORS配置
如果需要为整个应用程序配置CORS,可以在Spring Boot中使用WebMvcConfigurer:
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("*") // 允许所有来源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
.allowedHeaders("*") // 允许所有头信息
.allowCredentials(true) // 允许携带凭证(如Cookies)
.maxAge(3600); // 预检请求缓存时间
}
}
五、总结
CORS 是现代 Web 开发中解决跨域问题的标准方案。它通过 HTTP 头信息实现跨域访问控制,支持简单请求和复杂请求。
-
对于简单请求,只需在请求方法中设置
Access-Control-Allow-Origin即可。 -
对于复杂请求,除在请求方法设置
Access-Control-Allow-Origin以外,还需处理预检请求,请求方法为OPTIONS,预检方法目的为快速判断是否支持复杂请求。需设置Access-Control-Allow-Methods、Access-Control-Allow-Headers等响应头告知浏览器是否支持。
特别说明的是,跨域问题是浏览器的限制,即使服务方返回了正确的响应。浏览器也会因未返回对应header拦截的。