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 跨域,这应该是最全的解决方案了

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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。