前端跨域问题

前言

说实在的,跨域在我做 Android 的时候压根没碰过…也没想过前端还这样,但是这个跨域问题是前端无法忽视的一个地方,一般当然职务作品会有后端配合,但是自己如果用第三方 API 做点什么了都解决不了跨域就不太好了。

什么是跨域

跨域的诞生是为了避免同源策略,在浏览器最基本的安全问题就是同源策略。

什么是同源

同源简单的来讲就是除了统一域名下之外,都是跨域,举个 🌰,http://www.foo.com/ 这个网址所能请求的,只有这个域名下的地址,其他无论不同端口,协议(包括 HTTP 和 HTTPS 差异),域名还是对应 IP,还是说没有 http://foo.com/ 这种二级域名不一致,都不能算同源。

例外

imglinkscript是允许跨域加载资源的。

触发跨域会怎样

其实在服务端看来,即便触发了跨域,在他看来也是正常的一个请求,只是,浏览器拦截了,浏览器认为这是不安全的,所以给前端报错。

解决方案

JSONP

利用script不受同源策略影响所诞生的做法,用这种方式去加载一段执行代码,从而获取数据。

特性

  • 兼容性好
  • 只支持 GET 方法
  • 有可能遭受 XSS 攻击
  • 需要服务端配合!

实现

既然是利用script标签,本来地址如何,改成放到script就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// http://www.bar.com/jsonp
// 假设这个网址本身返回的就是 'bar'
// 如果我们用 JSONP 的方式就是

const jsonp = function({ url, params, callback }) {
return new Promise((resolve, rejct) => {
let loaded = false;
const script = document.createElement("script");
window[callback] = function(data) {
resolve(data);

loaded = true;

document.body.removeChild(script);
window[callback] = null;
};

const p = { ...params, callback };
const array = [];
for (let key in p) {
array.push(`${key}=${p[key]}`);
}
script.src = `${url}?${array.join("&")}`;
document.body.appendChild(script);

setTimeout(() => {
loaded || rejct("timeout");
});
});
};
jsonp({
url: "http://www.bar.com/jsonp",
callback: "jsonpCallback"
});

上述代码之后会返回什么呢?其实本质上服务端返回会从‘bar’jsonpCallback('bar')的形式,实际上就是加载并调用了jsonpCallback

jQuery

1
2
3
4
5
6
7
8
9
10
$.ajax({
url: "http://www.bar.com/jsonp",
dataType: "jsonp",
type: "get", // 可选
jsonpCallback: "jsonpCallback", // 可选
jsonp: "callback", // 指定 query string 的 callback key
success: function(data) {
console.log(data);
}
});

jQuery 也自带 JSONP 请求方法

CORS

这种方式也是需要后端支持,其实解决跨域基本上都需要后端支撑,不然就太危险了不是么?

这种解决方案主要是设置后端的Access-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Allow-CredentialsAccess-Control-Max-AgeAccess-Control-Expose-Headers等响应头,来和浏览器交互,告诉浏览器不需要拦截的条件。

访问场景

CORS 请求有点不一样,它分为两种,一种是简单请求,一种是复杂请求,如果是复杂请求,则会发送两次请求,第一次会先用options 方法发起一个预检的请求到服务器,来获取是否能实际请求,避免出现服务器已经执行但是浏览器拦截的 CSRF 攻击。

简单请求

如果满足下述条件,则可以视为简单请求,会直接进行请求

  • 使用下列方法之一:
    • GET
    • HEAD
    • POST
  • Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意额外的限制)
  • Content-Type 的值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencodeds
  • 请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器;XMLHttpRequestUpload对象可以使用XMLHttpRequest.upload属性访问。
  • 请求中没有使用ReadableStream对象。
复杂请求

如果超出了简单请求的范围,就是复杂请求了,首先会进行一次options方法检查,这个需要后端的同学明白。

代理

这种方式非常常用,开发的过程中用中间人代理,而且都是不用入侵代码的做法,十分方便。而且现在有一种做法就是前端写 node.js 和服务端渲染,所有请求交给 node.js 一层,改善 SEO 问题和速度体验。

websocket

websocket 长连接方式不受跨域影响,建立连接之后客户端和服务端都可以主动发送数据,进行交互,在一些业务上还是有需要用到的。
由于 API 不友好,一般使用Socket.io来进行开发。

总结

  • CORS 算是最正统的做法,也是根本解决方案,生产环境最常用
  • JSONP 兼容性比较好,但是现在可能比较难用得上
  • websocket 某些业务用得上,开发复杂度提升

前端很多问题都有非常多解决方案,但是我总感觉其他方案太黑魔法了,甚至有点厌恶,比如iframe都有几种办法,canvas也有,但是不太常用,最常用还是上面几种。