再不懂跨域的话我可就没办法了

284 阅读7分钟

一.什么是跨域?

跨域的产生来源于现代浏览器所通用的同源策略,指只有在url的

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

均一样的情况下,才允许访问相同的cookie,localStorage,以及访问页面的DOM或者发送Ajax请求。若在不同源的情况下访问,就称为跨域

例如以下为同源:

http://www.example.com:8080/index.html
http://www.example.com:8080/home.html

以下为跨域:

http://www.example.com:8080/index.html
http://www3.example.com:8080/index.html

注意⚠️:

但是有两种情况:http默认的端口号为80https默认端口号为443

http://www.example.com:80 === http://www.example.com
https://www.example.com:443 === https://www.example.com

二.为什么浏览器会禁止跨域?

  • 首先,跨域只存在于浏览器端。浏览器它为web提供了访问入口,并且访问的方式很简单,在地址栏输入要访问的地址或者点击某个链接就可以了,正是这种开放的形态,所以我们需要对它有所限制。

  • 所以同源策略它的产生主要是为了保证用户信息的安全,防止恶意的网站窃取数据。分为两种:Ajax同源策略与DOM同源策略:

    • Ajax同源策略它主要做了这两种限制:1.不同源页面不能获取cookie;2.不同源页面不能发起Ajax请求。我认为它是防止CSRF攻击的一种方式吧。因为我们知道cookie这个东西它主要是为了解决浏览器与服务器会话状态的问题,它本质上是存储在浏览器或本地文件中一个小小的文本文件,那么它里面一般都会存储了用户的一些信息,包括隐私信息。如果没有Ajax同源策略,恶意网站只需要一段脚本就可以获取你的cookie,从而冒充你的身份去给其它网站发送恶意的请求。
    • DOM同源策略也一样,它限制了不同源页面不能获取DOM。例如一个假的网站利用iframe嵌套了一个银行网站mybank.com,并把宽高或者其它部分调整的和原银行网站一样,仅仅只是地址栏上的域名不同,若是用户没有注意的话就以为这个是个真的网站。如果这时候用户在里面输入了账号密码,如果没有同源策略,那么这个恶意网站就可以获取到银行网站中的DOM,也就能拿到用户的输入内容以此来达到窃取用户信息的攻击。

同源策略它算是浏览器安全的第一层屏障吧,因为就像CSRF攻击,它只能限制不同源页面cookie的获取,但是攻击者还可能通过其它的方式来达到攻击效果。

(注,上面提到的iframe限制DOM查询,案例如下)

// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

三.解决方案

这里我只写一下我了解的,以及比较常用的。

3.1 jsonp跨域

基本原理:主要就是利用 script 标签的src属性没有跨域的限制,通过指向一个需要访问的地址,由服务端返回一个预先定义好的 Javascript 函数的调用,并且将服务器数据以该函数参数的形式传递过来,此方法需要前后端配合完成。

执行过程:

  • 前端定义一个解析函数(如: jsonpCallback = function (res) {})
  • 通过params的形式包装script标签的请求参数,并且声明执行函数(如cb=jsonpCallback)
  • 后端获取到前端声明的执行函数(jsonpCallback),并以带上参数且调用执行函数的方式传递给前端
  • 前端在script标签返回资源的时候就会去执行jsonpCallback并通过回调函数的方式拿到数据了

「JSONP优点」

  • 兼容性比较好,在一些古老的浏览器中都可以运行。

「JSONP缺点」

  • 只能进行GET请求

「与AJAX对比」

  • JSONP和AJAX相同,都是客户端向服务器发送请求,从服务器获取数据的方式。但是AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

代码实现:

(具体可以看这篇文章:JSONP原理及实现

3.1.1 封装一个JSONP方法

function JSONP({
url,
params = {},
callbackKey = 'cb',
callback
}) {
JSONP中维护一个数组用于储存回调函数,每个回调函数用id标识
// 定义本地的唯一callbackId,若是没有的话则初始化为1
JSONP.callbackId = JSONP.callbackId || 1;
let callbackId = JSONP.callbackId;
// 把要执行的回调加入到JSON对象中,避免污染window
JSONP.callbacks = JSONP.callbacks || [];
JSONP.callbacks[callbackId] = callback;
// 把这个名称加入到参数中: 'cb=JSONP.callbacks[1]'
params[callbackKey] = `JSONP.callbacks[${callbackId}]`;
// 得到'id=1&cb=JSONP.callbacks[1]'
const paramString = Object.keys(params).map(key => {
return `${key}=${encodeURIComponent(params[key])}`
}).join('&')
// 创建 script 标签
const script = document.createElement('script');
script.setAttribute('src', `${url}?${paramString}`);
document.body.appendChild(script);
// id自增,保证唯一
JSONP.callbackId++;

}

具体实现,看上面文章。

3.2跨域资源共享CORS

跨域资源共享(CORS)是一种机制,是W3C标准。它允许浏览器向跨源服务器,发出XMLHttpRequestFetch请求。并且整个CORS通信过程都是浏览器自动完成的,不需要用户参与。

而使用这种跨域资源共享的前提是,浏览器必须支持这个功能,并且服务器端也必须同意这种"跨域"请求。因此实现CORS的关键是服务器需要服务器。通常是有以下几个配置:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Credentials
  • Access-Control-Max-Age

具体可看:developer.mozilla.org/zh-CN/docs/…

过程分析:

  • 浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求;若不同源,则发送跨域请求。
  • 服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含 Access-Control-Allow-origin 字段,若配置过域名,则返回 Access-Control-Allow-origin + 对应配置规则里的域名的方式
  • 浏览器根据接受到的 响应头里的 Access-Control-Allow-origin 字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。

另外在CORS中有简单请求非简单请求,简单请求是不会触发CORS的预检请求的,而非简单请求会。

“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

3.2.1 CORS的哪些是简单请求?

简单请求不会触发CORS的预检请求,若请求满足所有下述条件,则该请求可视为“简单请求”:

简单回答

  • 只能使用GETHEADPOST方法。使用POST方法向服务器发送数据时,Content-Type只能使用application/x-www-form-urlencodedmultipart/form-datatext/plain编码格式。
  • 请求时不能使用自定义的HTTP Headers

详细回答

  • (一) 使用下列方法之一

    • GET
    • HEAD
    • POST
  • (二) 人为设置以下集合外的请求头

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(但是有限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • (三) Content-Type的值仅限于下面的三者之一

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

  • 请求中没有使用 ReadableStream 对象。

除了上面这些请求外,都是非简单请求。

3.2.2 CORS的预检请求具体是怎样的?

若是跨域的非简单请求的话,浏览器会首先向服务器发送一个预检请求,以获知服务器是否允许该实际请求。

整个过程大概是:

  • 浏览器给服务器发送一个OPTIONS方法的请求,该请求会携带下面两个首部字段:
    • Access-Control-Request-Method: 实际请求要用到的方法
    • Access-Control-Request-Headers: 实际请求会携带哪些首部字段
  • 若是服务器接受后续请求,则这次预请求的响应体中会携带下面的一些字段:
    • Access-Control-Allow-Methods: 服务器允许使用的方法
    • Access-Control-Allow-Origin: 服务器允许访问的域名
    • Access-Control-Allow-Headers: 服务器允许的首部字段
    • Access-Control-Max-Age: 该响应的有效时间(s),在有效时间内浏览器无需再为同一个请求发送预检请求
  • 预检请求完毕之后,再发送实际请求

这里有两点要注意:

一:

Access-Control-Request-Method没有s

Access-Control-Allow-Methodss

二:

关于Access-Control-Max-Age,浏览器自身也有维护一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效,而是以最大有效时间为主。

3.2.3 CORS附带身份凭证

对于跨域 XMLHttpRequestFetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest<span> </span>的某个特殊标志位。

例如我们想要在跨域请求中带上cookie,需要满足3个条件:

  • web(浏览器)请求设置withCredentialstrue
  • 服务器设置首部字段Access-Control-Allow-Credentialstrue
  • 服务器的Access-Control-Allow-Origin不能为*

3.2.4 如何减少CORS预请求的次数?

方案一:发出简单请求(这不是废话吗...)

方案二:服务端设置Access-Control-Max-Age字段,在有效时间内浏览器无需再为同一个请求发送预检请求。但是它有局限性:只能为同一个请求缓存,无法针对整个域或者模糊匹配 URL 做缓存。