我眼中的"跨域"

171 阅读6分钟

1. 源

源指的是访问URL的协议、主机(域名)和端口

当这三者完全一致时,称为同源

http://Example.com
http://example.com
是同源,域名不区分大小写(会统一转成小写)

2. 同源策略

同源策略用于限制一个源的资源如何于另一个源的资源进行交互,即限制不同于源之间相互的访问。这是浏览器的重要的安全策略

同源策略控制不同源之间的交互,具体如下:

  • 跨域写操作,一般是被允许的。例如链接,重定向和表单提交

  • 跨域资源嵌入,一般是被允许的。例如:

    <script src='xxx'></script>嵌入跨域脚本

    <link rel='stilesheet' href='xxx'>标签嵌入CSS

    通过<img>展示的图片

    通过<video><audio>播放的多媒体资源

    通过<boject><embed><applet>嵌入的插件

    通过@font-face引入的字体,部分浏览器允许跨域字体,部分需要同源字体

    通过<iframe>载入的任何资源

  • 跨域读操作,一般不被允许,比如通过AJAX获取其他源的资源

跨域的错误一般在控制台提示如下

error.png

3. CORS

跨源资源共享(跨域资源共享)是一种基于HTTP头的机制,该机制允许服务器标示除了它之际以外的其他源访问加载服务器上的资源

现在有a.com(http://localhost:8888)和`b.com`(http://localhost:9999),两个不同源,如果`b.com`想要访问`a.com`的`aa.json`,需要在a对应的服务器脚本上加上

if (path === '/aa.json') {
        res.statusCode = 200
        res.setHeader('Content-Type', 'text/json;charset=utf-8')
        res.setHeader('Access-Control-Allow-Origin', 'http://localhost:9999')
        res.write(fs.readFileSync('aa.json'))
        res.end()
    }
 res.setHeader('Access-Control-Allow-Origin', 'http://localhost:9999')

设置响应头Access-Control-Allow-Origin:<origin> | *,标识允许某个域可以访问。上例中允许http://localhost:9999访问`a.com`的数据,注意如果将`localhost`改成对应`IP`,将不会匹配上。通常它的值有如下几种设置:

  1. 设置*,所有域都可以访问,但不够安全
  2. 指定域,如上例中的,固定写死
  3. 设置白名单,拿到访问的域名后,去匹配该域名是否存在于白名单中,如果存在则将Access-Control-Allow-Origin设置为该域名

关于CORS的补充:

简单请求:不会触发CORS预检请求(对于可能对服务器数据产生副作用的HTTP请求)的请求,一般满足以下条件

  • 使用下列请求方法
    • GET
    • HEAD
    • POST
  • 对CORS安全的首部字段集合
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(需要注意额外的限制)
  • Content-Type的值仅限于下列三者之一:
    • text/plain(数据以纯文本形式进行编码)
    • multipart/form-data(专门用来传输特殊类型数据,比如图片、音频等)
    • application/x-www-form-urlencoded(通过表单发送数据时默认的编码类型)
  • 请求中的任意 XMLHttpRequest对象均没有注册任何事件监听器;XMLHttpRequest对象可以使用 XMLHttpRequest.upload 属性访问
  • 请求中没有使用 ReadableStream 对象

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

预检请求中,会携带下面两个首部字段

Access-Control-Request-Method:值 // 告诉服务器实际请求的方法
Access-Control-Request-Headers:值 //告诉服务器实际请求将携带的自定义请求首部字段

服务器会根据这两个首部字段,判断是否通过

附带身份凭证的请求

当XMLHttpRequest发送请求时,设置xml.withCredentials 为 true,浏览器会发送身份凭证信息向服务器发送 Cookies,这是一个简单 GET 请求,所以浏览器不会对其发起“预检请求”。但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials : true,浏览器将不会返回响应内容

注意,在响应附带身份凭证的请求时

  • 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域
  • 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为首部名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  • 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET

对于附带身份凭证的请求(通常是 Cookie),服务器不得设置 Access-Control-Allow-Origin 的值为“*”。

4. JSONP

一般采用CORS解决跨域问题,但如果需要兼容IE10以下版本,可以采用JSONP跨域

主要思路是,由于同源策略可以跨域资源嵌入(例如通过script标签获取不同源的js),可以动态生成script标签,获取其他源的数据

沿用上例,可以将a.comaa.json,在服务端改成aa.js,然后b.com通过script获取aa.js

// aa.js
window.xxx={{json}}

// server.js
if (path === '/aa.js') {
        res.statusCode = 200
        res.setHeader('Content-Type', 'text/javascript;charset=utf-8')
        let string = fs.readFileSync('aa.js').toString();
        let jsonData = fs.readFileSync('aa.json').toString();
        string = string.replace('{{ json }}', jsonData).replace('{{json}}', jsonData);
        res.write(string)
        res.end()
    }

 // b.com的b.js
let script = document.createElement('script');
script.src = `http://localhost:8888/aa.js`
document.head.appendChild(script)
script.onload = ()=>{
    console.log(window.xxx)
}

可以将aa.js改成函数 window.xxx({{json}})

// a.js
window.xxx({{json}})

// b.com的b.js
window.xxx = (data)=>{ // 回调函数
    console.log(data)
}
let script = document.createElement('script');
script.src = `http://localhost:8888/aa.js`
document.head.appendChild(script)

为了避免xxx被占用,将函数名改成随机数

// aa.js
window['{{xxx}}']({{json}})

// server.js
if (path === '/aa.js') {
        res.statusCode = 200
        res.setHeader('Content-Type', 'text/javascript;charset=utf-8')
        let string = fs.readFileSync('aa.js').toString();
        let jsonData = fs.readFileSync('aa.json').toString();
    	string = string.replace('{{xxx}}',query.functionName).replace('{{ json }}', jsonData).replace('{{json}}', jsonData);
        res.write(string)
        res.end()
    }

 // b.com的b.js
let script = document.createElement('script');
let funcName = Math.random()
script.src = `http://localhost:8888/aa.js?functionName=${funcName}`
document.head.appendChild(script)
window[funcName] = (data)=>{
    console.log(data)
}

jsonpfunctionName约定俗成是callback

如果另一个源c.com也知道通过jsonp获取a.com的数据,但a.com拒绝c.com的访问,怎么控制呢?

可以考虑通过referer匹配允许的源,是经过许可的源才允许通过jsonp形式访问数据

req.headers.referer.indexOf('http://localhost:9999') === 0

5. 封装JSONP

function jsonp(url){
    return new Promise((resolve,reject)=>{
        let script = document.createElement('script');
        let functionName = Math.random();
        script.src = `${url}?callback=${functionName}`
        document.head.appendChild(script)
        window[functionName] = (data)=>{
            resolve(data)
        }
        script.onload = ()=>{
            script.remove()
        }
        script.onerror = (err)=>{
            reject(err)
        }
    })
}
jsonp('http://localhost:8888/aa.js').then((data)=>{
    console.log(data)
})

可以将aa.js直接在后台赋值,减少一个文件

if (path === '/aa.js') {
        res.setHeader('Content-Type', 'text/javascript;charset=utf-8')
        // referer检查
        if(req.headers.referer.indexOf('http://localhost:9999') === 0){
            res.statusCode = 200
            let string = 'window["{{xxx}}"]({{json}})'; // 替换aa.js
            let jsonData = fs.readFileSync('aa.json').toString();
            string = string.replace('{{xxx}}',query.callback).replace('{{ json }}', jsonData).replace('{{json}}', jsonData);
            res.write(string)
            res.end()
        }else{
            res.statusCode = 404
            res.write('不存在')
            res.end()
        }
    }

jsonp的优缺点

  • 优点:兼容IE;不像XMLHttpRequest对象实现的AJAX请求收到同源策略的限制,可以跨域

  • 缺点:由于是通过动态生成script获取数据,所以

    只支持GET请求,不支持POST等其他类型的请求

    没有精确的响应信息,比如状态码

6. 其他跨域方法

  • WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
  • postMessage,可以安全地实现跨源通信,主要适用于页面与嵌套的iframe页面通信;源页面与window.open()新打开的页面通信;源页面与通过a标签打开的页面通信,部分场景下可以实现跨域,它是为了解决跨源通信,而不是跨域

待完善...