同源策略及规避方法

338 阅读4分钟

概述

定义

A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。

  • 协议相同
  • 域名相同
  • 端口相同

目的

是为了保证用户信息的安全,防止恶意的网站窃取数据。

限制范围

  • Cookie、LocalStorage 和 IndexDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

cookie

  1. 一级域名相同时,可通过设置相同的 document.domain 来设置两个页面的访问。(只适用于cookie 和 iframe窗口)
  2. 服务器在响应的时,设置domain 为一级域名,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
Set-Cookie: key=value; domain=.example.com; path=/

AJAX

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。规避的几种方法

  • JSONP
  • WebSocket
  • CORS

JSONP

  • JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
  • 它的基本思想是,网页通过添加一个 script 元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

前端实现:

function jsonP (url, callbackName = 'callback') {
    return new Promise((resolve, reject) => {
        const scriptTag = document.createElement('script')
        scriptTag.src = url + `?cb=${callbackName}`
        scriptTag.setAttribute('type', 'text/javascript')
        document.body.appendChild(scriptTag)

        window[callbackName] = function jsonPCallback (data) {
            if (data) resolve(data)
            else reject(new Error('no data'))

            document.body.removeChild(scriptTag)
        }
    })
}

jsonP('http://localhost:3000/jsonp').then(res => {
    this.res = JSON.stringify(res)
})

// 返回数据样例
/**/ typeof callback === 'function' && callback({"data":{"name":"Tom"}});

服务端实现(以express为例):

// app.js 添加cb函数名字段,该参数为前端查询参数
app.set('jsonp callback name', 'cb')

// 直接使用jsonp返回即可
router.get('/jsonp', function(req, res) {
  res.jsonp({data: {name: 'Tom'}})
});

// 前端请求实例:http://localhost:3000/jsonp?cb=callback

websocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

基本实现(socket.io)

前端:

const socket = socketIo('http://localhost:3000/', {
    query: {
        auth: '123'
    }
})

ocket.on('connect', () => {
    this.connectStatus = `链接成功-${socket.id}`
})

socket.on('disconnect', () => {
    this.connectStatus = '链接断开'
})

socket.on('connect_error', (err: Error) => {
    this.connectStatus = `链接失败-${err.message}`
})

socket.on('Hi', (message: string) => {
    this.message = message
})

服务端实现:

const io = require('socket.io')({
    path: '/socket.io',
    serveClient: false
});

io.on('connect', (socket) => {
    // 校验跨域源信息
    if (!['http://localhost:8080'].includes(socket.handshake.headers.origin)) {
        return socket.disconnect()
    }

    socket.emit('Hi', 'Hello');
})

io.attach(server, {
    pingInterval: 10000,
    pingTimeout: 5000,
    cookie: false
});

浏览器跨域资源共享-CORS

1. 概述

  • 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制.
  • 跨源资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
  • 规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先使用 OPTIONS 方法发起一个预检请求,从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

2. 添加的头部信息

请求首部信息
  1. Origin 请求源信息,即当前源信息。部字段表明预检请求或实际请求的源站。
  2. Access-Control-Request-Headers 用于预检请求,告诉服务器额外增加的首部字段。逗号分割
  3. Access-Control-Request-Method 用于预检请求,告诉服务器要使用的请求方法。
响应首部信息
  1. Access-Control-Allow-Origin 服务器允许跨域的源信息。
  2. Access-Control-Allow-Methods 预检请求响应,服务器允许跨域请求的方法。
  3. Access-Control-Expose-Headers 预检请求响应,服务器允许携带的头部信息,这里允许,前端才能读到响应的这些头部信息。
  4. Access-Control-Max-Age 预检请求响应,表示预检请求的结果可以缓存多久,单位秒。
  5. Access-Control-Allow-Credentials 指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容。当用在对预检测请求的响应中时,它指定了实际的请求是否可以使用credentials。

2. 访问控制场景

简单请求

这类请求不会触发预检请求

  • 属于简单请求的方法: 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

请求携带的首部信息:

Origin: http://foo.example

请求响应首部信息:

Access-Control-Allow-Origin: http://foo.example
非简单请求

非简单请求会在正式请求之前,发送一个预检请求,用来询问服务器,是否可以跨域请求以及允许的请求方法,头部字段等。

预检请求携带的首部信息:

Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

预检请求响应首部信息:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Expose-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

附带身份凭证请求

  • XMLHttpRequest 的 withCredentials 标志设置为 true,从而向服务器发送 Cookies。
  • 对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。必须指定明确的、与请求网页一致的域名。
  • Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
  • Access-Control-Allow-Credentials 响应请求需要携带为true,如果没有携带或者为false,简单请求浏览器将不会把响应内容返回给请求的发送者。预检请求会报错。

参考文档

  1. 跨源资源共享(CORS) - MDN
  2. 跨域资源共享 CORS 详解 - 阮一峰