前端跨域知識總結

導語: 在平常的開發過程當中,跨域是一個使人頭疼的事情,也在跨域的過程當中學到了很多知識,也吃了很多跨域的虧,一直沒有時間整理這部分的知識點,如今就我腦海中的跨域知識以及結合開發過程當中遇到的坎進行一個系統的總結。javascript

目錄

  • 跨域是什麼
  • 跨域解決方法

跨域是什麼

說到跨域,就不得不提遊覽器的同源安全策略。按照MDN所說,同源策略限制了從同一個源加載的文檔或者腳本如何和來自另外一個源的文檔和腳本等資源進行交互,這是阻止惡意文件程序攻擊的一個重要安全機制。css

同源就是協議(http\https),域名(www.taobao.com)和端口號(80,8080)都相同,這才稱之爲同源,與之相對的皆是不一樣源,是跨域的範圍了。html

這裏列一個表格來講明一下:vue

假設這裏有一個url爲http://www.example.com/a.html, 想要請求如下地址,默認端口爲80。java

序號 url 是否同源 緣由
1 http://www.example.com/b.html 協議、域名、端口相同,只是文件路徑不一樣
2 https://www.example.com/c.html 協議不一樣(http\https)
3 http://store.example.com/c.html 子域域名不一樣(www\store)
4 http://www.example.com:81/c.html 端口不一樣(80/81)
5 https://shop.example.com:82/c.html 協議、域名、端口都不一樣

注意:有時候w3c的標準到了ie這裏就行不通了。因此根據ie遊覽器的標準,有兩個不一樣點:一是授信範圍,也就是高度互信的兩個域名,不受同源限制;二是端口不一樣的兩個域名,不受同源限制。node

跨域解決方法

據我所知,跨域有如下幾種方法:jquery

  • jsonp方法,只適用於get請求
  • CROS,(跨域資源共享協議),適用於各類請求
  • domain設置,只適用於子域
  • post Message,適用於父子網頁iframe通訊

jsonp方法

採用這種方法,是因爲html標籤src屬性不受同源限制,下面就封裝一個jsonp方法來進行跨域資源請求。ios

  • 原生方法
function jsonp({url,callback}) {
  let script = document.createElement('script');
  script.src = `${url}&callback=${callback}`;
  document.head.appendChild(script);
}
複製代碼

一個簡單的案例:nginx

先用node開一個3000端口,做爲服務端,啓動node server.jsgit

// 保存爲server.js
const http = require('http');
const url = require('url');
const queryString = require('querystring');
const data = JSON.stringify({
  title: 'hello,jsonp!'
})

const server = http.createServer((request, response) => {
  let addr = url.parse(request.url);
  if (addr.pathname == '/jsonp') {
    let cb = queryString.parse(addr.query).callback;
    response.writeHead(200, { 'Content-Type': 'application/json;charset=utf-8' })
    response.write(cb + '('+ data +')');
  } else {
    response.writeHead(403, { 'Content-Type': 'text/plain;charset=utf-8' })
    response.write('403');
  }
  response.end();
})

server.listen(3000, () => {
  console.log('Server is running on port 3000!');
})
複製代碼

最後請求返回內容。

jsonp({
  url: 'http://localhost:3000/jsonp?from=1',
  callback: 'getData',
})

function getData(res) {
  console.log(res); // {title: "hello,jsonp!"}
}
複製代碼
  • jquery方法
<script src="https://unpkg.com/jquery@3.4.1/dist/jquery.js"></script>
$(function () {  
  $.ajax({
    url: 'http://localhost:3000/jsonp?from=1',
    type: 'get',
    dataType: 'jsonp',
    success: function(res) {
      console.log(res); // {title: "hello,jsonp!"}
    }
  })
})
複製代碼

CROS

CORS(Cross -Origin Resource Sharing),跨域資源共享,是一個W3C標準,在http的基礎上發佈的標準協議。

CORS須要遊覽器和服務器同時支持,解決了遊覽器的同源限制,使得跨域資源請求得以實現。它有兩種請求,一種是簡單請求,另一種是非簡單請求。

  • 簡單請求

知足如下兩個條件就屬於簡單請求,反之非簡單。

1)請求方式是GETPOSTHEAD; 2)響應頭信息是AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(只限於application/x-www-form-urlencodedmultipart/form-datatext/plain);

簡單請求有三個CORS字段須要加在響應頭中,前面部分都是以Access-Control開頭:

1.Access-Control-Allow-Origin,這個表示接受哪些域名的請求,若是是*號,那就是任何域名均可以請求; 2.Access-Control-Allow-Credentials,這個表示是否容許攜帶cookie,默認是false,不容許攜帶;

若是設置爲true, 要發送cookie,容許域就必須指定域名方法;客戶端http請求必須設置:

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
複製代碼

3.Access-Control-Expose-Headers,這個表示服務器可接受的響應頭字段,若是客戶端發送過來請求攜帶的Cache-ControlContent-LanguageContent-TypeExpiresLast-Modified,還有自定義請求頭字段。

例如:請求一個本地get接口。

本地服務端開啓訪問限制,打開server.js,輸入如下內容:設置容許域名爲http://localhost:8089,容許訪問請求方式是POST請求。

const http = require('http');
const url = require('url');
const queryString = require('querystring');
const data = JSON.stringify({
  title: 'hello,jsonp!'
})
const dataCors = JSON.stringify({
  title: 'hello,cors!'
})

const server = http.createServer((request, response) => {
  let addr = url.parse(request.url);
  response.setHeader("Access-Control-Allow-Origin", 'http://localhost:8089');
  response.setHeader("Access-Control-Allow-Headers", "Content-Type");
  response.setHeader("Access-Control-Allow-Methods","POST");
  response.setHeader("Content-Type", "application/json;charset=utf-8");
  if (addr.pathname == '/jsonp') {
    let cb = queryString.parse(addr.query).callback;
    response.writeHead(200, { 'Content-Type': 'application/json;charset=utf-8' })
    response.write(cb + '('+ data +')');
  } else if (addr.pathname == '/test'){
    if (request.method == 'POST') {
      response.writeHead(200, { 'Content-Type': 'application/json;charset=utf-8' })
      response.write(dataCors);
    } else {
      response.writeHead(404, { 'Content-Type': 'text/plain;charset=utf-8' })
    }
  } else {
    response.writeHead(403, { 'Content-Type': 'text/plain;charset=utf-8' })
    response.write('403');
  }
  response.end();
})

server.listen(3000, () => {
  console.log('Server is running on port 3000!');
})
複製代碼

express框架設置以下:

app.all("*", function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-control-Allow-Headers", "X-Auth");
  res.header("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS,HEAD,FETCH");
  res.header("Access-control-max-age", 60*60*24); //測試經過
  next();
})
複製代碼

若是使用get訪問http://localhost:3000/test這個接口地址,那麼就會報錯404。

httpReq這個方法在這篇文章查看。

async function getReqData() {  
  let data = await httpReq({
    type: 'get',
    url: 'http://localhost:3000/test',
    data: null,
  })
  console.log(data);
}
getReqData();
複製代碼

cors_get.png
cors_get_req

若是是post訪問,就會返回正常內容。

async function getReqData() {  
  let data = await httpReq({
    type: 'post',
    url: 'http://localhost:3000/test',
    data: null,
  })
  console.log(data);
}
getReqData();
複製代碼

post
post

  • 非簡單請求

就是除了簡單請求的幾種方法外,好比說PUT請求、DELETE請求,這種都是要發一個預檢請求的,而後服務器容許,纔會發送真正的請求。

非簡單請求有如下幾個字段須要傳遞:

1.Access-Control-Allow-Methods,值是以逗號分隔,好比:GET,POST,DELETE; 2.Access-Control-Allow-Headers,值是默認字段或者自定義字段,例如:X-Auth-Info; 3.Access-Control-Allow-Credentials,是否攜帶cookie信息; 4.Access-Control-Max-Age,表明預檢請求的有效期限,單位是秒。

例如:如下這個put請求,服務端設置一個put請求接口,這裏使用axios來請求。

// 設置返回信息
const dataUpdate = JSON.stringify({
  title: 'update success!'
})
// 設置容許
response.setHeader("Access-Control-Allow-Methods","POST,PUT");
response.setHeader("Access-Control-Allow-Credentials",false);
response.setHeader("Access-Control-Max-Age", 60*60*24);
if (addr.pathname == '/update'){
  if (request.method == 'PUT') {
    response.writeHead(200, { 'Content-Type': 'application/json;charset=utf-8' })
    response.write(dataUpdate);
  } else {
    response.writeHead(404, { 'Content-Type': 'text/plain;charset=utf-8' })
  }
}
複製代碼

客戶端請求,返回內容。

<script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
async function saveInfo () {
  let data = await axios.put('http://localhost:3000/update', {
    title: 'far',
    body: 'bar',
    userId: 121,
  })
  console.log(data);
}
saveInfo();
複製代碼

put
put

domain設置

這種方法只適用於子域不一樣時候的跨域請求,可使用document.domain來設置。

好比:map.domaintest.org子域名指向根域名domaintest.org,可使用下面的設置。

if (document.domain != 'domaintest.org') {
  document.domain = 'domaintest.org';
}
複製代碼

例如:

async function saveInfo () {
  let data = await httpReq({
    type: 'get',
    url: 'http://map.domaintest.org:8089/ky.html',
    data: null,
  })
  console.log(data);
}
saveInfo();
if (document.domain != 'domaintest.org') {
  document.domain = 'domaintest.org';
}
複製代碼

domain

domain

看狀況,使用谷歌請求,不加這段也能夠成功請求到子域的頁面內容。

post Message

這個post Message能夠安全的實現跨源通訊,適用於父頁面和子頁面或是兩個不一樣頁面之間的請求,iframe的狀況就適合用這個。

父頁面經過postMessage('<msg>','<url>'),子頁面接收消息,而且返回消息到父頁面,父頁面監聽message事件接收消息。

例如:http://map.domaintest.org:8089/parent.html發送消息給子頁面http://map.domaintest.org:8089/son.html,子頁面返回消息。

  • 父頁面的:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>父級頁面</title>
</head>
<body>
  <button id="btn">發送消息</button>
  <iframe id="child" src="http://map.domaintest.org:8089/son.html" width="100%" height="300"></iframe>
  <script> let sendBtn = document.querySelector('#btn'); sendBtn.addEventListener('click', sendMsg, false); function sendMsg () { window.frames[0].postMessage('getMsg', 'http://map.domaintest.org:8089/son.html'); } window.addEventListener('message', function (e) { let data = e.data; console.log('接收到的消息是:'+ data); }) </script>
</body>
</html>
複製代碼

parent

  • 子頁面的:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>子頁面</title>
</head>
<body>
  <h2>窗口</h2>
  <p>我是另一個窗口!</p>
  <script> window.addEventListener('message', function (e) { if (e.source != window.parent) return; window.parent.postMessage('我是來自子頁面的消息!', '*'); }, false) </script>
</body>
</html>
複製代碼

son

跨域代理方法補充

有時候可使用代理的方式來發送跨域請求,好比axios的跨域代理,nodejs跨域代理,nginx跨域代理。下面介紹常見的幾種跨域代理設置方法。

  • axios的跨域代理

建立實例創建代理:

import axios from 'axios';
var server = axios.create({
  baseURL: 'https://domain.com/api/',
  timeout: 1000,
});
複製代碼
  • vue中跨域設置

config/index.js中設置:

module.exports = {
  dev: {
    //...
    proxyTable: {
      '/api': {
          target: 'http://10.0.100.7:8081', //設置調用接口域名和端口號別忘了加http
          changeOrigin: true,
          pathRewrite:{
            "^/api":""
          }
      }
    },
  }
複製代碼

接口調用時:

this.axios.get('/api/user',{
    params: {
        userId:'1'
    }
}).then(function (res) {
    console.log(res);
}).catch(function (err) {
    console.log(err);
})
複製代碼
  • nodejs跨域代理

按照代理代理包

npm i -S http-proxy
複製代碼

設置跨域

var http = require('http');
var url=require('url');
var path=require('path');
var httpProxy = require('http-proxy');

//服務端口
var PORT = 8080;
//接口前綴
var API_URL='api';
//真正的api地址
var API_DOMAIN='http://www.example.com/';

//建立一個代理服務器
var proxy = httpProxy.createProxyServer({
    target: API_DOMAIN,
});

//代理出錯則返回500
proxy.on('error', function(err, req, res){
    res.writeHead(500, {
      'content-type': 'text/plain'
    });
    res.end('server is error!');
});

//創建一個本地的server
var server = http.createServer(function (request, response) {
    var pathname = url.parse(request.url).pathname;
    var realPath = path.join("./", pathname);
    var ext = path.extname(realPath);
    ext = ext ? ext.slice(1) : 'unknown';

    //判斷若是是接口訪問,則經過proxy轉發
    console.log(pathname);
    console.log(API_URL);
    if(pathname.indexOf(API_URL) > 0){
        console.log(request.url.substring(4,request.url.length));
        request.url=request.url.substring(4,request.url.length)
        proxy.web(request, response);
        return;
    }
});
server.listen(PORT);
console.log("Server runing at port: " + PORT + ".");
複製代碼
  • nginx跨域代理

設置cors

location / {
  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
    add_header 'Access-Control-Max-Age' 64800;
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    add_header 'Content-Length' 0;
    return 204;
  }
  if ($request_method = 'POST') {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  }
  if ($request_method = 'GET') {
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  }
}
複製代碼

或者是反向代理

server {
  listen       80; #監聽80端口
  server_name  localhost; # 當前服務的域名
  location / {
    proxy_pass http://localhost:81;
    proxy_redirect default;
  }

  location /apis { #添加訪問目錄爲/apis的代理配置
    rewrite  ^/apis/(.*)$ /$1 break;
    proxy_pass   http://localhost:82;
  }
}
複製代碼

寫在最後

跨域的方法就介紹這麼多,還有什麼不懂的能夠給我留言,個人github,獲取更多歡迎訪問個人我的博客

相關文章
相關標籤/搜索