跨域這個話題已經談了很多年了,怎麼現在又要談這個問題?本來是可以不必再提了的,但是由於Chrome 86
版本以後又增加了很多限制,導致我們不得不再次提起。
CORS
對於前端開發來說,跨域通常有兩種方式,一種是在服務端修改nginx
配置,在response headers
裡新增CORS
設定,另一種是在本地架設代理。我們先談第一種。
原本在nginx
裡新增CORS
已經是一種常規操作,簡單到無以復加:
location /somewhere/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "$http_origin";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "sitessubid,Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since";
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
if ($request_method = POST) {
add_header Access-Control-Allow-Origin "$http_origin";
add_header Access-Control-Allow-Credentials "true";
}
}
然後我們只要在每個axios
或者fetch
請求裡新增withCredentials
就能自動把對應該伺服器的cookies
隨請求一併傳送了。
但問題在Chrome 86
版本以後,Chrome
強制給從服務端下發的每個cookie
都增加了一個預設的SameSite
屬性,並且強制把這個屬性設定為Lax
,從而導致我們的withCredentials
不起作用,只要是跨域的情況,cookies
連取都取不到,更不要提傳送了。在這裡我不想再詳述SameSite
的原理,感興趣的可以去這裡瞭解。
這就需要我們必須從服務端入手,修改nginx
下發cookies
時的SameSite
屬性。
SameSite和secure
修改SameSite
又分為兩種情況:如果你用的是低版本的nginx
,可能還不一定能完全解決問題,目前唯一能修改的變通之道是修改cookie
的path
,使它後面帶上SameSite
屬性,例如這樣:
proxy_cookie_path ~(.*) "$1; SameSite=none";
這裡實際修改的是cookie
裡的path
值,但由於附加了;
,所以瀏覽器會認為自;
以後的是新屬性,所以也會接受這個SameSite
設定。
但是僅此還不夠,Chrome
又要求如果SameSite
值為none
的話,則還必須設定secure
屬性,也就是要求所有下發cookie
的域名必須使用https
協議,所以最終修改的結果是:
proxy_cookie_path ~(.*) "$1; SameSite=none; secure";
但這種情況只能解決類似於cookie
是以path
結尾的情況,如果上游伺服器下發cookie
時不止有path
,並且在path
結尾指定了其它的SameSite
,那就無能為力了,因為這麼修改path
之後cookie
會變成:
path=/; SameSite=none; secure; SameSite=Lax
瀏覽器仍然以最後一個SameSite=Lax
為準。目前低版本nginx
對這個問題無解。
但如果你是nginx 1.19.3
及以上,新增了一個屬性,可以更好地解決這個問題:
proxy_cookie_flags one samesite=none;
新增secure
標誌的方法類似,參考官方文件,不贅述。
localhost的https
由上可知,我們可以通過修改nginx
的方法來改造跨域cookies
的傳送。但這時如果你又不想跨域了,還想用本地代理的方式來解決,又會遇到一個問題:因為我們上面在下發cookies
的時候,預設地給每個cookie
都帶上了secure
屬性,這就導致我們在使用本地代理的時候反而無法設定cookies
,因為我們本地的開發伺服器往往使用的是類似於http://localhost:8080
這樣的地址,它不是https
協議,所以無法設定secure
的cookies
,那麼怎麼辦呢?
我們只能動手改造我們的本地伺服器,使它也支援https
協議:
第一步,我們先生成根證書:
openssl genrsa -des3 -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem
這時候我們會得到兩個檔案,一個是rootCA.key
,一個是rootCA.pem
。
我們把pem
檔案匯入系統的鑰匙鏈,並給予它完全的信任,這樣以後再由它簽發的證書才能被系統認可,否則即使我們用它簽發了證書,瀏覽器一樣會拒絕。
第二步,生成localhost
證書:
我們先建立一個名為server.csr.cnf
的檔案:
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
[dn]
C=US
ST=RandomState
L=RandomCity
O=RandomOrganization
OU=RandomOrganizationUnit
emailAddress=hello@example.com
CN = localhost
然後執行以下命令生成server.key
檔案
openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key -config <( cat server.csr.cnf )
再建立一個v3.ext
檔案:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
然後執行以下命令生成server.crt
檔案:
openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 -extfile v3.ext
好了,到此為止,這個server.key
和server.crt
檔案就是我們真正需要的兩個檔案,我們把它們引入系統,不同的系統有不同的引入方法,以下例子是react
的方法,建立一個.env
檔案:
SSL_CRT_FILE=server.crt
SSL_KEY_FILE=server.key
HTTPS=true
這時候再啟動你的工程,localhost
就帶有https
協議了。
跨域就是這麼麻煩,但不論如何麻煩,我們不辭麻煩,也一定要解決它!