完全弄懂跨域問題

跨域,老生常談的問題

簡述

做爲一隻前端菜鳥,跨域方面只懂得JSONP和CORS,並不曾深刻了解。但隨着春招愈來愈近,就算是菜鳥也要猛振翅膀。近幾日仔細研究了跨域問題,寫下這篇文章,但願對開發者們有所幫助。在讀本文前,但願您對如下知識略有了解。javascript

  • 瀏覽器同源策略
  • nodejs
  • iframe
  • docker, nginx

咱們爲什麼要研究跨域問題

由於瀏覽器的同源策略規定某域下的客戶端在沒明確受權的狀況下,不能讀寫另外一個域的資源。而在實際開發中,先後端經常是相互分離的,而且先後端的項目部署也經常不在一個服務器內或者在一個服務器的不一樣端口下。前端想要獲取後端的數據,就必須發起請求,若是不作一些處理,就會受到瀏覽器同源策略的約束。後端能夠收到請求並返回數據,可是前端沒法收到數據。php

爲什麼瀏覽器會制定同源策略

之因此有同源策略,其中一個重要緣由就是對cookie的保護。cookie 中存着sessionID 。黑客一旦獲取了sessionID,而且在有效期內,就能夠登陸。當咱們訪問了一個惡意網站 若是沒有同源策略 那麼這個網站就能經過js 訪問document.cookie 獲得用戶關於的各個網站的sessionID 其中可能有銀行網站 等等。經過已經創建好的session鏈接進行攻擊,好比CSRF攻擊。
這裏須要服務端配合再舉個例子,如今我扮演壞人 我經過一個iframe 加載某寶的登陸頁面 等傻傻的用戶登陸個人網站的時候 我就把這個頁面彈出 用戶一看 阿里唉大公司 確定安全 就屁顛屁顛的輸入了密碼 注意 若是沒有同源策略 我這個惡意網站就能經過dom操做獲取到用戶輸入的值 從而控制該帳戶因此同源策略是絕對必要的.
還有須要注意的是同源策略沒法徹底防護CSRF。html

多種跨域方法

跨域能夠大概分爲兩種目的前端

  • 先後端分離時,前端爲了獲取後端數據而跨域
  • 爲不一樣域下的前端頁面通訊而跨域

爲先後端分離而跨域

Cross Origin Resource Share (CORS)

CORS是一個跨域資源共享方案,爲了解決跨域問題,經過增長一系列請求頭和響應頭,規範安全地進行跨站數據傳輸java

請求頭主要包括

請求頭 解釋
Origin Origin頭在跨域請求或預先請求中,標明發起跨域請求的源域名。
Access-Control-Request-Method Access-Control-Request-Method頭用於代表跨域請求使用的實際HTTP方法
Access-Control-Request-Headers Access-Control-Request-Headers用於在預先請求時,告知服務器要發起的跨域請求中會攜帶的請求頭信息
with-credentials 跨域請求攜帶cookie

響應頭主要包括

響應頭 解釋
Access-Control-Allow-Origin Access-Control-Allow-Origin頭中攜帶了服務器端驗證後的容許的跨域請求域名,能夠是一個具體的域名或是一個*(表示任意域名)。
Access-Control-Expose-Headers Access-Control-Expose-Headers頭用於容許返回給跨域請求的響應頭列表,在列表中的響應頭的內容,才能夠被瀏覽器訪問。
Access-Control-Max-Age Access-Control-Max-Age用於告知瀏覽器能夠將預先檢查請求返回結果緩存的時間,在緩存有效期內,瀏覽器會使用緩存的預先檢查結果判斷是否發送跨域請求。
Access-Control-Allow-Methods Access-Control-Allow-Methods用於告知瀏覽器能夠在實際發送跨域請求時,能夠支持的請求方法,能夠是一個具體的方法列表或是一個*(表示任意方法)。

如何使用

  • 客戶端只需按規範設置請求頭。
  • 服務端按規範識別並返回對應響應頭,或者安裝相應插件,修改相應框架配置文件等。具體視服務端所用的語言和框架而定

SpringBoot 設置CORS例子

一個spring boot項目中關於CORS配置的一段代碼node

HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String temp = request.getHeader("Origin");
        httpServletResponse.setHeader("Access-Control-Allow-Origin", temp);
        // 容許的訪問方法
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH");
//         Access-Control-Max-Age 用於 CORS 相關配置的緩存
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        httpServletResponse.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept,token");
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");

JSONP 跨域

jsonp的原理就是藉助HTML中的<script>標籤能夠跨域引入資源。因此動態建立一個<srcipt>標籤,src爲目的接口 + get數據包 + 處理數據的函數名。後臺收到GET請求後解析並返回函數名(數據)給前端,前端<script>標籤動態執行處理函數
觀察下面代碼linux

  • 前端代碼nginx

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <script>
        var script = document.createElement('script');
        script.type = 'text/javascript';
    
        // 傳參並指定回調執行函數爲getData
        script.src = 'http://localhost:8080/users?username=xbc&callback=handleData';
        document.body.appendChild(script);
        // 回調執行函數
        function handleData(res) {
            data = JSON.stringify(res)
            console.log(data);
        }
    </script>
    </body>
    </html>
  • 後端代碼(nodejs)web

    var querystring = require('querystring');
    var http = require('http');
    var server = http.createServer();
    
    server.on('request', function(req, res) {
        var params = querystring.parse(req.url.split('?')[1]);
        var fn = params.callback;
    
        // jsonp返回設置
        res.writeHead(200, { 'Content-Type': 'text/javascript' });
        var data = {
            user: 'xbc',
            password: '123456'
        }
        res.write(fn + '(' + JSON.stringify(data) + ')');
    
        res.end();
    });
    
    server.listen('8080');
    console.log('Server is running at port 8080...');

在該例子中,前臺收到的res是這樣的
圖片描述spring

前端頁面是這樣的
圖片描述

注意

JSONP既是利用了<srcipt>,那麼就只能支持GET請求。其餘請求沒法實現

nginx 反向代理實現跨域

思路

既然瀏覽器有同源策略限制,那咱們把前端項目和前端要請求的api接口地址放在同源下不就能夠了?再結合web服務器提供的反向代理,即可以在前端和後端都不作配置的狀況下解決跨域問題。

以nginx爲例

  • 後端真實後臺地址:http://xxx.xxx.xxx.xxx:8085 後臺地址使用tomcat部署的spring boot項目 名爲gsms_test
  • nginx服務器地址: http://xxx.xxx.xxx.xxx:8082
  • tomcat和nginx都是用docker架設的,作了端口轉發
  • 使用條件:開發環境爲linux系統
  • nginx /etc/nginx/conf.d/default.conf配置代碼以下

    server {
        listen       80;
        server_name  localhost;
    
        #charset koi8-r;
        #access_log  /var/log/nginx/host.access.log  main;
    
        location / {
            # root   /usr/share/nginx/html/dist; # 前端項目路徑
            # index  index.html index.htm;
            proxy_pass http://localhost:8001/; # 前端本機地址,實現自動更新
            autoindex on;
            autoindex_exact_size on;
            autoindex_localtime on;
        }
    
        location /gsms_test/ {
            proxy_pass 後端真實地址;
        }
    
        
    
        #error_page  404              /404.html;
    
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }
    
        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}
    
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}
    
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }

不一樣域下頁面通訊而跨域

window.name + iframe 跨域

window.name是瀏覽器中一個窗口所共享的數據,在不一樣的頁面(甚至不一樣域名)加載後依舊存在(若是沒修改則值不會變化),而且能夠支持很是長的 name 值(2MB)。好比 a域的某頁面想獲取b域某頁面的數據,能夠在b域中修改window.name值,a域切換到b域再切回來便可獲得b域的window.name值。但是咱們在開發中確定不想頁面切來切去,因此就要結合iframe來實現。

示例 (以thinkjs實現)

  • a 域代碼以下

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>A 域</title>
    </head>
    <body>
    <h1>server A</h1>
    <script type="text/javascript">
        function getData() {
            var iframe = document.getElementById('proxy');
            iframe.onload = function () {
                var name = iframe.contentWindow.name; // 獲取iframe窗口裏的window.name值
                console.log(name)
            }
            // 因爲iframe信息傳遞也受同源策略限制,因此在window.name被B域修改後,將iframe轉回A域下。以便獲取iframe的window.name值
            iframe.src = 'http://127.0.0.1:8360/sub.html' 
        }
    </script>
    <iframe id="proxy" src="http://127.0.0.1:8361/index.html" style="width: 100%" onload="getData()">        </iframe>
    </body>
    </html>
  • b 域代碼

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>New ThinkJS Application</title>
    </head>
    <body>
      <h1>server 2</h1>
    <script type="text/javascript">
      window.name = 'user: xbc';
    </script>
    </body>
    </html>

注意

因爲受同源策略限制,父頁面獲取跨域的iframe頁面的信息不全,因此要在iframe的window.name被B域修改後,轉爲A域下的任一頁面(該一面不得修改window.name),在進行獲取。

代理頁面 + iframe 實現跨域訪問

因爲iframe與父頁面相互訪問也受同源策略限制,因此要藉助一代理頁面實現跨域。

圖片來源於https://www.jianshu.com/p/9d90d3333215

我的認爲有些麻煩,如有興趣請看前端如何用代理頁面解決iframe跨域訪問的問題?

總結

以上幾種皆是本人用過或測試過的跨域方法,還有postMessage,WebSocket等跨域方法因爲從未接觸不作說明。在項目中具體使用那些方法還需具體考慮各類問題

狀況 方法
只有GET請求 JSONP
對兼容性及瀏覽器版本無要求 CORS
對兼容性及瀏覽器版本有要求 iframe 或 服務器反向代理(linux 環境下開發)

本文參考

謝謝

本文若有錯誤,歡迎指出本人郵箱 xbc18304999858@gmail.com

相關文章
相關標籤/搜索