banner
bladedragon

bladedragon

记录一次跨域问题

image

最近在做前后端交互的时候遇到了特殊的跨域问题。其实自己平时做项目的时候也多多少少处理过跨域问题,但是这次情况特殊。正好借此机会将跨域问题的解决方法做一下总结,以备不时之需。

情景#

自己前段时间在学习前端,所以想趁着这个机会自己搞个小工具玩玩。所以就有了如下这个极其简陋的弹幕网站。

image

为了实现弹幕的持久保存,自己尝试前后端交互将弹幕存储在数据库中,然后前端每间隔一段时间就从后台拉取一次数据。

实现其实比较简单,但是当我在本地开始测试向后台发送请求的时候,问题出现了:

image

很明显这是一个跨域问题,所以接下来我们就要动用我们所学的知识去解决它。

首先第一步,分析问题原因:什么是跨域?

什么是跨域?#

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源

跨域其实不是什么 bug,而是由于浏览器为了安全起见指定了一系列 “同源策略”,这些同源策略可以保证用户信息的安全,防止被恶意的网站窃取数据。

该政策限制网页的某些行为必须限制在与自己 “同源” 的网页中才能进行,如果不是同源,就会导致该行为无法生效,也就是跨域失败。

这里出现了两个概念,一个是同源,一个是跨域行为

同源的定义包含三个方面

  1. 协议相同
  2. 域名相同
  3. 端口相同

只有三个条件都满足,才能认定两个网页是” 同源 “

跨域行为(自己定义的名词,大概就是我们会被同源政策影响到的操作)随着互联网的发展,范围变得越来越宽泛,一般我们常见的跨域行为包括

  • 获取 Cookie、LocalStorage 和 IndexDB
  • 获得 DOM 和 JS 对象
  • AJAX 请求

我们可以看到在前后端交互中最常见的 AJAX 请求也赫然在列,在前后端交互中解决跨域问题的不可避免的。

那为什么要定义同源策略呢?没有跨域限制不是更好吗?

如果没有跨域限制,网页将很容易受到 XSS、CSRF 等攻击,因为没有限制,恶意网站同样可以自由地发起攻击,这将大大提高网站的维护成本。因此,同源策略其实是一把双刃剑,只是在保护网页的同时,偶尔总会误伤友军。

知道了问题的根源,我们就可以对症下药,寻找解决方案

解决方案#

我们可以看到,跨域问题的关键在于我们的 M请求处于限制范围内没有做到同源>,从而导致的。

重点已经标出来了,其实我们解决的方法也就是从这两个思路着手

  1. 采用不在同源策略的行为操作
  2. 想办法让行为处于同源状态

这里我们指针对 AJAX 请求,对于其他诸如 cookie、iframe 等的跨域方案,其实参考相关的博客相信一定能得到答案

1.JSONP 跨域#

我们可以通过在 AJAX 请求中定义 JSONP 类型实现跨域,虽说如此,但 JSONP 本质上采用的是和 AJAX 完全不同的请求方式。

传统的 AJAX 请求其实是xhr的异步请求,而 JSONP 本质上是去构建一个<script>标签,利用script标签中的src不受同源政策的限制,在src中填写后端URL并添加回调函数,获取到的数据就通过回调函数处理。

参考阮一峰的博客, 实现思想大致如此:

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('response data: ' + JSON.stringify(data));
};                      
    

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse的步骤。

如何在 AJAX 实现:

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",    // 自定义回调函数名
    data: {}
});

如何在 vue 上实现:

this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

从实现原理上可以看出 JSONP 还是存在弊端,那就是使用 JSONP 必须是 GET 请求,如果要 POST 请求实现跨域,还是需要使用其他方法

2.WebSocket#

websocket本身就是一种通信协议,通过websocket通信,实际上就可以跨过同源策略, 实现某种意义上的 "同源"。

下面是websocket请求的 HTTP 头信息,重点关注Origin字段,这是实现跨域的关键

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

Origin 字段表示该请求的请求源,只要 Origin 字段中的源域名和请求的目的域名是同一个,就可以通过同源策略中的域名一致,实现跨域。

如果允许通信,WebSocket 的响应头如下

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

3.CORS#

CORS 即跨域资源共享 "(Cross-origin resource sharing),它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

这是解决跨域问题的常用方法。

实现原理#

其实现原理如图

image

CORS 请求主要分成两类:简单请求和非简单请求。

满足以下条件的就是简单请求,否则就是非简单请求

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP 的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

简单 CORS 请求只是在请求的时候在 http 头中加入 Origin 字段

非简单 CORS 请求的话,浏览器会在正式通信后先发送预检请求,先询问服务器是否允许请求,得到响应,检查相关字段后就可以做出回应,发起正式请求

假设现在发起一段 js 脚本

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

其中预检请求的请求方法是 OPTIONS,具体请求头类似如下

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

主要关注三个字段

  • Origin

    表示请求来自哪个源

  • Access-Control-Request-Method

    该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是PUT

  • Access-Control-Request-Headers

    该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是X-Custom-Header

得到响应如下之后就能确认允许跨域请求

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

其中

//表示支持任意跨域请求
Access-Control-Allow-Origin: *
//表示支持跨域请求的方法
Access-Control-Allow-Methods: GET, POST, PUT
//当浏览器请求包含Access-Control-Request-Headers的时候必需,表示支持的头信息字段
Access-Control-Allow-Headers: X-Custom-Header
//允许发送cookie和认证信息
Access-Control-Allow-Credentials: true
//指定本次预检请求的有效期
Access-Control-Max-Age: 1728000

实现方式#

这里主要是后端的操作,这里用了java springboot的跨域方式做为样例

@Configuration
public class CORSConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedHeaders("*")
                        .allowCredentials(true)
                        .allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")
                        .maxAge(3600);
            }
        };
    }
}

其实就是在服务端响应的时候添加请求头,springboot还支持在不同controller上使用注解添加

4.nginx 代理跨域#

这个原理也简单,其实就是让前端和后端处于同源上,利用 nginx 的反向代理可以修改请求的域名、端口,也能添加 cookie 信息啥的实现跨域

#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

另外使用一些中间件的代理方式其原理都是这回事,这里就不加赘述

分析问题#

一通分析#

上面解决方案说了一大堆,但最终还是要回归我们的问题,这次,我们开始对症下药。首先再看一遍报错日志:

image

嗯?好像和想象中的不太一样,常见的跨域问题应该如同:

image

这种,看起来其中有诈?

果不其然,通过后台添加跨域设置,我们的报错信息依然没有变化。

这时我们就需要对报错信息好好分析(其实这应该是分析日志的第一步,为了强行引入跨域解决方案,因此特地将分析放在了后面)

这句话引起了我的注意

Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

通过查阅资料发现,原来这里我的前后端交互都是再本地实现,本地打开 html 使用的 file 协议,但是 file 协议的请求无法被浏览器认可,网上提供的方法如下

在谷歌浏览器下的快捷方式位置

image

在目标处添加:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -args --disable-web-security --user-data-dir --allow-file-access-from-files

大概就是这样,但是我还是不推荐使用这种方法,因为这样的方式并不是特别优雅的解决方法

另外的尝试#

另外的解决方法就是在本地部署 nginx,诸如上面提到过的解决方法,不通过 file 协议打开文件。

什么是 file 协议的打开方式?

大概就是这种

image

要换成用 http 形式打开的方式,诸如这种

image

还有如果不嫌麻烦直接把网页部署到服务器上也是一种解决方法

意外的结果#

但是!!最终问题还是没有得到解决!这可把我难到了。。

事必有因,经过一个多小时的不懈努力,我终于找到了问题的根源

————

image

ajax 的请求 URL 必须以 http 的格式。。。。

啊啊啊啊果然还是我太菜了。。

因为刚学 ajax,对其原理不熟,导致最后出现了这种问题。

参考链接#

阮一峰的博客

ajax 跨域,这应该是最全的解决方案了

前端常见跨域解决方案(全)

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.