跨域问题详解
CouriourC Lv4

什么是跨域?

逐词拆解,先看动词是指从一个地方访问另外一个地方,是指域名。域名包含了指定的端口,和地址,在DNS服务中是掩盖了端口号为协议,比如 httphttpsws。这些实际上对应了不同的端口,一个完整的地址可以看作是 protocol://*.domain:port 构成。只要其中的几个关键点不对,那就说明你正在做跨域的事儿。实际上,在作为桌面程序或者脚手架开发的时候,是不存在跨域这个说法的,因为底层都是所谓的套接字协议,这个名词因为浏览器中的同源策略而出现的。具体可以看 MDN 所述的 浏览器的同源策略同源策略是一个重要的安全策略,它用于限制一个的文档或者它加载的脚本如何能与另一个源的资源进行交互。

为什么有跨域?

请记住前端干的活很坦率,做完了的东西全部都给了你,记住是它的一切,你所见到的那一切。

这当然是为了安全,否则访问个网页,就发现这个页面在后台四处请求,偷偷摸摸的做一些坏事,比如,你原本正在看小视频,结果网页后台发起一个支付请求,悄无声息给你付了钱给了其他的平台,你该找谁?那么羊毛出在羊身上,那你去找浏览器厂商吧!因此,浏览器为了你的安全做出了一定的限制,让出了事能找到人。

问题出在哪儿?

前面已经说了,这是浏览器的事儿,当你发起一个请求的时候,只要不和规矩(也就是同源策略),那么浏览器在发起请求的时候就给你 throw 一个错误就行了。

浏览器在限制同源限制的时候,也做出了让步,在实际请求的时候它会发送一个 Option preflight,也就是预检请求,借助 HTTP 报文向服务器协商。

当你请求的时候会告诉服务器信息。


其中响应报文中对此最重要的是:Access-Control-Allow-* 这一系列的约定。

此外还有一些是默认信任的一些内容,比如图片 <img src='//'> ,视频这些静态资源和一个比较特殊 iframe(因为这个本来就需要访问其他的网站),它会使用 sandbox来做出安全保证。

如何解决问题?

常规方案

正如上文所述,可以和浏览器协商,这也是最合理的方案,只需要后端在返回报文信息的时候,设置对应响应信息(Access-Control-Allow-* 这一系列的约定)。

HACK 方案

当后端忙的时候咋搞,那就之后 hack ,请记住 hack 的方案,它一定只是一个猥琐的手段。上不了正式台面( nginx 除外)。

代理

请记住,既然都叫做代理了,那么之后的请求就应该是让代理去完成,而不是仍然访问原来的地址。比如你希望从 www.couriourc.io/ 访问 www.github.com/api。那么你建立了一个中间层做为代理,那就只需要请求 www.couriourc.io/api 就可以了,(具体替换规则,自由设置)。

具体原理如下图所示:

运维的哥们帮助
1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name localhost;

location ~ /api/ {
root html;
proxy_pass http://127.0.0.1:8080;
}
}

比如这里从 http://localhost/api 代理到了 http://locahost:8080
那么实际请求的时候是使用 /api 请求。
具体到请求代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 为了便于演示,约定请求接口如下
enum HTTPRequestMethod {
GET = "GET",
POST = "POST",
...
}

abstract class HTTP {
request(url:string,method:HTTPRequestMethod,...args): HTTPResponse;
}
// 原本你的请求方式
(http as HTTP).request("http://locahost:8080/","POST");

// 代理之后你应该的请求方式
(http as HTTP).request("/api","POST");

换言之,只要是一个可以作为代理转发的工具都可以来拦截,转发,APACH ,也是如此。

开发工具

在我们开发的时候,有一个东西叫做 HMR ,本质上他就是一个跨域行为,有些打包工具默认提供了代理方法,比如 vue 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// vue.config.js
module.exports = {
devServer: {
host: 'localhost', // 这只是本地开启的服务
port: '80', // 这是你在 dev 时候的端口
proxy: {
'/api': { // /api 表示拦截以/api开头的请求路径
target: 'http://127.0.0.1:8080', // 跨域的域名
changeOrigin: true, // 是否开启跨域
}
}
}
}

自行修改对应的值后,效果如之前所说的 nginx 一样,因为本质是一样的。

手写一个 proxy

可以考虑用 nodejs 手写一个简易服务,在这个服务中,你用常规方案对浏览器响应,然后向服务器发送请求,这样就是一个建议 proxy_pass

浏览器限制下的自由

既然 img, iframe , script 这些原本可以访问(记住只有 GET 请求)。方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
function jsonp(url){
let requestor = document.createElement("script") // 创建一个 script
// 设置请求地址
requestor.setAttribute("src",url);
// 发起请求
requestor.addEventListener("load", ()=>{
console.log("GET请求成功")
console.log(requestor.innerText) // 打印请求结果
});
}
</script>

这样就可以作为跳板,来访问。

易混淆知识点

老是有人把它和 cookie 联系,这之间有一些关联,但是关系并不大。Cookie 是需要遵守同源策略(SameSite)的,也只是说他是本限制的一个点。

  • 同域 Cookie:每次访问的是同一个域下的不同页面、API(每次去的是同一家银行的不同网点,带上这家银行卡即可识别身份)
  • 不同域Cookie:同一个浏览器窗口内可能同时访问A网站和B网站,它们均有各自的Cookie,但访问A时只会带上A的Cookie(你可能有不同银行的多张银行卡,而去某个银行时只有带着他们家的银行卡才去有用嘛)
  • 跨域 Cookie 共享:访问 A 站点时已经登录从而保存姓名、头像等基本信息,这时访问该公司的 B 站点时就自然而然的能显示出这些基本信息,也就是实现信息共享(在银联体系中 A 银行办理的卡也能在 B 银行能取出钱来,也就是实现余额"共享")

请求可以通过 set-cookie 设置字段,但是这也只是说 cookie 被限制,那么之前所说的解决方法也是适用的。

复盘思考

  1. Access-Control-Allow-Origin 值设置为通配符(*)是一定可用的吗?
  2. 这个模式与属于哪个设计模式呢?
  3. jsonp 的解决方法在什么情况适合呢?

!!🚨最后的强调🚨

后面的 hack 方案,在产品环境(build 之后),脱离了对应的环境,那么就会失效,所以大胆点,去找后端,说不定基情和爱情就来了,勇敢爱!!。

 评论