原文出自:https://www.pandashen.com
前言
由於瀏覽器無狀態的特性,cookie
技術應運而生,cookie
是一個會話級的存儲,大小 4KB
左右,用於瀏覽器將服務器設置的信息重新帶給服務器進行驗證,不支持跨域,在瀏覽器清空緩存或超過有效期後失效,不能存放敏感信息,session
是專門用於存儲最初設置給瀏覽器 cookie
數據的地方,我們本篇就來討論一下 cookie
和 session
在 NodeJS 中的使用方式。
cookie 的基本使用
1、NodeJS 原生操作 cookie
下面是 cookie
在 Node 原生中的讀取和寫入方法。
// 原生中操作 cookie
const http = require("http");
// 創建服務
http.createServer((req, res) => {
if (req.url === "/read") {
// 讀取 cookie
console.log(req.headers.cookie);
res.end(req.headers.cookie);
} else if (req.url === "/write") {
// 設置 cookie
res.setHeader("Set-Cookie", [
"name=panda; domain=panda.com; path=/write; httpOnly=true",
`age=28; Expires=${new Date(Date.now() + 1000 * 10).toGMTString()}`,
`address=${encodeURIComponent("回龍觀")}; max-age=10`
]);
res.end("Write ok");
} else {
res.end("Not Found");
}
}).listen(3000);
上面代碼創建了一個 http
服務器,可以通過讀取 cookie
請求頭的值來獲取瀏覽器發來的 cookie
,服務器可以通過給瀏覽器設置響應頭 Set-Cookie
實現對瀏覽器 cookie
的設置,多個 cookie
參數爲數組,在數組內可以規定每一條 cookie
的規則,中間使用一個分號和一個空格隔開。
-
domain
用來設置允許訪問cookie
的域; -
path
用來設置允許訪問cookie
的路徑; -
httpOnly
用來設置是否允許瀏覽器中修改cookie
,如果通過瀏覽器修改設置過httpOnly=true
的cookie
,則會增加一條同名cookie
,原來的cookie
不會被修改; -
Expires
用來設置過期時間,絕對時間,值爲一個GMT
或UTC
格式的時間; -
max-age
同樣用來設置過期時間,相對時間,值爲一個正整數,單位s
。
cookie
默認不支持存儲中文,如果存儲中文需先使用 encodeURIComponent
方法進行轉譯,將轉譯後的結果存入 cookie
,在瀏覽器獲取 cookie
需使用 decodeURIComponent
方法轉回中文。
2、Koa 中操作 cookie
Koa
是當下流行的 NodeJS 框架,是對原生 Node 的一個輕量的封裝,但是內部實現了快捷操作 cookie
的方法,下面是原生中對 cookie
的操作在 Koa
中的寫法。
// Koa 中操作 cookie
const Koa = require("koa");
const Router = require("koa-router");
// 創建服務和路由
const app = new Koa();
const router = new Router();
// 簽名需要設置 key
app.keys = ["shen"];
router.get("/read", (ctx, next) => {
// 獲取 cookie
let name = ctx.cookies.get(name) || "No name";
let name = ctx.cookies.get(age) || "No age";
ctx.body = `${name}-${age}`;
});
router.get("/write", (ctx, next) => {
// 設置 cookie
ctx.cookies.set("name", "panda", { domain: "panda.com" });
ctx.cookies.set("age", 28, { maxAge: 10 * 1000, signed: true });
});
// 使用路由
app.use(router.routes());
app.listen(3000);
在 Koa
中將獲取和設置 cookie
的方法都掛在了 ctx
上下文對象的 cookies
屬性上,分別爲 get
和 set
。
cookies.get
的參數爲獲取 cookie
的鍵名,返回值爲鍵對應的值,cookies.set
的第一個參數同樣爲 cookie
的鍵名,第二個參數爲鍵對應的值,第三個參數爲一個對象,用來配置該條 cookie
的規則,如 domain
、path
和過期時間等,這裏 maxAge
值爲毫秒數。
注意:Koa
中設置的 cookie
默認不允許瀏覽器端通過 document.cookie
獲取,但是服務器也可以被欺騙,比如使用 postman
發送一個帶 Cookie
請求頭的請求,服務器可以通過設置簽名來預防,即添加 signed
選項並將值設置爲 true
。
3、Koa 操作 cookie 方法的原理
cookies
對象都是掛在 ctx
上來實現的,使用過 Koa
都知道如果要操作 ctx
就會用到中間件的思想,我們這就看看這兩個方法使用原生封裝的過程。
// Koa 中 ctx.cookies 對象 get 和 set 方法的原理
const Koa = require("koa");
const querystring = require("querystring");
const app = new Koa();
app.use(async (ctx, next) => {
// 獲取 cookie
const get = key => {
let cookies = ctx.get("cookie") || "";
return querystring.parse(result, "; ")[key];
};
// 設置 cookie,存儲所有的 cookie,等於 setHeader 中的第二個參數
let cookies = [];
const set = (key, val, options = {}) => {
// 用於構造單條 cookie 和權限等設置的數組,默認存放這條 cookie 的鍵和值
let single = [`${key}=${encodeURIComponent(val)}`];
// 下面是配置
if (options.domain) {
arr.push(`domain=${options.domain}`);
}
if (options.maxAge) {
arr.push(`Max-Age=${options.maxAge}`);
}
if (options.path) {
arr.push(`path=${options.path}`);
}
if (options.httpOnly) {
arr.push(`HttpOnly=true`);
}
// 將配置組合到 single 中後轉爲字符串存入 cookies
cookies.push(single.join("; "));
// 設置給瀏覽器
ctx.set("Set-Cookie", cookies);
}
// 將獲取和設置 cookie 的方法掛在 cookies 對象上
ctx.cookies = { get, set };
await next();
});
在 get
方法內部獲取 cookie
請求頭的值並根據傳入的 key
獲取值,set
方法內,將傳入的鍵值和選項拼接成符合 cookie
的字符串,通過 Set-Cookie
響應頭設置給瀏覽器。
session 的基本使用
1、NodeJS 原生使用 session
正常 session
是存放在數據庫中的,我們這裏爲了方便就用一個名爲 session
的對象來代替。
// 原生中使用 session
const http = require("http");
const uuid = require('uuid/v1'); // 生成隨字符串
const querystring = require("querystring");
// 存放 session
const session = {};
// 創建服務
http.createServer((req, res) => {
if (req.url === "/user") {
// 取出 cookie 存儲的用戶 ID
let userId = querystring.parse(req.headers["cookie"], "; ")["study"];
if (userId) {
if (session[userId].studyCount === 0) res.end("您的學習次數已用完");
session[userId].studyCount--;
} else {
// 生成 userId
userId = uuid();
// 將用戶信息存入 session
session[userId] = { studyCount: 30 };
// 設置 cookie
req.setHeader("Set-Cookie", [`study=${userId}`]);
}
// 響應信息
res.end(`
您的用戶 ID 爲 ${userId},
剩餘學習次數爲:${session[userId].studyCount}
`);
} else {
res.end("Not Found");
}
}).listen(3000);
上面寫的案例是一個網校的場景,一個新用戶默認有 30
次學習機會,以後每次訪問服務器學習次數減 1
,如果 studyCount
值爲 0
,則提示學習次數用完,否則提示當前用戶的 ID
和剩餘學習次數,session
中存儲的是每一個用戶 ID
對應的剩餘學習次數,這樣就不會輕易的被修改學習剩餘次數,因爲服務器只認用戶 ID
,再通過 ID
去更改對應的剩餘次數(當然忽略了別人冒充這個 ID
的情況,只能減,不能加),這樣就不會因爲篡改 cookie
而篡改用戶存在 session
中的數據,除非連整個數據庫都拖走。
2、Koa 中使用 session
我們接下來使用 Koa
實現和上面一摸一樣的場景,在 Koa
的社區中提供了專門操作 session
的中間件 koa-session
,使用前需安裝。
// Koa 中使用 session
const Koa = require("koa");
const Router = require("koa-router");
const session = requier("koa-session");
const uuid = require("uuid/v1");
// 創建服務和路由
const app = new Koa();
const router = new Router();
// cookie 的簽名
app.keys = ["panda"];
// 使用 koa-session 中間件
app.use(session({
key: "shen",
maxAge: 10 * 1000
}, app));
router.get("/user", (ctx, next) => {
// 取出 cookie 存儲的用戶 ID
let userId = ctx.cookie("study");
if (ctx.session.userId) {
if (ctx.session[userId].studyCount === 0) res.end("您的學習次數已用完");
ctx.session[userId].studyCount--;
} else {
// 生成 userId
userId = uuid();
// 將用戶信息存入 session
ctx.session[userId] = { studyCount: 30 };
// 設置 cookie
ctx.cookies.set("study", userId);
}
// 響應信息
ctx.body = `
您的用戶 ID 爲 ${userId},
剩餘學習次數爲:${session[userId].studyCount}
`;
});
// 使用路由
app.use(router.routes());
app.listen(3000);
使用 Koa
的 koa-session
以後,不再需要我們創建 session
對象進行存儲,並且 cookie-session
中間件幫我們封裝了 API 可以直接操作 mongo
和 MySQL
數據庫,上面代碼中與用原生相比還增加了 cookie
和 session
的簽名和過期時間,比原生寫起來要方便很多。
總結
本篇內容更偏向於 cookie
和 session
在 NodeJS 中的使用,沒有過多的敘述理論性的內容,cookie
和 session
是相互依存的,也就是說共同使用的,現在已經有 JWT 的方案來替代,因爲相比較下有很多優點,但某些項目和特殊場景還在使用 cookie
和 session
,所以還是寫了這一篇,如果對 JWT 感興趣可以看 通過一個案例理解 JWT。