跨域问题解决及实践

2,001 阅读7分钟

跨域问题及解决方案

一、跨域的定义及解决方案

什么是跨域

简单来说,跨域(Cross-Origin)是浏览器从一个域名(源)向另一个域名(源)发起HTTP请求时,受到 同源策略(Same-Origin Policy)  限制的一种行为。

同源策略是浏览器的一项安全机制,用于限制从一个源加载的资源如何与另一个源的资源进行交互。源(Origin)  是由以下三部分组成的:

  • 协议(如httphttps
  • 域名(如example.com
  • 端口号(如80443

如果两个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请求:

    • 请求方法为GETPOSTHEAD
    • 请求头仅包含以下字段:AcceptAccept-LanguageContent-LanguageContent-Type(仅限于application/x-www-form-urlencodedmultipart/form-datatext/plain)。
    • 没有使用XMLHttpRequest.upload事件。
    • 请求中没有包含自定义头信息。
  • 复杂请求: 不满足简单请求的请求既是复杂请求。复杂请求通常需要浏览器在发送实际请求之前,先发送一个预检请求(Preflight Request),以确认服务器是否允许该请求。预检请求使用OPTIONS方法。

CORS的工作流程

对于简单请求而言,工作流程如下:

  1. 浏览器发送跨域请求,会自动在请求头添加Origin字段,表明请求来源:
GET /api/data HTTP/1.1
Origin: https://frontend.com
Host: https://backend.com

  1. 服务器接收到请求后,需要检查下Origin字段,决定是否允许跨域,如果允许,会在响应头添加Access-Control-Allow-Origin等字段,表示允许的源:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Content-Type: application/json

{"message": "Hello, CORS!"}

  1. 浏览器进行验证:浏览器检查响应头中的 Access-Control-Allow-Origin 字段。如果与请求的 Origin 匹配,则允许访问响应数据,否则拦截响应并报错。

对于复杂请求,还需要在请求前发送一个预检请求OPTIONS,流程如下:

  1. 发送预检请求,会在请求头添加如下字段:
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

  1. 如果预检请求通过,浏览器再发送实际的复杂请求
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方法(如 GETPOST)。
  • Access-Control-Allow-Headers:允许的请求头(如 Content-TypeAuthorization)。
  • 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请求服务端可以成功返回,但会被浏览器拦截

截屏2025-03-02 23.10.21.png

截屏2025-03-02 23.12.10.png

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>");
}

再次请求发现跨域问题解决了

截屏2025-03-02 23.19.39.png

复杂请求的处理

对于复杂请求,需要处理预检请求(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通过DispatcherServletHandlerMapping自动拦截并处理这些请求,生成一个符合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-MethodsAccess-Control-Allow-Headers等响应头告知浏览器是否支持。

特别说明的是,跨域问题是浏览器的限制,即使服务方返回了正确的响应。浏览器也会因未返回对应header拦截的。