對於切圖仔而言,跨域是個很是熟悉的名詞了。雖然瀏覽器爲了咱們的網站安全操碎了心,可是每每咱們爲了網站可以被用戶正常訪問,不得不繞過這個限制,cors
就是其中一種經常使用的解決跨域的方案。javascript
經過設置Access-Control-Allow-Credentials: true
和xhr.withCredentials = true
,能夠實現跨域傳遞Cookie
,達到保存用戶登陸態等目的。這種方案雖好,可是若是使用不當,會有CSRF的風險。因此,從Chrome 51
開始,瀏覽器的Cookie
新增長了一個SameSite
屬性,用來防止CSRF
攻擊和用戶追蹤。css
該特性當前默認是關閉的,可是有部分用戶已經提早受到了影響。若是你出現了沒法使用某些網站的第三方登陸功能的時候,請檢查是否是受了該設置的影響。html
SameSite
驗證雖然官方聲明,從Chrome 79
開始,該功能就會默認開啓(以前宣稱是從Chrome 80
開始,最新的聲明已經改了),可是經測試發現,部分用戶在默認狀況下該功能仍然是關閉的,因此咱們先禁用SameSite
驗證,看看狀況會怎樣。前端
打開Chrome
設置,將chrome://flags/#same-site-by-default-cookies
先禁用,而後重啓瀏覽器。java
使用本文最後面的示例代碼
,在本地模擬登陸操做跨域獲取Cookie
,而後攜帶Cookie
獲取用戶信息。nginx
能夠發現,咱們可以正常使用服務端寫入的Cookie
來發送請求並獲取用戶信息,可是會在Console
中看到一條警告信息。程序員
按照程序員一向的「警告便可忽略」的原則,咱們彷佛能夠不用管這個特性。可是一旦Chrome
瀏覽器全面開啓SameSite
特性,且用戶升級了瀏覽器,那麼基於Cookie
跨域登陸的網站都將沒法登陸。接下來咱們模擬下這個過程。chrome
SameSite
驗證一樣打開Chrome
設置,將chrome://flags/#same-site-by-default-cookies
啓用,而後重啓瀏覽器。express
清空Cookie
並從新登陸。注意:Cookie
是在後端域名下,不要清除前端域名對應的Cookie
。json
這時咱們能夠發現:請求的Response Cookies
下,SameSite
屬性有了一個提示信息,告訴咱們SameSite
屬性沒有設置,將使用默認值Lax
。
此時再去獲取用戶信息,將沒法成功獲取,由於Cookie
沒有跟隨請求一塊兒帶給後端服務。通過檢查能夠發現,該Cookie
沒有成功寫入用戶瀏覽器。
因而可知,若是不想2020年背故障,那麼如今就要開始提早處理這個問題了。
SameSite
驗證SameSite
屬性的默認值Lax
只容許get
請求攜帶Cookie
,這顯然無法知足,因此咱們將SameSite
屬性的值改成None
,同時將secure
屬性設置爲true
。這也意味着你的後端服務域名必需使用https
協議訪問。
// 注意:cookie 模塊必須要更新到最新的版本(0.4.0),才支持 sameSite=none
res.cookie('token', 'token 123', { maxAge: 2592000000, httpOnly: true, sameSite: 'none', secure: true, });
複製代碼
再次嘗試可發現,問題解決。
不過,這只是一種權宜之計,由於設置sameSite
爲None
以後,CSRF
的風險又回來了。因此,換成token
的檢驗方式而不依賴Cookie
,或許纔是更合理的解決方案。
如下是完整的示例代碼及Nginx
配置。
app.js
var path = require('path');
var cors = require('cors');
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express();
app.use(cors({ origin: true, credentials: true, }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); // index.html放在public目錄下
app.get('/api/info', function login(req, res) {
let token = req.cookies['token'];
if (token) {
res.json({ success: true, data: { name: 'somebody', age: 21 } });
} else {
res.json({ success: false, message: '請先登陸' });
}
});
app.get('/api/login', function login(req, res) {
res.cookie('token', 'token 123', { maxAge: 2592000000, httpOnly: true, });
res.end();
});
app.get('/api/login/security', function login(req, res) {
// 注意:cookie 模塊必須要更新到最新的版本(0.4.0),才支持 sameSite=none
res.cookie('token', 'token 123', { maxAge: 2592000000, httpOnly: true, sameSite: 'none', secure: true, });
res.end();
});
app.listen(8888, function () {
console.log('http://localhost:8888');
});
複製代碼
index.html
<html>
<head>
<title>Demo</title>
<style> button { width: 80px; height: 32px; line-height: 32px; text-align: center; } </style>
</head>
<body>
<div>
<p>
<button id="login">登陸</button>
<button id="security">安全登陸</button>
</p>
<p>
<button id="check">查詢</button>
</p>
</div>
<script> (function() { var get = function get(url, callback) { var xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open('get', url); xhr.onreadystatechange = function onreadystatechange() { if (xhr.readyState === 4) { var res = xhr.response; try { res = JSON.parse(res); } catch (e) {} typeof callback === 'function' && callback(res); } }; xhr.send(null); }; var login = document.querySelector('#login'); var check = document.querySelector('#check'); var security = document.querySelector('#security'); login.addEventListener('click', function onLogin() { get('https://api.server.cn/login'); }); check.addEventListener('click', function onLogin() { get('https://api.server.cn/info', function callback(res) { if (res.success) { console.log(res.data); } else { alert(res.message); } }); }); security.addEventListener('click', function onLogin() { get('https://api.server.cn/login/security'); }); })(); </script>
</body>
</html>
複製代碼
nginx.conf
server {
listen 443 ssl;
server_name api.server.cn;
ssl_certificate /path/to/ssl/server.crt;
ssl_certificate_key /path/to/ssl/server.key;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:8888/api/;
}
}
複製代碼
hosts
127.0.0.1 api.server.cn
複製代碼
注意
由於咱們的證書是自簽名的,並不能真正經過瀏覽器的證書檢驗,因此須要先手動點擊「繼續前往xxx(不安全)」,才能正常向後端服務發送請求。