跨域及其处理

knoci 发布于 2025-02-07 94 次阅读


什么是跨域

​ 当a.knoci.cn名下的页面或脚本试图去请求b.knoci.cn域名下的资源时,就是典型的跨域行为。跨域的定义从受限范围可以分为两种,广义跨域和狭义跨域。

广义跨域

​ 广义跨域通常包含以下三种行为:

  • 资源跳转:a链接、重定向。

  • 资源嵌入:<link>、<script>、<img>、<frame>dom标签,还有样式中background:url()、@font-face()等文件外链。

  • 脚本请求:浏览器存储数据读取、domjs对象的跨域操作、js发起的ajax请求等。

​ 其中,资源跳转和资源嵌入行为可以正常请求到跨域资源,脚本请求在未经任何处理的情况下,通常会有跨域问题。

狭义跨域

​ 狭义跨域正是浏览器同源策略限制的一类请求场景,即我们通常说的跨域行为,通常包含以下三种行为:

  • cookie、localStorage和indexDB无法读取。

  • dom和js对象无法获取和操作。

  • ajax请求无法发送。


为什么会跨域

​ 说到跨域不得不谈的就是浏览器的同源策略,跨域也是因为浏览器这个机制引起的,这个机制的存在还是在于安全。

什么是源

Web内容的源由用于访问它的URL 的方案(协议),主机(域名)和端口定义。只有当方案,主机和端口都匹配时,两个对象具有相同的起源。

​ 同源不同源一句话就可以判断:就是url中 scheme host port 都相同即为同源

​ 下面认识下url 结构中的这三个部分。

URL结构

URL 代表着是统一资源定位符(Uniform Resource Locator)。URL 无非就是一个给定的独特资源在 Web 上的地址。

URL有如下结构组成:

  • Schme 或者 Protocol
img

  • Domain Name 也叫做host域名
img

  • port 端口号
img

  • Parameters参数
img

  • Anchor 锚点,一般用于定位位置
img

同源不同源举例

举一下同源不同源的例子,便于理解:

同源例子
例子原因
http://example.com/app1/index.html http://example.com/app2/index.html相同的 scheme http 和host
http://Example.com:80 http://example.comhttp 默认80端口所以同源
不同源例子
例子原因
http://example.com/app1 https://example.com/app2不同的协议
http://example.com http://myapp.example.com不同的host
http://example.com http://example.com:8080不同的端口

浏览器为什么需要同源策略

​ 同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。一个页面:

  • 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
  • 无法接触非同源网页的 DOM
  • 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)

常规前端请求跨域

​ 在没有前后端分离的时候,跨域问题往往是很少的。因为前后端都部署到一起。现在前后端分离不管vue /react 面临跨域请求的问题。

​ 下面是引用官网描述的一张图来解释跨域:

img

跨源域资源共享(CORS)机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

前端处理跨域

jsonp跨域

​ jsonp (JSON with Padding)的原理非常简单,就是HTML标签中,很多带src属性的标签都可以跨域请求内容,比如我们熟悉的img图片标签。同理,script标签也可以,可以利用script标签来执行跨域的javascript代码。通过这些代码,我们就能实现前端跨域请求数据。

​ jsonp 可以在前端解决跨域问题,但是只是针对于get请求。实现方式可以引用一些npm 第三方库实现,jquery 也是带的。

​ jsonp跨解实现流程:

图片

​ jsonp跨域代码示例:

var script = document.createElement('script');
script.type = 'text/javascript';
// 传参callback给后端,后端返回时执行这个在前端定义的回调函数
script.src = 'http://a.qq.com/index.php?callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
    alert(JSON.stringify(res));
}

jsonp跨域优点:

  • jsonp兼容性强,适用于所有浏览器,尤其是IE10及以下浏览器。

jsonp跨域缺点:

  • 没有关于调用错误的处理。

  • 只支持GET请求,不支持POST请求以及大数据量的请求,而且也无法拿到相关的返回头,状态码等数据。

  • callback参数恶意注入,可能会造成xss漏洞。

  • 无法设置资源访问授权。

webpack-dev-server

​ 前端无论是vue项目还是react 项目大多数都会以webpack-dev-server 来运行,webpack-dev-server 可以设置代理,前端可以在开发环境设置代理解决跨域问题。

 proxy: {
      '/api': {
        target: 'http://localhost:3000',
        pathRewrite: { '^/api': '' },
        changeOrigin: true,
      },
   }

​ vue-cli、create-react-app、umi 等脚手架找到webpack devserver配置位置配上即可。

注意: 只限在开发环境,生产环境需要web 服务器同样原理代即可。下面会说明怎么用。

document.domain

​ 利用document.domain 可以修改 访问页面的域,多用在父子关系的域,子域可以设置document.domain等于父域,从而解决同父域跨域问题。

图片

举例子:

​ www.a.knoci.cn访问www.b.knoci.cn跨域,可以利用 document.domain 设置成 www.knoci.cn

代码示例:

<!-- A页面 http://a.qq.com/a.html -->
<iframe id="iframe" src="http://b.qq.com/b.html"></iframe>
<script>
    document.domain = "qq.com";
    var windowB = document.getElementById("iframe").contentWindow;
    alert("B页面的user变量:" + windowB.user);
</script>
<!-- B页面 http://b.qq.com/b.html -->
<script>
    document.domain = "qq.com";
    var user = "saramliu";
</script>

document.domain+iframe方案优点

  • 实现逻辑简单,无需额外中转页面

document.domain+iframe方案缺点

  • 仅适用于主域相同,子域不同的前端通信跨域场景

后端处理跨域

Http配置CORS

​ 跨域其实也是http层面上可以解决的问题,后端解决也是比较简单的,也是项目常见的解决手法。

​ CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

​ 同源安全策略 默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。

  • Access-Control-Allow-Origin 指示请求的资源能共享给哪些域。
  • Access-Control-Allow-Credentials 指示当请求的凭证标记为 true 时,是否响应该请求。
  • Access-Control-Allow-Headers 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
  • Access-Control-Allow-Methods 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。
  • Access-Control-Expose-Headers 指示哪些 HTTP 头的名称能在响应中列出。
  • Access-Control-Max-Age 指示预请求的结果能被缓存多久。
  • Access-Control-Request-Headers 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。
  • Access-Control-Request-Method 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。
  • Origin 指示获取资源的请求是从什么域发起的。

​ gin框架设置跨域:

func Cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        method := c.Request.Method

        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")
        c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
        c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
        c.Header("Access-Control-Allow-Credentials", "true")

        if method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
        }
    }
}

Nginx

添加响应头(CORS)

​ 最常见的方式是在 Nginx 配置中添加Access - Control - Allow - Origin等相关响应头来允许跨域访问。

server {
    listen       80;
    server_name  your_domain.com;

    location / {
        add_header 'Access-Control-Allow-Origin' '*'; 
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User - Agent,X - Requested - With,If - Modified - Since,Cache - Control,Content - Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content - Length,Content - Range';
        # 以下是正常的代理转发等配置,根据实际情况而定
        proxy_pass http://backend_server;
    }
}
  • add_header 'Access - Control - Allow - Origin' '*';:这一行允许来自任何域的跨域请求。如果只想允许特定的域,可以将*替换为具体的域名,如http://example.com
  • add_header 'Access - Control -Allow - Methods' 'GET, POST, OPTIONS';:指定允许的 HTTP 请求方法。除了常见的GETPOST,还添加了OPTIONS方法。OPTIONS方法通常用于浏览器在实际请求之前发送的预检请求,用于检查服务器是否允许跨域请求。
  • add_header 'Access - Control -Allow - Headers' 'DNT,User - Agent,X - Requested - With,If - Modified - Since,Cache - Control,Content - Type,Range';:定义允许的请求头。当客户端发送带有自定义请求头的跨域请求时,需要在这里明确允许这些请求头,否则请求可能会被拒绝。
  • add_header 'Access - Control -Expose - Headers' 'Content - Length,Content - Range';:指定哪些响应头可以暴露给客户端。有些响应头默认是不暴露的,通过这个配置可以让客户端访问这些响应头。

处理预检请求(OPTIONS)

​ 当浏览器发送跨域请求时,对于非简单请求(例如包含自定义请求头、非GET/POST/HEAD方法等),会先发送一个OPTIONS预检请求。Nginx 需要正确处理这个预检请求。

server {
    listen       80;
    server_name  your_domain.com;

    location / {
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User - Agent,X - Requested - With,If - Modified - Since,Cache - Control,Content - Type,Range';
            add_header 'Access-Control-Expose-Headers' 'Content - Length,Content - Range';
            return 204;
        }
        # 正常的代理转发等配置
        proxy_pass http://backend_server;
    }
}
  • if ($request_method = 'OPTIONS'):这个条件判断请求方法是否为OPTIONS。如果是,就执行下面的配置。
  • 内部的add_header指令和前面类似,用于添加允许跨域的响应头。return 204;表示返回一个空的响应体,状态码为204(无内容)。这是处理OPTIONS预检请求的常见方式,告诉浏览器服务器允许跨域请求,并且可以继续发送实际的请求。

总结

​ 跨域限制像道墙,同源策略紧守望。

​ 前端后端不同网,请求数据遇阻挡。

​ 允许源址要得当,方法头域细思量。

​ 预检 OPTIONS 别忘,CORS 头是好良方。

​ 安全合规不能放,合理配置跨域爽。