前端跨域問題解決方案(基於node與nginx)

sanseo發表於2019-01-09

1.跨域是什麼

  跨域是指去向一個為非本origin(協議、域名、埠任意一個不同)的目標地址傳送請求的過程,這樣之所以會產生問題是因為瀏覽器的同源策略限制。看起來同源策略影響了我們開發的順暢性.實則不然,同源策略存在的必要性之一是為了隔離攻擊。

同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。

  我們就拿同源策略隔離的主要攻擊之一CSRF為例講述下同源策略存在的必要性

CSRF

  CSRF,又稱跨站請求偽造,指非法網站挾持使用者cookie在已登陸網站上實施非法操作的攻擊,這是基於使用cookie在網站免登和使用者資訊留存的實用性,接下來來講講正常網站免登的請求流程。 請求流程如下:

  1. 我們進入一個網站,傳送登陸請求給後端
  2. 後端接受登陸請求,判斷登陸資訊是否準確
  3. 判斷資訊準確後後端後會傳送response給瀏覽器並在response header中加入set-cookie欄位
  4. 瀏覽器接受response返給使用者,並將header中的cookie進行儲存
  5. 使用者關閉當前網站視窗後再次開啟後,瀏覽器會自動將cookie加入request header實現免登

我們設想這樣一個場景

  1. 小a登陸了網銀網站,小a所在瀏覽器記錄了網銀回饋的cookie
  2. 這時他qq上收到個連結,什麼澳門賭場,美女荷官,線上送錢的網站b
  3. 他點開那個連結之後,網站b就可以攜帶瀏覽器設定的cookie向網銀系統上傳送請求

  結果不言而喻,輕則資訊洩漏,重則錢財損失,而且cookie正常的儲存時間是直到關閉瀏覽器為止,而不是關閉網站,所以很多使用者會以為關閉網站了再去開啟澳門的網站就安全了emmmm。

  在一些安全性要求高的網站,同源策略還是有存在的必要的,需要跨域實現的請求也最好設定限制,比如設定指定的白名單origin。

2.跨域問題的解決方案

1.jsonp

  最早的解決方案之一就是jsonp,實現方式是通過script標籤傳遞資料,因為script請求不會被同源策略禁止,所以通過script標籤去請求跨域資料,並且在script的cb對應func中實現對資料的獲取是可行的,當然這種方式需要後端進行配合,後端在前端進行對應請求的時候返回對應的jsonp格式的資料 php案例如下:

<?php
header('Content-type: application/json');
//獲取回撥函式名
$jsoncallback = htmlspecialchars($_REQUEST ['jsoncallback']);
//json資料
$json_data = '["customername1","customername2"]';
//輸出jsonp格式的資料
echo $jsoncallback . "(" . $json_data . ")";
?>
複製程式碼

  客戶端用法如下:

 <script type="text/javascript">
		function callbackFunction(result, methodName)
        {
            ///result 指向對應資料
        }
</script>
<script type="text/javascript" src="http://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction"></script>
複製程式碼

2.CORS

  接下來講到的就是我們的主角CORS,那麼CORS是什麼呢?

跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓執行在一個 origin (domain) 上的Web應用被准許訪問來自不同源伺服器上的指定的資源。當一個資源從與該資源本身所在的伺服器不同的域、協議或埠請求一個資源時,資源會發起一個跨域 HTTP 請求

  CORS又分為簡單請求預檢請求

簡單請求

mdn定義的簡單請求就是某些不會觸發cors預檢的請求。

這裡簡單描述下簡單請求的重點,也就是在實際開發過程中會碰到的主要情況

  1. 設定不會觸發預檢的Methods : GETHEADPOST。 GET和POST大家都很熟悉,不再贅述,解釋下HEAD請求,HEAD就是隻傳送請求不會收到響應的一種請求方式,日常用的比較少

  2. 簡單請求只可以設定如下header如下AcceptAccept-LanguageContent-LanguageContent-Type

  3. Content-Type標頭允許的值只能是: application/x-www-form-urlencoded、 multipart/form-data、 text/plain

後端適配方案: 在respones header中新增Access-Control-Allow-Origin

'Access-Control-Allow-Origin''xxx'
複製程式碼

  Access-Control-Allow-Origin代表允許傳送請求的源,引數可以是固定的白名單ip或者萬用字元,可以用萬用字元"*",代表接受所有請求。不過有種特殊情況是不能使用萬用字元的,就是前端請求header中含有withCredentials,withCredentials:true是跨域請求想要攜帶cookie必須加入的headers配置

預檢請求

  預檢請求就是在跨域的時候設定了對應的需要預檢的內容,結果上會在普通跨域請求前新增了個options請求,用來檢查前端headers的修改是否在後端允許範圍內。 觸發預檢請求在跨域開發中會碰到的主要情況如下

  1. 首先methods設定 PUTDELETECONNECTOPTIONSTRACE會導致預檢請求
  2. 設定了AcceptAccept-LanguageContent-LanguageContent-Type 之外的headers中任一的配置,比如常見的token:authorization,快取機制cache-contorl
  3. Content-Type設定了簡單請求不允許的值,如常用的application/json

那麼預檢請求我們需要如何處理呢?

預檢請求就需要後端設定更多的respones headers了,常用如下:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
複製程式碼
  • Access-Control-Allow-Methods代表可接受methods
  • Access-Control-Allow-Headers代表可接受的headers修改
  • Access-Control-Max-Age代表預檢的殘留時間,代表預檢之後可以免預檢的時間

除此之外,後端還需要設定對options請求的判斷,我在node中介軟體中新增的判斷如下:

if (req.method == 'OPTIONS') {
    res.send(200);
  } else {
    next();
  }
複製程式碼

  更多詳細cors內容可見:developer.mozilla.org/en-US/docs/…

實現CORS的幾種方式

  1. 本地代理
  2. nodejs中介軟體
  3. nginx代理

本地代理

  在dva中的實現方式是在.webpackrc中新增如下程式碼

 "proxy": {
    "/api": {
      "target": "http://127.0.0.1:8988/",
      "changeOrigin": true,
      "pathRewrite": { "^/api" : "" }
    }
  }
複製程式碼

  /api代表代理的路徑名,target代表代理的地址,changeOrigin代表更改發出源地址為target,pathRewrite代表路徑重寫,別的腳手架直接載入webpack配置檔案即可

nodejs跨域中介軟體

  具體實現過程我是使用express+http-proxy-middleware

  1. 用express腳手架生成express模具
npm install express-generator -g
express --view=pug myapp
複製程式碼
  1. 設定一個全域性路由攔截
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
if (req.method == 'OPTIONS') {
  res.send(200);
} else {
  next();
}
})
複製程式碼
  1. 再設定對應的代理邏輯
var options = {
  target: 'https://xxxx.xxx.xxx/abc/req',
  changeOrigin: true,
  pathRewrite: (path,req)=>{
    return path.replace('/api','/')
  }
}
app.use('/api', proxy(options));
複製程式碼
  1. 進入bin/www中設定對應的埠,或者在process.env.PORT設定port啟動值
var port = normalizePort(process.env.PORT || '7002');
複製程式碼
  1. 啟動腳手架
DEBUG=myapp:* npm start
複製程式碼

啟動代理後就可以直接在對應的專案中請求中介軟體實現跨域了

Nginx跨域代理

  Nginx是國外大神實現用的用於反向代理的非同步web伺服器 他除了用於反向代理以外還可以用於負載均衡、HTTP快取 接下來來介紹安裝方式 首先安裝Nginx要安裝Homebrew, 然而我的mac並沒有Homebrew,那麼還需要先用Ruby安裝一下這個包管理器

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
複製程式碼

  檢視下brew是否安裝成功,成功後可以順利的安裝Nginx啦

brew -v
brew install nginx
nginx -v
複製程式碼

  Nginx的常用命令有這些

#檢視版本,以及配置檔案地址
nginx -V
#檢視版本 
nginx -v
#指定配置檔案
nginx -c filename
#幫助
nginx -h
#重新載入配置|重啟|停止|退出 nginx
nginx -s reload|reopen|stop|quit
#開啟 nginx
sudo nginx
#測試配置是否有語法錯誤
sudo nginx -t

複製程式碼

  然後就要進入Nginx配置檔案

sudo vim /usr/local/etc/nginx/nginx.conf
複製程式碼

  ps:nginx-http常見配置項如下

http {
    #匯入型別配置檔案
    include       mime.types;
    #設定預設型別為二進位制流
    default_type  application/octet-stream;
    #啟用sendfile()函式
    sendfile        on;
    #客戶端與伺服器連線的超時時間為65秒,超過65秒,伺服器關閉連線
    keepalive_timeout  65;
    #是否開啟gzip,預設關閉
    #gzip  on;
    #一個server塊
    server {
        #伺服器監聽的埠為80
        listen       80;
        #伺服器名稱為localhost,我們可以通過localhost來訪問這個server塊的服務
        server_name  localhost;
        #location塊,它存放在server塊當中,location會嘗試根據使用者請求中的URI來匹配上面的/uri表示式,如果可以匹配,就選擇location {}塊中的配置來處理使用者請求。
        location / {
            #以root方式設定資源路徑,它與alias的不同請見下面的 http模組中檔案路徑定義
            root   html;
            #預設訪問的頁面,從左依次找到右,直到找到這個檔案,然後返回結束請求
            index  index.html index.htm;
            #設定錯誤頁面,對應的錯誤碼是404,錯誤頁面是/Users/user/Sites/404.html
            error_page 404  /404.html;
        }
    }
    include servers/*;
}
複製程式碼

  接下來就是重要的實現反向代理的方式了 這裡介紹的主要展示方式是如何線上上設定代理,所以代理的入口是靜態資源

server {
        listen       80;
	server_name  localhost;
	location / {
            root   /Users/abc/dist/;
            index  index.html index.htm;
        }

        location /api/ {
                proxy_pass  https://xxx.xxx.xxx/req/;
        }
}
複製程式碼

  location中的後的內容會嘗試根據使用者請求中的URI來匹配上面的/uri表示式,如果可以匹配,就選擇location {}塊中的配置來處理使用者請求

  本專案中第一個location用於指向靜態資源位置 root:目錄,index:入口檔案,第二個location用於進行api的跨域指向

  如果你想要對不同的埠實現代理,可以設定多個server listen同一個埠,根據server_name判斷請求來源,根據location 設定代理去向

  看到這裡,如果你沒碰到許可權問題,那麼恭喜你,如果碰到了,往下看tips3

  nginx踩坑:

  1. nginx在每次修改conf後都需要重啟,而且需要sudo
  2. nginx -s reload 有的時候會莫名丟失PID,網上很多的方案推薦 nginx -c來解決,實際上reload的時候通常nginx服務還是在跑著的,此時根本無法nginx -c,就會無限的 "address already in use" 此時需要的是殺掉對應的程式。。。。
ps -ef | grep nginx 查詢程式號
sudo kill -QUIT 主程式號 殺掉主程式號
sudo nginx 即可
複製程式碼
  1. nginx許可權問題 許可權問題最最最最最坑,但是你又說不出什麼,因為合情合理,他最主要的顯示方式是你的靜態資源網站會一直顯示為403,通常出現於dist目錄與nginx目錄分開的情況下,偶爾會出現於你修改了conf後-t的時候 此時需要設定許可權 sudo chown -R 'username' /usr/xxx/xxx/run/nginx.pid 坑的是,這個只能解決你 -t的時候缺乏許可權的問題 403還需要在nginx.conf增加配置 user your_username staff; 設定nginx的username才能解決403許可權的問題。

擴充話題 nginx還可以用於實現多入口

server {
        listen       80;
	server_name  www.aaa.com;
        location /api/ {
                proxy_pass  http://localhost:7001;
        }
}
server {
        listen       80;
	server_name  www.bbb.com;
        location /api/ {
                proxy_pass  http://localhost:7002;
        }
}
複製程式碼

  nginx還有實現負載均衡的功能,這裡就不詳細展開了

3.解決方案的對比

對比jsonp和cors,兩者優劣如下

  1. json只支援get請求,無法支援複雜的請求
  2. jsonp出現錯誤的時候,很難去進行錯誤識別與處理,cors可以正常錯誤捕捉
  3. jsonp的相容性比較高,而cors在舊版ie中需要尋找對應的替代方案

cors相容性如下:

undefined
pc與移動端主要瀏覽器完美支援,問題就是在ie低版本中需要找尋降級方案

  至於CORS的三種代理方案優缺點主要如下:

  1. 設定webpack-dev-server的proxy最簡單,但是通常只能用於本地環境,線上環境通常無法直接代理
  2. node代理書寫比較方便靈活,而且不需要過多的學習成本,前端了解一定的後端知識然後寫好異常捕捉就可以上手,還可以進行一些業務方面的處理,比如對接收請求進行攔截
  3. nginx實現代理的方案最敏捷,效能相較於node高出不少,nginx本身就以使用少資源,高併發,高效率的處理靜態資源和反向代理而聞名,nginx也更加穩定,在處理請求的同時cpu記憶體使用率低,非同步處理的流程能更好的抗住壓力,同一個程式能面對萬級的連線,nginx的效能解釋可以閱覽如下: www.mamicode.com/info-detail…

  總結的來說,其實node和nginx都可以實現代理功能,兩者都是非同步非阻塞模型都可以很好的支援前端的高頻率請求,node可以實現對請求的攔截與二次控制,而nginx在實現靜態資源代理、反向代理這幾個方面更勝一籌,而且nginx還具有高穩定性,高併發支援而佔優,兩者可以在一定程度上結合,node攔截+nginx反代也是一種思路

思索

  其實nginx並不算前端的知識內容,node才是一個可以無縫銜接的前端代理中介軟體,但是nginx他本身的優良與和前端請求的緊密結合其實都可以作為前端的知識體系的擴充。

參考資料:

  1. 同源策略 developer.mozilla.org/zh-CN/docs/…

  2. cors developer.mozilla.org/en-US/docs/…

  3. express中文網 www.expressjs.com.cn/

  4. nginx官網 nginx.org/en/docs/

  5. nginx效能解釋 www.mamicode.com/info-detail…

相關文章