Session 和 Cookie,可別光說不練!

前言

不少人都已經看過 Session 和 Cookie 相關的入門文章,卻只限於紙上談兵,不懂得實際運用,本文從最小項目入手,結合前端跨域、HTTP 等知識點,作一次深刻實踐javascript

業務場景

在用戶訪問網站時,咱們常常須要記錄一些信息,好比html

  • 會話狀態管理(如用戶登陸狀態、購物車信息)
  • 個性化設置(如用戶自定義設置、主題等)
  • 瀏覽器行爲跟蹤(如跟蹤分析用戶行爲,淘寶的商品推薦)
  • 用戶身份(如權限較高的頁面,普通用戶沒法訪問)

這時候咱們能夠藉助 Cookie,如下來自 MDN 的官方解釋前端

HTTP Cookie(也叫Web Cookie或瀏覽器Cookie)是服務器發送到用戶瀏覽器並保存在本地的一小塊數據,它會在瀏覽器下次向同一服務器再發起請求時被攜帶併發送到服務器上。一般,它用於告知服務端兩個請求是否來自同一瀏覽器,如保持用戶的登陸狀態。Cookie使基於無狀態的HTTP協議記錄穩定的狀態信息成爲了可能。java

Session 和 Cookie 的聯繫

Session 中文意思名爲「會話」,是一種解決方案,表明客戶端和服務端的一次通訊過程,在這個過程當中若是客戶端須要記錄數據,服務端會暫時把數據掛載到 session 對象上,當請求結束響應時,將 session 中掛載的數據持久化到客戶端的 cookie上,清空 session,關閉會話web

Cookie 能夠看作一個信息容器,藉助瀏覽器的環境對服務端的數據進行持久化存儲,隨後每次都會在 HTTP 請求頭中攜帶併發送至服務端,這樣服務端就能夠辨識請求的來源數據庫

建立Node服務

下面,咱們藉助 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 的擴展。

  • 語法簡潔,更加語義化
  • 基於標準 Promise 實現,支持 async/await
  • 原生提供,更加底層,提供的API豐富(request, response)

爲了最大程度上還原開發時的場景,咱們cnpm i serve --save,它可使本地靜態文件成爲在瀏覽器端口上運行的靜態站點

點擊按鈕,不出意外,咱們遭遇了瀏覽器的同源策略限制,因爲前端端口是5000,後端端口是8080,端口不一致,瀏覽器出於安全,禁止了跨域資源的讀取(跨域資源寫入是支持的,好比 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 自己結構簡單,核心代碼只有一兩百行,包括掛載 RequestResponseContext 上,Compose 實現中間件(Middleware依次調用,即洋蔥模型,每一個請求都會通過全部中間件的過濾

因此,咱們能夠利用豐富的中間件使自己短小精悍的 Koa 應用構建成爲大型的 Web 應用

話很少說,繼續點擊按鈕

哈哈,咱們成功拿到了數據,不過細心的咱們發現了,在 Network 面板卻發送了兩次 /login請求,這是怎麼回事呢?

咱們接下來看看,第一次是個 OPTIONS 請求,但是咱們發送的明明是 POST 請求

這仍是牽涉到了 CORS,在 CORS 模式下,當服務端接收到 非簡單請求時,會先發出」預檢」請求,也就是正常請求以前的 OPTIONS 請求。那麼什麼是 HTTP 簡單請求?

符合如下條件的就是簡單請求,反之就是非簡單請求

  • 請求方法是如下三種方法之一:
    • HEAD
    • GET
    • POST
  • HTTP的頭信息不超出如下幾種字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain

而咱們在fetch配置中,指定了Content-Type,故會發起一次預檢請求,來請示服務端是否執行客戶端真正的請求。

headers: {
    'Content-Type': 'application/json; charset=UTF-8'
}
複製代碼

而 koa-cors 已經爲咱們考慮周到,在背後作了這幾件事:

  • 在 OPTIONS 響應體和 POST 響應體中寫入如下 Header
  • 結束 OPTIONS 響應,並返回 204 No Content,並執行 POST 請求

使用 Session 和 Cookie

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中快速配置

Name

Cookie的名稱

Value

Cookie的值,常爲一段通過JSON.stringify()處理後的字符串

Expires / Max-Age

分別指 Cookie 的一個特定的過時時間和有效期

  • Expires 時間要轉成 GMT 形式
  • 當 Cookie 的過時時間被設定時,設定的日期和時間只與客戶端相關,而不是服務端
  • 全部支持 Max-Age 的瀏覽器會忽略 Expires 的值,只有 IE 另外,IE 會忽略 Max-Age 只支持 Expires
  • 當 Max-Age 設置爲負數,表示是會話級別的 Cookie,只存儲在瀏覽器內存裏,只要關閉瀏覽器,此 Cookie 就會消失
  • 當 Max-Age 設置爲 0,表示刪除此 Cookie
  • 當二者都不設置,則 Cookie 失去了持久化的特性,就成爲了會話 Cookie ,關閉瀏覽器,該 Cookie 就會清除

可是事實並非如此,我在 Chrome 和 Firefox 中嘗試會話 Cookie,先修改 koa-session 的maxAge屬性爲session

const CONFIG = {
    maxAge: 'session',
}
複製代碼

點擊按鈕,能夠看到 Expires 屬性被設置爲N/A

但我在關閉瀏覽器後, Cookie 依舊存在,緣由多是瀏覽器一種防止會話 Cookie 過時的安全機制

Domain

指定了哪些主機域名能夠訪問 Cookie,Request Body中的Host字段表明了主機域名,若是設置 Domain = .b2d1.top,那麼 m.b2d1.top、b2d1.top 也包含在 Cookie 的訪問範圍內,實現多網站共享 Cookie

Path

指定了主機域名下的哪些路徑能夠訪問Cookie,如設置/docs,則如下http://Domain/docs、http://Domain/docs/web/、http://Domain/docs/web/HTTP路徑均可訪問 Cookie,其餘路徑獲取不到 Cookie

HttpOnly

如設置爲true,則不能經過document.cookie來訪問此 Cookie

Size

Cookie 的大小

Secure

如設置爲true,則只應經過被 HTTPS 協議加密過的請求發送給服務端 - 當咱們在http協議中,試圖接受設置 Secure 爲 true 的 Cookie 時,服務端會報錯, Error: Cannot send secure cookie over unencrypted connection

至此,咱們的 koa-demo 已經實現了最基本的登陸接口,並藉助 Seesion 和 Cookie存儲用戶登陸狀態的功能,可謂小而美。

使用externalKey

通過上述的 demo 演示,其實核心就是一句話

Session 是一種服務端接受會話信息的解決方案,Cookie 是客戶端實現的一個信息容器

那麼,咱們是否能夠把信息存儲到其餘地方,答案是固然能夠,理論上,能夠存儲到任何媒介(Cookie,數據庫,系統文件)。出於安全考慮,咱們能夠在 Cookie 中保存 session 的 externalKey,將信息主體保存到數據庫中,經過 externalKey 來映射數據庫中的信息主體。

externalKey 事實上是 session 數據的索引,此時相比於直接把 session 存在 cookie 來講多了一層,cookie 裏面存的不是 session 而是找到 session 的鑰匙。固然咱們保存的時候就要作兩個工做,一是將 session 存入數據庫,另外一個是將 session 對應的 key 即(externalKey)寫入到 cookie

實現自定義 Store

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,還有不少值得探討的地方

  • session 是如何掛載到 Koa 上的,cookie 又是如何掛載到 session 上
  • cookie 的初始化、格式化
  • cookie 的簽名加密,校驗手段

我會專門寫一篇 koa-session 源碼解析的文章,提升對 Koa 框架的理解,JS 編程思想的提升,如何從底層處理 Session 和 Cookie

好比 Cookie 的處理,你們能夠先睹爲快

相關文章
相關標籤/搜索