同源策略和跨域问题(包含Ajax)

237 阅读5分钟

1. 同源策略

概念

如果有两个url的 协议域名端口号完全一致,那么这两个url就是同源的在浏览器里打开页面则默认遵循同源策略。

源的概念

协议(HTTP/HTTPS)+端口+域名

限制

不是同一个源,不能权限操作另一个源的数据;不同源的页面间,不需互相访问数据。

优缺点

  • 优点:保证用户的隐私安全和数据安全
  • 缺点:当前端需要访问另一个域名的后端接口时,会被浏览器阻止其获取相应。

2. 前后端通信

  • Ajax:只适合同源通信
  • WebScoket:不受同源策略限制
  • CORS:支持同源通通信支持跨域通信
  • Fetch API:只适合同源通信

3. Ajax

原理

简单来说通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。

实现过程

实现 Ajax异步交互需要服务器逻辑进行配合,需要完成以下步骤:

  • 创建 Ajax的核心对象 XMLHttpRequest对象
  • 通过 XMLHttpRequest 对象的 open() 方法与服务端建立连接
  • 构建请求所需的数据内容,并通过XMLHttpRequest 对象的 send() 方法发送给服务器端
  • 通过 XMLHttpRequest 对象提供的 onreadystatechange 事件监听服务器端你的通信状态
  • 接受并处理服务端向客户端响应的数据结果
  • 将处理结果更新到 HTML页面中

实例

  1. 初始化XMLHttpRequest实例对象
const xhr = new XMLHttpRequest();
  1. 通过 XMLHttpRequest 对象的 open() 方法与服务器建立连接
xhr.open(method, url, [async][user][password])
参数说明
method表示当前的请求方式,常见的有GETPOST
url服务端地址
async布尔值,表示是否异步执行操作,默认为true
user可选的用户名用于认证用途;默认为null
password可选的用户名用于认证用途;默认为null
  1. 通过send() 方法,将客户端页面的数据发送给服务端
xhr.send([body])

body: 在 XHR 请求中要发送的数据体,如果不传递数据则为 null

如果使用GET请求发送数据的时候,需要注意如下:

  • 将请求数据添加到open()方法中的url地址中
  • 发送请求数据中的send()方法中参数设置为null
  1. 绑定onreadystatechange事件
  • onreadystatechange 事件用于监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState
  • 只要 readyState属性值一变化,就会触发一次 readystatechange 事件
  • XMLHttpRequest.responseText属性用于接收服务器端的响应结果
const request = new XMLHttpRequest();
request.open('GET', 'https://xxx');
request.onreadystatechange = function () {
  if (request.readyState === 4) {
    if (request.status >= 200 && request.status <= 300 || request.status === 304) {
      console.log(request.responseText) // 服务端返回的结果
    } else {
      console.log("错误信息:" + request.status)
    }
  }
};

request.send('{"xxx":"xxx"}');

封装

//封装一个ajax请求
function ajax(options) {
    //创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest()

    //初始化参数的内容
    options = options || {}
    options.type = (options.type || 'GET').toUpperCase()
    options.dataType = options.dataType || 'json'
    const params = options.data

    //发送请求
    if (options.type === 'GET') {
        xhr.open('GET', options.url + '?' + params, true)
        xhr.send(null)
    } else if (options.type === 'POST') {
        xhr.open('POST', options.url, true)
        xhr.send(params)

    //接收请求
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            let status = xhr.status
            if (status >= 200 && status < 300) {
                options.success && options.success(xhr.responseText, xhr.responseXML)
            } else {
                options.fail && options.fail(status)
            }
        }
    }
}

使用方式:

ajax({
    type: 'post',
    dataType: 'json',
    data: {},
    url: 'https://xxxx',
    success: function(text,xml){   //请求成功后的回调函数
        console.log(text)
    },
    fail: function(status){       //请求失败后的回调函数
        console.log(status)
    }
})

4. 跨域的解决方法

1. JSONP

  • 甲站点利用script标签可以跨域/异步加载的特性,向乙站点发送GET请求
  • 乙站点后端改造 JS 文件内容,将数据传进回调函数
  • 甲站点通过回调函数拿到乙站点数据
var jsonp = function() {
  // 创建script标签
  var script = document.createElement('script');
  
  // 给script添加属性,如src,在src中约定callback函数。
  script.src = 'http:/www.test.com/?name=zhangsan&callback=jsonp'
  
  // script加载完的回调
  script.onload = function() {}
  
  // script加载失败的回调
  script.onerror = function() {}
  
  // 将script添加到head标签中
  document.getElementsByTagName('head')[0].appendChild(script);
}

优点

  • 兼容IE
  • 可以跨域
  • 改动较小,只需要后端改造JS文件内容,将数据传进回调函数

缺点

  • 由于它是script标签,所以它无法读取到Ajax那么精确,接收不到状态码及响应头等
  • 用户认证缺失
  • 由于它是script标签,因此它只能发GET请求,不支持POST

2. CORS

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),他允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服了Ajax只能同源使用的限制。浏览器将CORS请求分为简单请求和非简单请求两类。

简单请求

对于简单请求(GET,HEAD,POST),乙站点在响应头里添加 Access-Control-Allow-Origin:https://甲站点 即可.

// node.js设置响应头 
response.setHeader('Access-Control-Allow-Origin','https://甲站点')

非简单请求

对于复杂请求,如 PATCH ,被访问(乙)站点需要

  • 先响应 OPTIONS 请求,在响应中添加如下的响应头:
Access-Control-Allow-Origin: https://甲站点
Access-Control-Allow-Methods: POST, GET, OPTIONS, PATCH
Access-Control-Allow-Headers: Content-Type
  • 然后响应 POST 请求,在响应中添加 Access-Control-Allow-Origin 头。

其他

如果需要附带身份信息,JS 需要在 AJAX 里设置xhr.withCredentials = true

优缺点

优点

使用方便

缺点

不兼容IE6、7、8、9浏览器

3. Proxy(包含 Nginx / Node)

本地服务器作为请求代理对象

通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象。通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

// 添加至 vue.config.js
amodule.exports = {
    devServer: {
        host: '127.0.0.1',
        port: 8084,
        open: true,// vue项目启动时自动打开浏览器
        proxy: {
            '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
                changeOrigin: true, //是否跨域
                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
                    '^/api': "" 
                }
            }
        }
    }
}
// 通过 axios发送请求,配置请求的根路径
axios.defaults.baseURL = '/api'

Nginx 代理

还可通过服务端实现代理请求转发,前端 => 后端 => 另一个域名的后端。

server {
    listen    80;
    # server_name www.josephxia.com;
    location / {
        root  /var/www/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    location /api {
        proxy_pass  http://127.0.0.1:3000;
        proxy_redirect   off;
        proxy_set_header  Host       $host;
        proxy_set_header  X-Real-IP     $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

node.js 代理

var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
                      }));
module.exports = app

4. WebSocket

是一种网络传输协议,可在单个TCP连接上进行全双工通信,即通信允许数据在两个方向上同时传输,能更好的节省服务器资源和带宽并达到实时通讯,本身不受同源策略限制。

// 1. 申明
var ws = new WebSocket('wss://....'); // wss/ws => 加密/不加密
// 2. 建立链接
ws.onopen = function(e) {
  console.log('开始连接...');
  ws.send('hello WebSocket!');
}
// 3. 接受消息
ws.onmessage = function(e) {
  console.log('接收数据:'+ e.data);
  ws.close() // 关闭连接
}
// 4. 关闭连接的回调,确定以关闭连接
ws.onclose= function(e) {
  console.log('连接关闭');
}

优点

  • 控制开销小,数据包头部协议较小,不同于http每次请求需要携带完整的头部
  • 实时性强,相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少
  • 保持创连接状态,创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证
  • 支持扩展,用户可以扩展websocket协议、实现部分自定义的子协议
  • 定义了二进制帧,更好处理二进制内容

应用场景

  • 弹幕
  • 媒体聊天
  • 协同编辑
  • 基于位置的应用
  • 体育实况更新
  • 股票基金报价实时更新

5. PostMessage

// 如A源向B源发送数据data
// A, 参数含义:发送的数据,接受方的源或者"*"
window.postMessage('data', 'http/B.com'); 
// B: 监听message,判断消息来源,接受数据
window.addEventListerr('message', function(event) {
    console.log(event);
    // event.origin // 消息来源,'http/A.com'
    // event.source // A的引用window
    // event.data  // 接受的数据
}, false)