2020再談跨域

張京發表於2020-10-26

跨域這個話題已經談了很多年了,怎麼現在又要談這個問題?本來是可以不必再提了的,但是由於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,可能還不一定能完全解決問題,目前唯一能修改的變通之道是修改cookiepath,使它後面帶上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協議,所以無法設定securecookies,那麼怎麼辦呢?

我們只能動手改造我們的本地伺服器,使它也支援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.keyserver.crt檔案就是我們真正需要的兩個檔案,我們把它們引入系統,不同的系統有不同的引入方法,以下例子是react的方法,建立一個.env檔案:

SSL_CRT_FILE=server.crt
SSL_KEY_FILE=server.key
HTTPS=true

這時候再啟動你的工程,localhost就帶有https協議了。


跨域就是這麼麻煩,但不論如何麻煩,我們不辭麻煩,也一定要解決它!

相關文章