不少人都已經看過 Session 和 Cookie 相關的入門文章,卻只限於紙上談兵,不懂得實際運用,本文從最小項目入手,結合前端跨域、HTTP 等知識點,作一次深刻實踐javascript
在用戶訪問網站時,咱們常常須要記錄一些信息,好比html
這時候咱們能夠藉助 Cookie,如下來自 MDN 的官方解釋前端
HTTP Cookie(也叫Web Cookie或瀏覽器Cookie)是服務器發送到用戶瀏覽器並保存在本地的一小塊數據,它會在瀏覽器下次向同一服務器再發起請求時被攜帶併發送到服務器上。一般,它用於告知服務端兩個請求是否來自同一瀏覽器,如保持用戶的登陸狀態。Cookie使基於無狀態的HTTP協議記錄穩定的狀態信息成爲了可能。java
Session 中文意思名爲「會話」,是一種解決方案,表明客戶端和服務端的一次通訊過程,在這個過程當中若是客戶端須要記錄數據,服務端會暫時把數據掛載到 session 對象上,當請求結束響應時,將 session 中掛載的數據持久化到客戶端的 cookie上,清空 session,關閉會話web
Cookie 能夠看作一個信息容器,藉助瀏覽器的環境對服務端的數據進行持久化存儲,隨後每次都會在 HTTP 請求頭中攜帶併發送至服務端,這樣服務端就能夠辨識請求的來源數據庫
下面,咱們藉助 Koa 框架搭建後端服務,來走一遍具體流程,新建一個koa-demo
項目npm
mkdir koa-demo && cd koa-demo
npm init -y
cnpm i koa --save
touch app.js index.html
複製代碼
寫入如下代碼編程
// app.js
const Koa = require('koa');
const app = new Koa();
app.use((ctx) => {
ctx.body = 'hello, Koa';
});
const PORT = '8080';
app.listen(PORT, () => {
console.log(`server is running at http://localhost:${PORT}`);
});
複製代碼
運行並訪問localhost:8080
,就能夠看到訪問成功!json
cnpm i koa-router koa-body --save
複製代碼
// app.js
const Koa = require('koa');
const Router = require('koa-router'); // 實現Koa的路由機制
const koaBody = require('koa-body'); // 對請求體中的數據作格式化處理
const app = new Koa();
const router = new Router();
app.use(router.routes()).use(router.allowedMethods());
app.use(koaBody());
router.post('/login', (ctx) => {
const { usr } = ctx.request.body;
ctx.body = usr;
});
const PORT = '8080';
app.listen(PORT, () => {
console.log(`server is running at http://localhost:${PORT}`);
});
複製代碼
在index.html中添加如下代碼後端
<!-- index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<button id="btn">send request</button>
</body>
<script> const btn = document.getElementById('btn'); const data = { usr: 'b2d1', psd: '123' }; const request = () => { return fetch('http://localhost:8080/login', { body: JSON.stringify(data), method: 'POST', headers: { 'Content-Type': 'application/json; charset=UTF-8' } }); }; const sendRequest = async () => { const res = await request(); // 返回一個 ReadableStream 對象 return await res.text(); // 因爲後端返回一段文本數據,利用text()來獲取數據,相似的還有json(),blob() }; btn.addEventListener('click', async () => { const msg = await sendRequest(); console.log(msg); }); </script>
</html>
複製代碼
注意,咱們採用 Fetch API
替代了 XMLHttpRequest API
,Fetch 方法提供了一種簡單,合理的方式來跨網絡異步獲取資源。Fetch 還提供了單個邏輯位置來定義其餘 HTTP 相關概念,例如 CORS 和 HTTP 的擴展。
爲了最大程度上還原開發時的場景,咱們cnpm i serve --save
,它可使本地靜態文件成爲在瀏覽器端口上運行的靜態站點
img
標籤的
src
屬性,嵌入
script
腳本)
CORS(跨域資源共享)是一種網絡瀏覽器的技術規範,爲web服務器跨域訪問控制提供了安全的跨域數據傳輸。
根據控制檯的提示,咱們須要在服務器的響應頭中加入Access-Control-Allow-Origin:whiteList
,這個whiteList
能夠是*
或者http://localhost:5000
,咱們能夠藉助koa-cors
來快速設置CORS
cnpm i koa-cors
複製代碼
// app.js
const cors = require('koa-cors');
// ...
app.use(cors());
app.use(koaBody());
app.use(router.routes()).use(router.allowedMethods());
// ...
複製代碼
這裏 app.use
的順序十分重要,由於 Koa
自己結構簡單,核心代碼只有一兩百行,包括掛載 Request
和 Response
到 Context
上,Compose
實現中間件(Middleware
)依次調用,即洋蔥模型,每一個請求都會通過全部中間件的過濾
因此,咱們能夠利用豐富的中間件使自己短小精悍的 Koa 應用構建成爲大型的 Web 應用
話很少說,繼續點擊按鈕
哈哈,咱們成功拿到了數據,不過細心的咱們發現了,在 Network 面板卻發送了兩次/login
請求,這是怎麼回事呢?
咱們接下來看看,第一次是個 OPTIONS 請求,但是咱們發送的明明是 POST 請求
這仍是牽涉到了 CORS,在 CORS 模式下,當服務端接收到
非簡單請求時,會先發出」預檢」請求,也就是正常請求以前的 OPTIONS 請求。那麼什麼是 HTTP 簡單請求?
符合如下條件的就是簡單請求,反之就是非簡單請求
而咱們在fetch配置中,指定了Content-Type
,故會發起一次預檢請求,來請示服務端是否執行客戶端真正的請求。
headers: {
'Content-Type': 'application/json; charset=UTF-8'
}
複製代碼
而 koa-cors 已經爲咱們考慮周到,在背後作了這幾件事:
cnpm i koa-session --save
複製代碼
koa-session 是一個高度封裝了 Cookie 和 Session 操做的 NPM 包
const session = require('koa-session');
app.keys = ['some secret hurr']; // 做爲cookies簽名時的祕鑰
const CONFIG = {
key: 'koa:sess', // cookie的鍵名
maxAge: 86400000, // 過時時間,這裏爲一天的期限
overwrite: true, // 是否覆蓋cookie
httpOnly: true, // 是否JS沒法獲取cookie
signed: true, // 是否生成cookie的簽名,防止瀏覽器暴力篡改
encode: (json) => JSON.stringify(json), // 自定義cookie編碼函數
decode: (str) => JSON.parse(str) // 自定義cookie解碼函數
};
// 再次強調,app.use(fn)的順序很重要
app.use(session(CONFIG, app));
app.use(cors());
app.use(koaBody());
app.use(router.routes()).use(router.allowedMethods());
...
複製代碼
接下來,改造一下登陸接口
// app.js
router.post('/login', (ctx) => {
const { usr } = ctx.request.body;
const logged = ctx.session.usr || false;
if (!logged) {
ctx.session.usr = usr;
ctx.body = 'welcome, you are first login';
} else {
ctx.body = `hi, ${ctx.session.usr}, you haved logined`;
}
});
複製代碼
咱們滿懷期待的點下按鈕,成功啦!
點第2次,點第3次,點N+1次,革命還沒有成功,cookie根本沒有設置成功 咱們在查閱Fetch的MDN文檔發現默認狀況下,fetch 不會從服務端發送或接收任何 cookies, 若是站點依賴於用戶 session,則會致使未經認證的請求(要發送 cookies,必須設置 credentials 選項)。
真相大白,咱們須要手動設置credentials
屬性的值爲include
,才能在當前域名內自動發送 cookie,回到 index.html,修改request
函數
return fetch('http://localhost:8080/login', {
credentials: 'include',
...
});
複製代碼
來,再次點擊按鈕,出現以下錯誤
根據控制檯的報錯信息,咱們需在服務端響應體中設置'Access-Control-Allow-Credentials':'true'
的 Header,而 koa-cors 已經內置了相關 API,只需修改一下
app.use(cors({ credentials: true }));
最終咱們點擊按鈕,第一次首次登陸,沒有問題
在這個過程當中,服務端會在POST請求響應體設置Set-Cookie
的 Header, 多是由於跨域的緣由,我在 Chrome 的請求響應體裏死活找不到,用了 Firefox 就能夠看到了
這時候瀏覽器就知道要把數據寫入 Cookie 中
繼續點擊,服務器已經記住了咱們登陸狀態
查看後續的請求報文,發現每次都會帶上 Cookie,以標識請求身份 能夠在 Application 面板查看 Cookies,能夠看到已經寫入的信息 koa:sess.sig 是 koa-session 對該 Cookie 的簽名,是對 Cookie 原文進行加密生成的一段字符串,它爲了防止 Cookie 在瀏覽器端被暴力修改,假設咱們強制修改了 Cookie 的過時時間,服務端會對修改後的 Cookie 生成新的簽名,發現與以前的簽名不一致,則會清除 Cookie就自動登陸而言,大體流程以下圖所示
這裏咱們順便介紹一下 Cookie 的經常使用屬性,加深對 Cookie 的理解,咱們能夠在 koa-cors 的CONFIG
中快速配置
Cookie的名稱
Cookie的值,常爲一段通過JSON.stringify()
處理後的字符串
分別指 Cookie 的一個特定的過時時間和有效期
可是事實並非如此,我在 Chrome 和 Firefox 中嘗試會話 Cookie,先修改 koa-session 的maxAge
屬性爲session
const CONFIG = {
maxAge: 'session',
}
複製代碼
點擊按鈕,能夠看到 Expires
屬性被設置爲N/A
指定了哪些主機域名能夠訪問 Cookie,Request Body
中的Host
字段表明了主機域名,若是設置 Domain = .b2d1.top
,那麼 m.b2d1.top、b2d1.top
也包含在 Cookie 的訪問範圍內,實現多網站共享 Cookie
指定了主機域名下的哪些路徑能夠訪問Cookie,如設置/docs
,則如下http://Domain/docs、http://Domain/docs/web/、http://Domain/docs/web/HTTP
路徑均可訪問 Cookie,其餘路徑獲取不到 Cookie
如設置爲true,則不能經過document.cookie
來訪問此 Cookie
Cookie 的大小
如設置爲true
,則只應經過被 HTTPS 協議加密過的請求發送給服務端 - 當咱們在http協議中,試圖接受設置 Secure 爲 true 的 Cookie 時,服務端會報錯, Error: Cannot send secure cookie over unencrypted connection
至此,咱們的 koa-demo 已經實現了最基本的登陸接口,並藉助 Seesion 和 Cookie存儲用戶登陸狀態的功能,可謂小而美。
通過上述的 demo 演示,其實核心就是一句話
Session 是一種服務端接受會話信息的解決方案,Cookie 是客戶端實現的一個信息容器
那麼,咱們是否能夠把信息存儲到其餘地方,答案是固然能夠,理論上,能夠存儲到任何媒介(Cookie,數據庫,系統文件)。出於安全考慮,咱們能夠在 Cookie 中保存 session 的 externalKey,將信息主體保存到數據庫中,經過 externalKey 來映射數據庫中的信息主體。
externalKey 事實上是 session 數據的索引,此時相比於直接把 session 存在 cookie 來講多了一層,cookie 裏面存的不是 session 而是找到 session 的鑰匙。固然咱們保存的時候就要作兩個工做,一是將 session 存入數據庫,另外一個是將 session 對應的 key 即(externalKey)寫入到 cookie
koa-session 爲咱們提供了 store 接口並提供三個方法:get、set、destroy,來實現自定義的存儲機制
// app.js
let store = {
storage: {},
get(key, maxAge) {
return this.storage[key];
},
set(key, sess, maxAge) {
this.storage[key] = sess;
},
destroy(key) {
delete this.storage[key];
}
};
const CONFIG = {
...,
store
};
router.post('/login', (ctx) => {
...
console.log(store.storage);
});
複製代碼
清除Cookie,從新發起請求,能夠看到此時,Cookie 的 Value 爲一段隨機生成的 Key
再次點擊按鈕,查看 Node服務的打印記錄,咱們已經將信息主體存儲在咱們實現的 store 中,經過 Cookie 的 Key 來獲取數據,安全性大大提升本文涉及的知識點較多,建議本身手把手敲出 koa-demo,針對 koa-session,還有不少值得探討的地方
我會專門寫一篇 koa-session 源碼解析的文章,提升對 Koa 框架的理解,JS 編程思想的提升,如何從底層處理 Session 和 Cookie
好比 Cookie 的處理,你們能夠先睹爲快