Egg 的 Cookie 與 Session

2020-02-06 14:11 更新

Cookie

HTTP 請求都是無狀態(tài)的,但是我們的 Web 應(yīng)用通常都需要知道發(fā)起請求的人是誰。為了解決這個問題,HTTP 協(xié)議設(shè)計了一個特殊的請求頭:Cookie。服務(wù)端可以通過響應(yīng)頭(set-cookie)將少量數(shù)據(jù)響應(yīng)給客戶端,瀏覽器會遵循協(xié)議將數(shù)據(jù)保存,并在下次請求同一個服務(wù)的時候帶上(瀏覽器也會遵循協(xié)議,只在訪問符合 Cookie 指定規(guī)則的網(wǎng)站時帶上對應(yīng)的 Cookie 來保證安全性)。

通過 ctx.cookies,我們可以在 controller 中便捷、安全的設(shè)置和讀取 Cookie。

class HomeController extends Controller {
async add() {
const ctx = this.ctx;
let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
}
async remove() {
const ctx = this.ctx;
ctx.cookies.set('count', null);
ctx.status = 204;
}
}

ctx.cookies.set(key, value, options)

設(shè)置 Cookie 其實是通過在 HTTP 響應(yīng)中設(shè)置 set-cookie 頭完成的,每一個 set-cookie 都會讓瀏覽器在 Cookie 中存一個鍵值對。在設(shè)置 Cookie 值的同時,協(xié)議還支持許多參數(shù)來配置這個 Cookie 的傳輸、存儲和權(quán)限。

  • {Number} maxAge: 設(shè)置這個鍵值對在瀏覽器的最長保存時間。是一個從服務(wù)器當(dāng)前時刻開始的毫秒數(shù)。
  • {Date} expires: 設(shè)置這個鍵值對的失效時間,如果設(shè)置了 maxAge,expires 將會被覆蓋。如果 maxAge 和 expires 都沒設(shè)置,Cookie 將會在瀏覽器的會話失效(一般是關(guān)閉瀏覽器時)的時候失效。
  • {String} path: 設(shè)置鍵值對生效的 URL 路徑,默認設(shè)置在根路徑上(/),也就是當(dāng)前域名下的所有 URL 都可以訪問這個 Cookie。
  • {String} domain: 設(shè)置鍵值對生效的域名,默認沒有配置,可以配置成只在指定域名才能訪問。
  • {Boolean} httpOnly: 設(shè)置鍵值對是否可以被 js 訪問,默認為 true,不允許被 js 訪問。
  • {Boolean} secure: 設(shè)置鍵值對只在 HTTPS 連接上傳輸,框架會幫我們判斷當(dāng)前是否在 HTTPS 連接上自動設(shè)置 secure 的值。

除了這些屬性之外,框架另外擴展了 3 個參數(shù)的支持:

  • {Boolean} overwrite:設(shè)置 key 相同的鍵值對如何處理,如果設(shè)置為 true,則后設(shè)置的值會覆蓋前面設(shè)置的,否則將會發(fā)送兩個 set-cookie 響應(yīng)頭。
  • {Boolean} signed:設(shè)置是否對 Cookie 進行簽名,如果設(shè)置為 true,則設(shè)置鍵值對的時候會同時對這個鍵值對的值進行簽名,后面取的時候做校驗,可以防止前端對這個值進行篡改。默認為 true。
  • {Boolean} encrypt:設(shè)置是否對 Cookie 進行加密,如果設(shè)置為 true,則在發(fā)送 Cookie 前會對這個鍵值對的值進行加密,客戶端無法讀取到 Cookie 的明文值。默認為 false。

在設(shè)置 Cookie 時我們需要思考清楚這個 Cookie 的作用,它需要被瀏覽器保存多久?是否可以被 js 獲取到?是否可以被前端修改?

默認的配置下,Cookie 是加簽不加密的,瀏覽器可以看到明文,js 不能訪問,不能被客戶端(手工)篡改。

  • 如果想要 Cookie 在瀏覽器端可以被 js 訪問并修改:
ctx.cookies.set(key, value, {
httpOnly: false,
signed: false,
});
  • 如果想要 Cookie 在瀏覽器端不能被修改,不能看到明文:
ctx.cookies.set(key, value, {
httpOnly: true, // 默認就是 true
encrypt: true, // 加密傳輸
});

注意:

  1. 由于瀏覽器和其他客戶端實現(xiàn)的不確定性,為了保證 Cookie 可以寫入成功,建議 value 通過 base64 編碼或者其他形式 encode 之后再寫入。
  2. 由于瀏覽器對 Cookie 有長度限制限制,所以盡量不要設(shè)置太長的 Cookie。一般來說不要超過 4093 bytes。當(dāng)設(shè)置的 Cookie value 大于這個值時,框架會打印一條警告日志。

ctx.cookies.get(key, options)

由于 HTTP 請求中的 Cookie 是在一個 header 中傳輸過來的,通過框架提供的這個方法可以快速的從整段 Cookie 中獲取對應(yīng)的鍵值對的值。上面在設(shè)置 Cookie 的時候,我們可以設(shè)置 options.signed 和 options.encrypt 來對 Cookie 進行簽名或加密,因此對應(yīng)的在獲取 Cookie 的時候也要傳相匹配的選項。

  • 如果設(shè)置的時候指定為 signed,獲取時未指定,則不會在獲取時對取到的值做驗簽,導(dǎo)致可能被客戶端篡改。
  • 如果設(shè)置的時候指定為 encrypt,獲取時未指定,則無法獲取到真實的值,而是加密過后的密文。

如果要獲取前端或者其他系統(tǒng)設(shè)置的 cookie,需要指定參數(shù) signed 為 false,避免對它做驗簽導(dǎo)致獲取不到 cookie 的值。

ctx.cookies.get('frontend-cookie', {
signed: false,
});

Cookie 秘鑰

由于我們在 Cookie 中需要用到加解密和驗簽,所以需要配置一個秘鑰供加密使用。在 config/config.default.js 中

module.exports = {
keys: 'key1,key2',
};

keys 配置成一個字符串,可以按照逗號分隔配置多個 key。Cookie 在使用這個配置進行加解密時:

  • 加密和加簽時只會使用第一個秘鑰。
  • 解密和驗簽時會遍歷 keys 進行解密。

如果我們想要更新 Cookie 的秘鑰,但是又不希望之前設(shè)置到用戶瀏覽器上的 Cookie 失效,可以將新的秘鑰配置到 keys 最前面,等過一段時間之后再刪去不需要的秘鑰即可。

Session

Cookie 在 Web 應(yīng)用中經(jīng)常承擔(dān)標(biāo)識請求方身份的功能,所以 Web 應(yīng)用在 Cookie 的基礎(chǔ)上封裝了 Session 的概念,專門用做用戶身份識別。

框架內(nèi)置了 Session 插件,給我們提供了 ctx.session 來訪問或者修改當(dāng)前用戶 Session 。

class HomeController extends Controller {
async fetchPosts() {
const ctx = this.ctx;
// 獲取 Session 上的內(nèi)容
const userId = ctx.session.userId;
const posts = await ctx.service.post.fetch(userId);
// 修改 Session 的值
ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
ctx.body = {
success: true,
posts,
};
}
}

Session 的使用方法非常直觀,直接讀取它或者修改它就可以了,如果要刪除它,直接將它賦值為 null:

ctx.session = null;

需要 特別注意 的是:設(shè)置 session 屬性時需要避免以下幾種情況(會造成字段丟失,詳見 koa-session 源碼)

  • 不要以 _ 開頭
  • 不能為 isNew
// ? 錯誤的用法
ctx.session._visited = 1; // --> 該字段會在下一次請求時丟失
ctx.session.isNew = 'HeHe'; // --> 為內(nèi)部關(guān)鍵字, 不應(yīng)該去更改

// ?? 正確的用法
ctx.session.visited = 1; // --> 此處沒有問題

Session 的實現(xiàn)是基于 Cookie 的,默認配置下,用戶 Session 的內(nèi)容加密后直接存儲在 Cookie 中的一個字段中,用戶每次請求我們網(wǎng)站的時候都會帶上這個 Cookie,我們在服務(wù)端解密后使用。Session 的默認配置如下:

exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1 天
httpOnly: true,
encrypt: true,
};

可以看到這些參數(shù)除了 key 都是 Cookie 的參數(shù),key 代表了存儲 Session 的 Cookie 鍵值對的 key 是什么。在默認的配置下,存放 Session 的 Cookie 將會加密存儲、不可被前端 js 訪問,這樣可以保證用戶的 Session 是安全的。

擴展存儲

Session 默認存放在 Cookie 中,但是如果我們的 Session 對象過于龐大,就會帶來一些額外的問題:

  • 前面提到,瀏覽器通常都有限制最大的 Cookie 長度,當(dāng)設(shè)置的 Session 過大時,瀏覽器可能拒絕保存。
  • Cookie 在每次請求時都會帶上,當(dāng) Session 過大時,每次請求都要額外帶上龐大的 Cookie 信息。

框架提供了將 Session 存儲到除了 Cookie 之外的其他存儲的擴展方案,我們只需要設(shè)置 app.sessionStore 即可將 Session 存儲到指定的存儲中。

// app.js
module.exports = app => {
app.sessionStore = {
// support promise / async
async get (key) {
// return value;
},
async set (key, value, maxAge) {
// set key to store
},
async destroy (key) {
// destroy key
},
};
};

sessionStore 的實現(xiàn)我們也可以封裝到插件中,例如 egg-session-redis 就提供了將 Session 存儲到 redis 中的能力,在應(yīng)用層,我們只需要引入 egg-redis 和 egg-session-redis 插件即可。

// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
exports.sessionRedis = {
enable: true,
package: 'egg-session-redis',
};

注意:一旦選擇了將 Session 存入到外部存儲中,就意味著系統(tǒng)將強依賴于這個外部存儲,當(dāng)它掛了的時候,我們就完全無法使用 Session 相關(guān)的功能了。因此我們更推薦大家只將必要的信息存儲在 Session 中,保持 Session 的精簡并使用默認的 Cookie 存儲,用戶級別的緩存不要存儲在 Session 中。

Session 實踐

修改用戶 Session 失效時間

雖然在 Session 的配置中有一項是 maxAge,但是它只能全局設(shè)置 Session 的有效期,我們經(jīng)常可以在一些網(wǎng)站的登陸頁上看到有 記住我 的選項框,勾選之后可以讓登陸用戶的 Session 有效期更長。這種針對特定用戶的 Session 有效時間設(shè)置我們可以通過 ctx.session.maxAge= 來實現(xiàn)。

const ms = require('ms');
class UserController extends Controller {
async login() {
const ctx = this.ctx;
const { username, password, rememberMe } = ctx.request.body;
const user = await ctx.loginAndGetUser(username, password);

// 設(shè)置 Session
ctx.session.user = user;
// 如果用戶勾選了 `記住我`,設(shè)置 30 天的過期時間
if (rememberMe) ctx.session.maxAge = ms('30d');
}
}

延長用戶 Session 有效期

默認情況下,當(dāng)用戶請求沒有導(dǎo)致 Session 被修改時,框架都不會延長 Session 的有效期,但是在有些場景下,我們希望用戶如果長時間都在訪問我們的站點,則延長他們的 Session 有效期,不讓用戶退出登錄態(tài)??蚣芴峁┝艘粋€ renew 配置項用于實現(xiàn)此功能,它會在發(fā)現(xiàn)當(dāng)用戶 Session 的有效期僅剩下最大有效期一半的時候,重置 Session 的有效期。

// config/config.default.js
module.exports = {
session: {
renew: true,
},
};


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號