Skip to content
On this page

跨域请求

一个域上的文档或者脚本试图请求另一个域下的资源,就是跨域。比如

  • 资源跳转:a 链接、重定向、表单提交;
  • 资源引入:link、script、img、iframe 等标签,css background:url()、font-face;

同源限制--狭义的跨域

同源策略(same origin policy)是一种浏览器安全策略,即只允许浏览器向同源的服务器请求资源(请求还是会发送,服务器也会响应,只是响应被浏览器拦截了),可避免浏览器遭到 xss、csfr 等攻击。同源:协议+域名+端口三者相同,即使不同的域指向同一个 ip,也非同源。

同源限制了几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法获取;
  • DOM 和 JS 对象无法获得;
  • AJAX 返回值不能获取。

容易混淆的跨域:

http://www.domain.com/a.js

http://192.168.4.12/b.js

域名和域名对应相同 ip 不允许

http://www.domain.com/a.js

http://x.domain.com/b.js

http://domain.com/c.js

主域相同,子域不同 不允许

跨域解决方案

  • JSONP
  • CORS
  • WebSocket
  • node 中间件代理

还有其他一些方法,本文主要介绍 JSONP 和 CORS。

jsonp 实现跨域

原理:具有 src 属性的标签在请求资源时,不受同源策略限制(历史遗留问题),可以通过这些标签(img、script 等)加载外域的脚本,在本域执行,实际是加载一个函数调用。

例如:

html
<!-- 在 http://127.0.0.1:8016/ 的页面内有一下标签 -->
<script>
  function localHandler(data) {
    alert(data.name)
  }
</script>
<!-- jsonp.js 脚本里有 localHandler 函数,该函数在本域有定义,加载该脚本后就会执行本域的 localHandler 处理数据  -->
<script src="http://localhost:3001/jsonp.js"></script>
js
//http://localhost:3001 域下有如下脚本 jsonp.js
localHandler({ name: 'jack', age: 24 })

关键:外域上的脚本是一个执行函数,函数参数是一个对象或者 JSON ,且与本域的处理函数同名。

问题:外域的脚本是写死了,我们如何统一本域和外域的函数名呢?

改进 1:在 src 传递过去就好,其实还可传递其他参数。

html
<script>
  function localHandler(data) {
    alert(data.name)
  }
</script>
<!-- 在本域传递参数 -->
<script src="http://localhost:3001/jsonp.js?callback=localHandler&id=abc8848"></script>

外域服务端处理:

js
//node 代码
let query = Url.parse(req.url, true).query
let callback = query.callback // 本地传递过来的本地函数名
let id = query.id // 假设根据id,查询数据库,获取用户信息 userInfo
let userInfo = { id, name: 'jack', age: 24 }
let response = callback + '(' + JSON.stringify(userInfo) + ')' // 将处理函数和传递的数据组合
res.write(response)
res.end()

问题:在本域 script 标签写死了,动态插入,会更加好。

改进 2:动态插入 script 标签。

js
//本域动态插入 script 标签
//处理返回数据
var localHandler = function (data) {
  console.log(data)
}
// 提供 jsonp 服务的 url 地址(生成的返回值都是一段javascript代码)
// 将处理函数传递给服务端,这里查询 id 为 abc8848 的用户信息
var url = 'http://localhost:3001/jsonp?callback=localHandler&id=abc8848'
// 创建script标签,设置其属性
var script = document.createElement('script')
script.setAttribute('src', url)
// 把script标签加入 head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script)

外域服务端处理:

js
// node 代码
let query = Url.parse(req.url, true).query
let callback = query.callback //本地传递过来的本地函数名
let id = query.id // 假设根据 id,查询数据库,获取用户信息 userInfo
let userInfo = { id, name: 'jack', age: 24 }
let response = callback + '(' + JSON.stringify(userInfo) + ')' //将处理函数和传递的数据组合
res.write(response)
res.end()

jquery AJAX 跨域

html
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
  $(document).ready(function () {
    $.ajax({
      type: 'get',
      async: false,
      url: 'http://localhost:3001/jsonp?id=abc8848',
      dataType: 'jsonp',
      jsonp: 'callback', //传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)
      jsonpCallback: 'localHandler', //自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
    })
      .done(json => {
        console.log(json)
      })
      .fail(err => {
        console.log(err)
      })
  })
</script>

外域服务处理同上。

jsonp 的优缺点:

优点

  • 所有浏览器都支持。
  • 简单。

缺点

  • 由于返回的数据当成脚本执行,会存在脚本注入的安全问题。
  • 只能发一次请求。
  • 只能使用 GET 方法。
  • 不好处理请求错误。

cors 跨域

CORS 需要浏览器和服务器同时支持。目前主流浏览器都都支持 cors,cors 通信过程浏览器自动完成,和同源通信无差别。cors 关键是服务器,只要服务器设置了 CORS,就能实现跨域请求。

两种请求

满足以下两个条件的是简单请求,否则为非简单请求。

  • 请求方法为:GET|POST|HEAD
  • 请求头部超出以下字段:
    • Accept|Accept-Language|Content-Language|Last-Event-ID
    • Content-Type 的值限制在application/x-www-form-urlencoded|multipart/form-data|text/plain

简单请求和非简单请求,浏览器处理不同。

简单请求

简单请求,浏览器器直接发出 CORS 请求,会自动在请求头中增加 Origin 字段,告诉服务器,本次请求从哪个源(协议+域名+端口号)发起,服务器根据该源,决定是否同意本次请求。

服务响应头信息Access-Control-Allow-Origin包含源,则同意请求,否则出错,触发 XHR 的 error 事件。注意,这种错误无法根据状态码识别,因为可能为 200。

简单请求,响应头信息除了Access-Control-Allow-Origin字段必须设置外,可选地设置以下字段:

Access-Control-Allow-Credentials:布尔值,是否允许发起者获取响应内容。CORS 默认发送 Cookie,但是响应不携带Access-Control-Allow-Credential:true,浏览器不会把响应内容返回给请求的发起者(即该请求拿不到响应)。不需要携带认证信息,可设置请求xhr.withCredentials = false;。附带认证的请求,Access-Control-Allow-Origin必须设置一个具体的值,否则请求将失败。响应中携带 Set-Cookie字段,尝试对 Cookie 进行修改,失败会抛出异常。

Access-Control-Expose-Headers:CORS 请求时,XHR 的getResponseHeader()只能拿到 6 个头信息:Cache-Control|Content-Language|Content-Type|Expires|Last-Midified|Pragma。想获取到其他字段,必须在这个响应头里指定。

非简单请求

非简单请求,在发送实际请求之前,会发送一个预请求(preflight),以确定跨域信息。跨域信息包括:**允许跨域的域 + 方法 + 请求 URL + 特殊的请求头字段 + credentials **。跨域信息符合实际请求,才会发起实际请求,否则报错。通过预检请求会,在跨域缓存时间内,不会再发起预检请求。

实际请求:

js
var url = 'http://api.alice.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('PUT', url, true) // PUT
xhr.setRequestHeader('X-Custom-Header', 'value') //自定义请求头
xhr.send()

浏览器检测到非简单请求,会发情预检请求:

js
OPTIONS /cors HTTP/1.1    // OPTIONS 方法,询问跨域信息
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...

预请求响应:

js
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 // 允许跨域的请求头字段 必需的
Access-Control-Max-Age: 1728000  // 缓存跨域请求的时间,单位为秒,非必需
Access-Control-Allow-Credentials: false // 响应内容控制,非必需
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

实际请求满足预请求响应,会自动发送实际请求,否则触发 error 事件。通过了预检请求,以后只要在跨域缓存时间内(注意,URL 变了,跨域缓存信息也会失效),就和简单请求一样,携带 Origin 字段,服务器响应,有Access-Control-Allow-Origin字段。

实际请求:

js
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

实际响应(部分字段):

js
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

CORS 优劣

优点:

  • 支持的方法多;
  • 更加安全。

缺点:

  • 老旧浏览器不支持;
  • 会多发请求。

相比 CORS 的优点,缺点可忽略。

参考

前端 | 浅谈 preflight request

Released under the MIT License.