天天看點

前端跨域問題(CORS)前端跨域問題(CORS)

前端跨域問題(CORS)

文章目錄

  • 前端跨域問題(CORS)
    • 跨域定義
    • 怎麼才算跨域?
    • 為什麼要有跨域限制?
    • 什麼是預請求?
    • 什麼時候會有預請求?
    • 跨域流程圖
    • 跨域解決方案
    • 跨域實踐
    • 參考文章

主要是總結浏覽器

CORS

跨域處理方式,其他處理方式的認為是

僞跨域處理方式

,如

iframe

window.name

window.postMessage

跨域定義

跨站 HTTP 請求(Cross-site HTTP request)是指發起請求的資源所在域不同于該請求所指向資源所在的域的 HTTP 請求。

跨站 HTTP正常請求,但是結果被浏覽器攔截了,就是

跨域問題

跨域問題

隻有在浏覽器才會出現,javascript等腳本的主動http請求才會出現

跨域問題

。後端擷取http資料不會存在跨域問題。

跨域問題

可以說是浏覽器獨有的(或者說http用戶端獨有的,這個其實看制定者是否遵循協定)。

注意:有些浏覽器不允許從HTTPS的域跨域通路HTTP,比如Chrome和Firefox,這些浏覽器在請求還未發出的時候就會攔截請求,這是一個特例。

怎麼才算跨域?

那要先了解何為

同源(同域)

如果兩個頁面的協定,端口(如果有指定)和域名都相同,則兩個頁面具有相同的源。

不同源就是跨域。

下表給出了相對

http://store.company.com/dir/page.html

跨域檢測的示例:

連結 跨域與否 原因
http://store.company.com/dir2/other.html
http://store.company.com/dir/inner/another.html
https://store.company.com/secure.html 不同協定 ( https和http )
http://store.company.com:81/dir/etc.html 不同端口 ( 81和80)
http://news.company.com/dir/other.html 不同域名 ( news和store )

跨域情況總結如下:

  • 不同協定 (

    https

    http

    )
  • 不同端口 (81和80)
  • 不同域名 (

    news

    store

    )

IE會有點不一樣,更加寬松,但是前端相容是短柄**,是以不用理會IE。**

為什麼要有跨域限制?

同源政策限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行互動。這是一個用于隔離潛在惡意檔案的重要安全機制。

什麼是預請求?

預請求就是使用OPTIONS方法。跨域請求首先需要發送預請求,即使用OPTIONS方法發起一個預請求到伺服器,以獲知伺服器是否允許該實際請求。預請求的使用,可以避免跨域請求對伺服器的使用者資料産生未預期的影響。

跨域才會有預請求,但是不是所有跨域請求都會發送預請求的。 預請求伺服器正常傳回,浏覽器還要判斷是否合法,才會繼續正常請求的。是以web服務程式需要針對options做處理,要不然OPTIONS的請求也會運作後端代碼。一般預請求最好傳回204(NO-Content)。在谷歌開發者工具上檢視網絡請求時,如果預請求是不在

XHR

這個分類中,可以在``Other

分類或者

ALL`中檢視。

什麼時候會有預請求?

一般伺服器預設允許GET、POST、HEAD請求(前提跨域),是以這些請求,隻要前端腳本不追加請求頭,是不會有預請求發出的。這些請求叫簡單請求。

可以簡單總結為隻有GET、POST、HEAD才可能沒有預請求。

大多數浏覽器不支援針對于預請求的重定向。如果一個預請求發生了重定向,浏覽器将報告錯誤:

The request was redirected to 'https://example.com/foo', which is disallowed for cross-origin requests that require preflight
           

簡單舉個

javascript

代碼例子:

有預請求:

fetch('http://localhost:3000/demo-01', {
    method: 'get',
    headers: {
        //這裡定義了請求頭就一定會有預請求。
        'Cache-Control': 'no-cache',
        Pragma: 'no-cache'
    },
})
           

無預請求:

fetch('http://localhost:3000/demo-01', {
	method: 'get'
})
           

跨域流程圖

流程圖不會把代理伺服器考慮進去,而且不考慮異常狀态碼。

前端跨域問題(CORS)前端跨域問題(CORS)

跨域解決方案

正如大家所知,出于安全考慮,浏覽器會限制腳本中發起的跨站請求。但是為了能開發出更強大、更豐富、更安全的Web應用程式,開發人員渴望着在不丢失安全的前提下,Web 應用技術能越來越強大、越來越豐富。

Web 應用工作組(Web Applications Working Group)推薦了一種的機制,即跨源資源共享(

Cross-Origin Resource Sharing (CORS)

)。

跨域資源共享标準新增了一組 HTTP 首部字段,允許伺服器聲明哪些源站有權限通路哪些資源。

跨域需要設定的HTTP首部字段實作前後端跨域請求,需要設定下面相關的HTTP響應頭:

字段名 必須設定與否
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Max-Age

一般隻要設定好

Access-Control-Allow-Origin

就可以跨域了,其他的字段都是配合使用的(其他字段有預設值)。

  • Access-Control-Allow-Origin

跨域允許就是這個字段設定的,預設不設定時不允許跨域。

Access-Control-Allow-Origin: <origin> | *
           

origin

參數指定一個允許向該伺服器送出請求的URI.對于一個不帶有

credentials

(即

cookie

)的請求,可以指定為’*’,表示允許來自所有域的請求。如果想要更安全點,當然也可以指定URI。

Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: http://foo.example
           
  • Access-Control-Allow-Credentials

此字段是用來設定是否允許傳cookie,預設為false。

Access-Control-Allow-Credentials: true | false
           
  • Access-Control-Allow-Methods

預設值一般為GET、HEAD、POST,是以請delete等方法的時候,預設會被限制。

Access-Control-Allow-Methods: <method>[, <method>]*
           

指明資源可以被請求的方式有哪些(一個或者多個)。這個響應頭資訊在用戶端發出預檢請求的時候會被傳回。設定為*時,沒有囊括全部方式,例如patch,所有還是設定為全部方式更保險。

Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Methods: *
Access-Control-Allow-Methods: GET
           
  • Access-Control-Allow-Headers

浏覽器自身附帶的請求頭預設是被允許的,但是前端代碼追加的請求頭,在跨域的時候是要被允許才可通路。

而且浏覽器本身預設自帶請求頭是不可修改的,如User-Agent、Origin等。

Access-Control-Allow-Headers: Pragma,Cache-Control
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: Pragma
           
  • Access-Control-Max-Age
Access-Control-Max-Age: <delta-seconds>
           

這個響應頭告訴我們這次預請求的結果的有效期是多久,有效期期間内的請求都不用使用預請求。

跨域實踐

請在谷歌通路和運作前端代碼,IE域名相同端口不同是不存在跨域問題的。

node express服務執行個體

const express = require('express');
const app = express();

//begin----禁止用戶端緩存的影響
app.disable('etag');
app.use(function(req, res, next) {
//浏覽器用戶端和代理伺服器都不可以緩存
res.header('Cache-Control', 'no-store');
//相容http1.0
res.header('Pragma', 'no-cache');
next();
});
//end----禁止用戶端緩存的影響

//all,預設不做請求類型限制,但是響應頭可以限制。
app.all('/demo-normal', function(req, res) {
res.header('Access-Control-Allow-Origin', '*');
res.send('Hello World!');
});

app.all('/demo-allow-method', function(req, res) {
res.header('Access-Control-Allow-Origin', '*');
//追加允許delete請求方式,預設的get等還是允許的
res.header('Access-Control-Allow-Methods', 'DELETE');
res.send('Hello World!');
});

app.all('/demo-credentials-cannot-work', function(req, res) {
res.header('Access-Control-Allow-Origin', '*');
//這樣設定是無效的,因為Access-Control-Allow-Origin為*時,設定允許使用
//允許請求附帶cookie,必須指定域名
//同時前端預設跨域請求是不傳cookie的,需要允許附帶cookie請求頭
res.header('Access-Control-Allow-Credentials', 'true');
res.send('Hello World!');
});
app.all('/demo-credentials-can-work', function(req, res) {
//擷取通路的域名,但是實際上不可以用這種通用的形式,需要指定授權域名。
//允許請求附帶cookie,必須指定域名
//同時前端預設跨域請求是不傳cookie的,需要允許附帶cookie請求頭
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.send('Hello World!');
});

app.all('/demo-max-age', function(req, res) {
res.header('Access-Control-Allow-Origin', '*');
//預請求有效期30秒
res.header('Access-Control-Max-Age', 30);
//delete請求會有預請求
res.header('Access-Control-Allow-Methods', 'DELETE');
if (req.method === 'OPTIONS') {
//預請求不要運作代碼,隻是給浏覽器傳回響應響應頭資訊。
//預請求應該傳回204,NO-Content(無内容)
res.status(204).send('');
return;
}
res.send('Hello World!');
});

app.listen(3000, () => console.log('Demo listening on port 3000!'));
           

前端執行個體

首先要啟動上面的node服務,運作下面的指令:

mkdir cors-node-server
cd cors-node-server
npm init #初始化npm項目
npm i express #安裝express
touch index.js #建立檔案,需要把上面的node express服務代碼複制到index.js
node ./index.js
#然後服務就啟動了
           

然後下面的代碼可以直接複制到浏覽器Console中運作,然後檢視網絡請求。不過涉及到cookie的,需要直通路

html

檔案,才可以附帶cookie。不要在目前的express中運作,否則當然不會有跨域問題。

請求一

fetch('http://localhost:3000/demo-normal', {
method: 'GET',
})
           

method為GET、POST、HEAD都通路正常,但是使用DELETE、PUT、PATCH等就會通路失敗。而且GET、POST、HEAD是沒有預請求的,但是DELETE、PUT、PATCH就是預請求失敗,谷歌console傳回下面這類資訊:

Failed to load http://localhost:3000/demo-normal: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
           

請求二

fetch('http://localhost:3000/demo-allow-method', {
method: 'DELETE'
})
           

通路method為delete是會有預請求,然後請求正常傳回。其他的跟請求一的例子響應一緻。

請求三(附帶cookie)

method: 'GET',
credentials: 'include'
})
           

通路失敗,傳回錯誤如下:

Failed to load http://localhost:3000/demo-credentials-cannot-work: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. Origin 'https://developer.mozilla.org' is therefore not allowed access.
           

意思就是

Access-Control-Allow-Credentials

設定為

true

時,

Access-Control-Allow-Origin

不能為*,需要指定具體通路的域名來源。

請求四(附帶cookie)

首先我們要在

localhost

目前域名下添加cookie。

fetch('http://localhost:3000/demo-credentials-can-work', {
method: 'GET',
credentials: 'include'
})
           

正常通路,然後在請求頭上,可以看到Cookie字段,當然前提是

localhost

這個域名有cookie。

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Cookie: test=123
Host: localhost:3000
Origin: https://developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
           

請求五(預請求有效期)

fetch('http://localhost:3000/demo-max-age', {
method: 'DELETE'
})
           

首先第一次請求會有預請求,然後30秒有效期内再次請求無預請求。然後這個有效期會不斷循環。

參考文章

  • 浏覽器的同源政策
  • HTTP通路控制
  • 前端跨域問題