ããã¯ããªã«ãããããŠæžãããã®ïŒ
Expressã®ã»ãã·ã§ã³ç®¡çã®ä»çµã¿ã䜿ã£ãŠã¿ããããªããšæããŸããŠã
Expressãšã»ãã·ã§ã³
Expressã®ã»ãã·ã§ã³ã«é¢ãã話ã¯ã以äžã«å°ãåºãŠããŸãã
Production Best Practices: Security / Use cookies securely
Expressã«ã¯ã以äžã®2ã€ã®ã»ãã·ã§ã³ã¢ãžã¥ãŒã«ãããããã§ãã
- cookie-session ⊠GitHub - expressjs/cookie-session: Simple cookie-based session middleware
- express-session ⊠GitHub - expressjs/session: Simple session middleware for Express
cookie-sessionã¯Cookieã«ã»ãã·ã§ã³ããŒã¿ãä¿åããããã«ãŠã§ã¢ã§ãã
express-sessionã¯Cookieã«ã»ãã·ã§ã³IDã®ã¿ãä¿åããŠãã»ãã·ã§ã³ããŒã¿ã¯å¥ã®ã¹ãã¬ãŒãžã«ä¿åããããã«ãŠã§ã¢ã§ãã
cookie-sessionã¯Cookieã«ã»ãã·ã§ã³ããŒã¿ãå«ãããã以äžã®ãããªç¹åŸŽããããŸãã
- ä¿åããããŒã¿ã®åèšã¯ãCookieã®äžéã«çžããã
- 代ããã«ããµãŒããŒåŽã«ããŒã¿ããŒã¹çã¯å¿ èŠãªã
- è² è·åæ£ã容æã«ãªã
ã»ãã·ã§ã³ããŒã¿ãæ¯èŒçå°ãããããŒã¿ãããªããã£ããšããŠç°¡åã«ãšã³ã³ãŒãã§ããå Žåã«ã®ã¿äœ¿çšããããšãã
æ¹éã¿ããã§ãã
Only use it when session data is relatively small and easily encoded as primitive values (rather than objects).
ä»åã¯ãexpress-sessionã®æ¹ã䜿ã£ãŠã¿ãŸãã
ã¡ãªã¿ã«ããã®ã»ã¯ã·ã§ã³ã«æžãããŠããå
容ã¯ã»ãã·ã§ã³çšã®ããã«ãŠã§ã¢ã䜿ãéã®ã»ãã¥ãªãã£äžã®æ³šæäºé
ãªã®ã§ãå®éã«äœ¿ãæã«ã¯èŠãŠãããšããã§ãããã
Production Best Practices: Security / Use cookies securely
express-session
express-sessionã«ã€ããŠãããå°ãã
GitHub - expressjs/session: Simple session middleware for Express
ç¹°ãè¿ãã«ãªããŸãããexpress-sessionã¯Cookieã«ã»ãã·ã§ã³IDã®ã¿ãä¿åããŠãã»ãã·ã§ã³ããŒã¿ã¯
å¥ã®ã¹ãã¬ãŒãžã«ä¿åããããã«ãŠã§ã¢ã§ãã
ããã©ã«ãã®ã¹ãã¬ãŒãžã¯ã€ã³ã¡ã¢ãªã§ãããã¡ãã¯ã¡ã¢ãªãŒãªãŒã¯ãçºçããå¯èœæ§ãé«ããæ¬çªç°å¢ã§ã®
å©çšãæå³ããŠèšèšããããã®ã§ã¯ãªãããšã«æ³šæãå¿
èŠããã§ãã
Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.
ã¹ãã¬ãŒãžïŒã»ãã·ã§ã³ã¹ãã¢ïŒã¯ãä»ã®ãã®ã«å€æŽããããšãã§ããŸãããã®äžèŠ§ã¯ãã¡ãã
express-session / Compatible Session Stores
ä»åã¯ã€ã³ã¡ã¢ãªïŒããã©ã«ãïŒãšRedisãããããè©ŠããŠã¿ãããšæããŸãã
æåã«ã€ã³ã¡ã¢ãªã®æ¹ã§ç¢ºèªããŠããã¡ããããŒã¹ã«Redisãçµã¿èŸŒãæ¹åã§ãã£ãŠã¿ãããšæããŸãã
ç°å¢
ä»åã®ç°å¢ã¯ããã¡ãã
$ node --version v16.13.1 $ npm --version 8.1.2
Redisã¯ä»¥äžã®ããŒãžã§ã³ã䜿çšãã172.17.0.2ã§åäœããŠãããã®ãšããŸãããŸãããã¹ã¯ãŒãã¯redispass
ãšããŸãã
$ bin/redis-server --version Redis server v=6.2.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=ef9c41a6cf56a536
æºå
Node.jsã®ãããžã§ã¯ãã¯2ã€äœãããšã«ããŸãããå ±éã®ãšããã¯ãŸãšããŠæžããŸãã
$ npm init -y $ npm i -D typescript $ npm i -D -E prettier $ mkdir src
TypeScriptã®ããŒãžã§ã³ã
$ npx tsc --version Version 4.5.4
ExpressãããããNode.jsãšExpressã®å宣èšã®ã€ã³ã¹ããŒã«ã
$ npm i express $ npm i -D @types/node @types/express
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "esModuleInterop": true }, "include": [ "src" ] }
.prettierrc.json
{ "singleQuote": true }
ã€ã³ã¡ã¢ãªã§express-sessionã䜿ã
ã§ã¯ããŸãã¯express-sessionã«æ £ããŠãããŸãããã
express-sessionãšå宣èšãã€ã³ã¹ããŒã«ã
$ npm i express-session $ npm i -D @types/express-session
ä»åã®äŸåé¢ä¿ã¯ããã®ããã«ãªããŸããã
"devDependencies": { "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/node": "^17.0.5", "prettier": "2.5.1", "typescript": "^4.5.4" }, "dependencies": { "express": "^4.17.2", "express-session": "^1.17.2" }
äœæãããœãŒã¹ã³ãŒãã¯ããã¡ãã
src/app.ts
import express from 'express'; import session from 'express-session'; declare module 'express-session' { interface SessionData { firstAccessTime: string; counter: number; message: string; } } const app = express(); let port: number; const args = process.argv.slice(2); if (args.length == 0) { port = 3000; } else { port = parseInt(args[0], 10); } app.set('trust proxy', 1); app.use( session({ secret: 's3Cur3', name: 'session', // default: connect.sid resave: false, saveUninitialized: true, cookie: { path: '/', // default httpOnly: true, // default maxAge: 10 * 1000, // 10sec }, }) ); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { if (!req.session.firstAccessTime) { const now = new Date(); req.session.firstAccessTime = now.toISOString(); } req.session.counter = req.session.counter ? req.session.counter + 1 : 1; next(); }); app.post('/message', (req, res) => { const message = req.body['message']; req.session.message = message; res.send({ firstAccessTime: req.session.firstAccessTime, counter: req.session.counter, message: req.session.message, }); }); app.get('/message', (req, res) => { res.send({ firstAccessTime: req.session.firstAccessTime, counter: req.session.counter, message: req.session.message ? req.session.message : 'Hello World', }); }); app.listen(port, () => { console.log(`[${new Date().toISOString()}] start server[${port}]`); });
ã¡ãã£ãšãã€ã解説ããŠãããŸãã
ã¢ãžã¥ãŒã«ã®ã€ã³ããŒãã
import session from 'express-session';
次ã«ãã»ãã·ã§ã³ã§æ±ãããŒã¿ïŒSessionData
ïŒã®å宣èšãããŠãããŸãã
declare module 'express-session' { interface SessionData { firstAccessTime: string; counter: number; message: string; } }
ããããã£ãŠãããªããšãRequest#session
ã䜿ã£ãã»ãã·ã§ã³ããŒã¿ãæ±ããŸãããå°ãè£è¶³ãããã®ã§ãããã¯
åŸè¿°ããŸãã
ã»ãã·ã§ã³ããã³Cookieã®èšå®ã
app.use( session({ secret: 's3Cur3', name: 'session', // default: connect.sid resave: false, saveUninitialized: true, cookie: { path: '/', // default httpOnly: true, // default maxAge: 10 * 1000, // 10sec }, }) );
åèšå®é ç®ã¯ããã¡ãã«èšèŒããããŸãã
Cookieã®éšåã¯ããŠããããã®ä»ã®é ç®ãããã€ãèŠãŠã¿ãŸãããã
name
⊠Cookieåãããã©ã«ãã§ã¯connect.sid
ã ããå€æŽãæšå¥šãããŠããresave
âŠtrue
ã«ãããšããªã¯ãšã¹ãäžã«ã»ãã·ã§ã³ãå€æŽãããªãã£ãå Žåã§ãã匷å¶çã«ã»ãã·ã§ã³ã¹ãã¢ã«ä¿åããªãã- ããã©ã«ãå€ã¯
true
ã ããã»ãã·ã§ã³ã¹ãã¢ãšãŠãŒã¹ã±ãŒã¹ã«å¿ããŠéžæãã - ã»ãã·ã§ã³ã¹ãã¢ã«
touch
ãå®è£ ãããŠãããããã€ã»ãã·ã§ã³ã«æå¹æéãããå Žåã¯èšå®ãå¿ èŠãªå¯èœæ§ããã
- ããã©ã«ãå€ã¯
saveUninitialized
⊠åæåãããŠããªãã»ãã·ã§ã³ãã匷å¶çã«ã»ãã·ã§ã³ã¹ãã¢ã«ä¿åãã- ããã©ã«ãå€ã¯
true
ã ããæ瀺çã«èšå®ããããšãæšå¥š
- ããã©ã«ãå€ã¯
secret
âŠ å¿ é é ç®ãã»ãã·ã§ã³IDãä¿åããCookieã®çœ²åã«äœ¿çšããã- ä»åã¯ããŒãã³ãŒãããŠããããã·ãŒã¯ã¬ããã¯ç°å¢å€æ°çããååŸããã©ã³ãã ãªå€ã«ããããšãæšå¥š
ãã®ä»ã«ããããããªãã·ã§ã³ããããŸãã
Cookieã«é¢ããèšå®ã¯ãHTTPãšããŠã®Cookieã«å¯Ÿããå 容ã»ãŒãã®ãŸãŸãªã®ã§ãä»åæžããŠããªããã®ãå«ããŠå²æâŠã
æå¹æéã¯ãmaxAge
ãexpires
ã§æå®ããããšã«ãªããŸããããmaxAge
ã®ã¿ã䜿ãããšãæšå¥šãããŠããã¿ããã§ãã
cookie: { path: '/', // default httpOnly: true, // default maxAge: 10 * 1000, // 10sec },
ä»åã¯ã10ç§ã«ããŠãããŸãããCookienã®maxAge
ã«åæ ãããŸãã
ç¶ããŠãã»ãã·ã§ã³ã«ä¿åããããŒã¿ã¯ã©ããããããªããšæã£ãã®ã§ãããååã¢ã¯ã»ã¹æã®æ¥æãšã¢ã¯ã»ã¹ãã床ã«
å¢ããã«ãŠã³ã¿ãŒãããã«ãŠã§ã¢ã®åŠçã§èšå®ããããšã«ããŸããã
app.use((req, res, next) => { if (!req.session.firstAccessTime) { const now = new Date(); req.session.firstAccessTime = now.toISOString(); } req.session.counter = req.session.counter ? req.session.counter + 1 : 1; next(); });
ããšã¯ãPOST
ãããã¡ãã»ãŒãžãä¿åããããååŸãããã
app.post('/message', (req, res) => { const message = req.body['message']; req.session.message = message; res.send({ firstAccessTime: req.session.firstAccessTime, counter: req.session.counter, message: req.session.message, }); }); app.get('/message', (req, res) => { res.send({ firstAccessTime: req.session.firstAccessTime, counter: req.session.counter, message: req.session.message ? req.session.message : 'Hello World', }); });
ãã®æã«ãã»ãã·ã§ã³å ã®ããŒã¿ãã¬ã¹ãã³ã¹ã«å«ããŠã¿ãŸãã
ãã€ã³ãããããŒãã¯ãèµ·ååŒæ°ã«å¿ããŠå€æŽã§ããããã«ããŠããŸãããä»åã¯ããã»ã©æå³ã¯ãããŸããã
let port: number; const args = process.argv.slice(2); if (args.length == 0) { port = 3000; } else { port = parseInt(args[0], 10); }
å®æããã®ã§ãåäœç¢ºèªããŸãããã
ãã«ãã
$ npx tsc --project .
èµ·åã
$ node dist/app.js [2021-12-28T17:04:26.565Z] start server[3000]
ãšãããããã¢ã¯ã»ã¹ã
$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-sYkTynMndBqQZveBjfEAdNjDMqw" Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:36 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:05:26 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":1,"message":"Hello World"}
ã¡ãã»ãŒãžãä¿åããŠããªããŠããããã©ã«ãã®ã¡ãã»ãŒãžïŒHello World
ïŒãè¿ã£ãŠããŸãã
10ç§çµéãããŸã§ã¯ãååã¢ã¯ã»ã¹ã®æ¥æã¯åãã§ããã
$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-vyS+8+D6lA6S21HrzlrP2QkfBcg" Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:42 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:05:32 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":2,"message":"Hello World"}
ã«ãŠã³ã¿ãŒã¯äžãã£ãŠãããŸãã
ããã§ãã¡ãã»ãŒãžãä¿åã
$ curl -XPOST -c cookie.txt -b cookie.txt -i -H 'Content-Type: application/json' localhost:3000/message -d '{"message": "Hello Express-Session"}' HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 92 ETag: W/"5c-IKcfg7BQZtni1wQBP2yyNEfkayM" Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:47 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:05:37 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":3,"message":"Hello Express-Session"
確èªã
$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 92 ETag: W/"5c-3PLziySobG9OtC4EEA5aV6oo32k" Set-Cookie: session=s%3AlMjoQ202glCfSgGGWHnKcDL72lPLVGle.VvlWEocjst3r9hNWO2xXFEfQT2FrlXnJ5o33VmGQi7c; Path=/; Expires=Tue, 28 Dec 2021 17:05:54 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:05:44 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:05:26.716Z","counter":4,"message":"Hello Express-Session"}
ã»ãã·ã§ã³ã«ä¿åããã¡ãã»ãŒãžãååŸã§ããŸããã
ãªãã10ç§ä»¥äžçµéãããšãCookieã®maxAge
ãè¶
ããã®ã§ã»ãã·ã§ã³ãååŸã§ããªããªããŸãã
$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-fTkTw7lMrghKrv3y6JTcAEkRQBM" Set-Cookie: session=s%3ARZs7HJyez1JS4OUEv0RyN5NBxbV8xhBM.IkRYnD2xoZw%2BVjgsIhvDX%2Fl0zpOD2mrseTPpLKrQ33U; Path=/; Expires=Tue, 28 Dec 2021 17:06:06 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:05:56 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:05:56.916Z","counter":1,"message":"Hello World"}
ããã§ãåºæ¬çãªåäœç¢ºèªã¯ã§ããŸãããã
ã¡ãªã¿ã«ãããã²ãšã€ãµãŒããŒãèµ·åããŠ
$ node dist/app.js 3001
ãã¡ãã«åãCookieã䜿ã£ãŠã¢ã¯ã»ã¹ããŠããå¥ã®ã»ãã·ã§ã³ã«ãªãããšã確èªã§ããŸãã
$ curl -c cookie.txt -b cookie.txt localhost:3001/message
ã¡ã¢ãªã»ãã·ã§ã³ãªã®ã§ãåœç¶ã§ããããã®åŸã¯ããã¡ãã確èªããŠãããŸãã
è£è¶³
ãã®æç¹ã§ãããã€ãè£è¶³ãããŸãã
SessionDataã®å宣èšã®ããŒãž
ãŸãããã¡ãã®SessionData
ã«å¯Ÿããå宣èšã«ã€ããŠã
declare module 'express-session' { interface SessionData { firstAccessTime: string; counter: number; message: string; } }
ãã¡ãã¯ã以åã¯äžèŠã ã£ãã¿ãããªã®ã§ãããexpress-sessionã®1.17.1以éã¯SessionData
ã«æ瀺çã«å宣èšã
ããªããšãã»ãã·ã§ã³ã«ããŒã¿ãä¿åãããŒã¿ãååŸããããšããŠå±æ§ã«ã¢ã¯ã»ã¹ããã³ãŒããæžããšãã³ã³ãã€ã«ã
éããªããªããŸãã
src/app.ts:33:17 - error TS2339: Property 'firstAccessTime' does not exist on type 'Session & Partial<SessionData>'. 33 req.session.firstAccessTime = now.toISOString(); ~~~~~~~~~~~~~~~
issueã«ãäžãã£ãŠããŸããããããã¯æå³çãªå€æŽã®ããã§ãã
Incorrect typings for session object · Issue #49941 · DefinitelyTyped/DefinitelyTyped · GitHub
ããšããšã®SessionData
ã®åå®çŸ©ã¯ä»¥äžãªã®ã§ãããã³ã¡ã³ãã«ããããã«å©çšåŽã§ãå宣èšãè¿œå ãããŒãžããŠ
䜿çšããããšãæ³å®ããŠããã¿ããã§ãã
/** * This interface allows you to declare additional properties on your session object using [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). * * @example * declare module 'express-session' { * interface SessionData { * views: number; * } * } * */ interface SessionData { cookie: Cookie; }
TypeScriptã®å宣èšã®ããŒãžã«ã€ããŠã¯ããã¡ãã
TypeScript: Documentation - Declaration Merging
ããã«ã¯ããããªå£°ãããã¿ããã§ãexpress-sessionã®å宣èšã¯1.17.0ãå©çšãããšãã£ãæ¹æ³ãæ¡å
ãããŠããŸãããã
èšèšã®æå³ãšã¯åãã圢ã§ããã
ããã©ã«ãã®ã€ã³ã¡ã¢ãªã»ãã·ã§ã³ã¹ãã¢ã«ã€ããŠ
express-sessionãããã©ã«ãã§äœ¿çšããã€ã³ã¡ã¢ãªã»ãã·ã§ã³ã¹ãã¢ã«ã¯ããããªæ³šæäºé ããããŸããã
Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.
GitHub - expressjs/session: Simple session middleware for Express
ã»ãšãã©ã®ã±ãŒã¹ã§ã¡ã¢ãªãªãŒã¯ãããšããããšãªã®ã§ãããã©ãããããšã§ãããïŒ
ãœãŒã¹ã³ãŒããèŠãŠããæããæ瀺çã«ã»ãã·ã§ã³ãdestroy
ããªãéããã»ãã·ã§ã³ããŒã¿ãæ®ãç¶ããã¿ããã§ããã
ã»ãã·ã§ã³IDãä¿æããŠããmaxAge
ãéããŠããŸããšãã»ãã·ã§ã³ã远跡ã§ããªããªãã®ã§æ¶ãããšãã§ããã
ã¡ã¢ãªãªãŒã¯ããŸãããšâŠã
ãšããããã§ãæ¬çªç°å¢ãªã©ã§ã¯ãã®ã»ãã·ã§ã³ã¹ãã¢ã¯äœ¿ããã«ãä»ã®ã»ãã·ã§ã³ã¹ãã¢ã䜿ããŸãããããšã
connect-redisã䜿ã
express-sessionã§å©çšã§ããã»ãã·ã§ã³ã¹ãã¢ã®ãã¡ãRedisãããã¯ãšã³ãã«äœ¿ããã®ãconnect-redisã§ãã
GitHub - tj/connect-redis: Redis session store for Connect
ä»åã¯ããã¡ãã䜿ã£ãŠã»ãã·ã§ã³ããŒã¿ãRedisã«ä¿åããŠã¿ãŸãã
connect-redisã䜿ãã«ã¯ãRedisã¯ã©ã€ã¢ã³ããå¥ã«ã€ã³ã¹ããŒã«ããå¿ èŠããããŸãã䜿çšã§ããã®ã¯ã以äžã®3ã€ã§ãã
- GitHub - redis/node-redis: A high-performance Node.js Redis client.
- ããŒãžã§ã³4ç³»ã«ã¯æªå¯Ÿå¿ã§ãããŒãžã§ã³3ç³»ã䜿ãå¿ èŠããã
- GitHub - luin/ioredis: 🚀 A robust, performance-focused, and full-featured Redis client for Node.js.
- GitHub - yeahoffline/redis-mock: Node.js redis client mock
- ã¢ãã¯ãªã®ã§ããã¹ãç®ç
ã©ããããããªãšæã£ãã®ã§ãããä»åã¯ioredisã䜿ãããšã«ããŸãã
æ°ããNode.jsãããžã§ã¯ããäœæããŠãå ã»ã©ãšåãããã«Expressã®åŸã«express-sessionãšå宣èšãã€ã³ã¹ããŒã«ã
$ npm i express-session $ npm i -D @types/express-session
ç¶ããŠãconnect-redisãšioredisãã€ã³ã¹ããŒã«ããŸãã
$ npm i ioredis connect-redis $ npm i -D @types/ioredis @types/connect-redis
ä»åã®äŸåé¢ä¿ã¯ããã¡ãã
"devDependencies": { "@types/connect-redis": "^0.0.18", "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/ioredis": "^4.28.5", "@types/node": "^17.0.5", "prettier": "2.5.1", "typescript": "^4.5.4" }, "dependencies": { "connect-redis": "^6.0.0", "express": "^4.17.2", "express-session": "^1.17.2", "ioredis": "^4.28.2" }
ãœãŒã¹ã³ãŒãã¯ãå ã»ã©ã®ãœãŒã¹ã³ãŒããå ã«connect-redisã䜿ãããã«ä¿®æ£ããŸãã
src/app.ts
import express from 'express'; import session from 'express-session'; import connectRedis from 'connect-redis'; import Redis from 'ioredis'; declare module 'express-session' { interface SessionData { firstAccessTime: string; counter: number; message: string; } } const app = express(); let port: number; const args = process.argv.slice(2); if (args.length == 0) { port = 3000; } else { port = parseInt(args[0], 10); } const RedisStore = connectRedis(session); const redisClient = new Redis({ host: '172.17.0.2', port: 6379, password: 'redispass', }); app.set('trust proxy', 1); app.use( session({ secret: 's3Cur3', name: 'session', // default: connect.sid resave: false, saveUninitialized: true, cookie: { path: '/', // default httpOnly: true, // default maxAge: 10 * 1000, // 10sec }, store: new RedisStore({ client: redisClient }), }) ); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use((req, res, next) => { if (!req.session.firstAccessTime) { const now = new Date(); req.session.firstAccessTime = now.toISOString(); } req.session.counter = req.session.counter ? req.session.counter + 1 : 1; next(); }); app.post('/message', (req, res) => { const message = req.body['message']; req.session.message = message; res.send({ firstAccessTime: req.session.firstAccessTime, counter: req.session.counter, message: req.session.message, }); }); app.get('/message', (req, res) => { res.send({ firstAccessTime: req.session.firstAccessTime, counter: req.session.counter, message: req.session.message ? req.session.message : 'Hello World', }); }); app.listen(port, () => { console.log(`[${new Date().toISOString()}] start server[${port}]`); });
å€æŽéšåã¯ãã¢ãžã¥ãŒã«ã®ã€ã³ããŒãã
import connectRedis from 'connect-redis'; import Redis from 'ioredis';
Redisãžæ¥ç¶ããããã®ã¯ã©ã€ã¢ã³ãã®äœæã
const RedisStore = connectRedis(session); const redisClient = new Redis({ host: '172.17.0.2', port: 6379, password: 'redispass', });
ãããŠãã»ãã·ã§ã³ããã«ãŠã§ã¢ãžã®store
æå®ã§ããããã¡ãã§ãå
ã»ã©äœæããã¯ã©ã€ã¢ã³ãã䜿ããŸãã
app.use( session({ secret: 's3Cur3', name: 'session', // default: connect.sid resave: false, saveUninitialized: true, cookie: { path: '/', // default httpOnly: true, // default maxAge: 10 * 1000, // 10sec }, store: new RedisStore({ client: redisClient }), }) );
èšå®å 容ã«ã€ããŠã¯ããã¡ããåç §ã
connect-redis / RedisStore(options)
ä»åã¯client
ã®ã¿ãæå®ããŠããã®ã§ãããcookie.expires
ãæå®ããŠããå Žåã¯ãããttl
ãšããŠã䜿çšãããŸãã
ãšæžããšãä»åã¯cookie.maxAge
ã®ã¿ãæå®ããŠããã®ã§å¹æããªãããã«æããŸããå®éãconnect-redisãèŠãŠããã®ã
cookie.expires
ã§ãã
https://github.com/tj/connect-redis/blob/v6.0.0/lib/connect-redis.js#L138-L147
ã§ãããexpress-sessionåŽã§cookie.maxAge
ãèšå®ãããšcookie.expires
ãåæã«èšå®ããã®ã§ãããã§ãåé¡
ãªãããã§ãã
session/cookie.js at v1.17.2 · expressjs/session · GitHub
ãã®ãããããåäœç¢ºèªæã«èŠãŠãããŸãããã
ã§ã¯ããã«ãã
$ npx tsc --project .
ãµãŒããŒã2ã€èµ·åããŸãã
$ node dist/app.js [2021-12-28T17:09:58.042Z] start server[3000] $ node dist/app.js 3001 [2021-12-28T17:09:59.014Z] start server[3001]
ã»ãã·ã§ã³ã¹ãã¢ãã¡ã¢ãªã ã£ãæãšåãæäœããã¢ã¯ã»ã¹ãããµãŒããŒã亀äºã«ããªããè©ŠããŠã¿ãŸãããã
ãŸãã¯1ã€ç®ã®ãµãŒããŒã«ã¢ã¯ã»ã¹ã
$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-yJLufkq12oSrS08zelcHcp3KZFw" Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:05 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:10:55 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":1,"message":"Hello World"}
2ã€ç®ã®ãµãŒããŒã«ã¢ã¯ã»ã¹ã
$ curl -c cookie.txt -b cookie.txt -i localhost:3001/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-Cr32RdSZjDB1tDYcFHwEedQGAjI" Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:11 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:11:01 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":2,"message":"Hello World"}
åãã»ãã·ã§ã³ãèŠãŠããããšã確èªã§ããŸãã
ããã§ã1ã€ç®ã®ãµãŒããŒã«å¯ŸããŠã¡ãã»ãŒãžãä¿åã
$ curl -XPOST -c cookie.txt -b cookie.txt -i -H 'Content-Type: application/json' localhost:3000/message -d '{"message": "Hello Express-Session"}' HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 92 ETag: W/"5c-emnmjDwUdhAq5Z49Escy25ABU00" Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:16 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:11:06 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":3,"message":"Hello Express-Session"}
2ã€ç®ã®ãµãŒããŒã«å¯ŸããŠãåç §ã§ããã確èªã
s$ curl -c cookie.txt -b cookie.txt -i localhost:3001/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 92 ETag: W/"5c-x1y1bK4LdcEFThWoEN+fI3fnu6s" Set-Cookie: session=s%3AkTnWdu3my-RutEu4qW2ppytVowE6exsk.S8lGv9ThCcOkGxQxAkPBpj8i0zq7J9ANLre2hcPQ8SQ; Path=/; Expires=Tue, 28 Dec 2021 17:11:21 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:11:11 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:10:55.322Z","counter":4,"message":"Hello Express-Session"}
ãµãŒããŒãè·šãã§ãã»ãã·ã§ã³ãæäœã§ããŠããŸããã
10ç§ä»¥äžçµéãããšãCookieã®maxAge
ãè¶
ããã®ã§ã»ãã·ã§ã³ãååŸã§ããªããªããšãããåãã§ãã
s$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-TZMcIC3EIc9lkB8W/R10PtrgYVg" Set-Cookie: session=s%3A2TySiWJl8xIwdMq1-gR1cmNXOzJmLSak.dl0PVDfh9LAqIq6ec6kKLIrZLdeXiWiAkBFOp4GzsBM; Path=/; Expires=Tue, 28 Dec 2021 17:11:35 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:11:25 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:11:25.742Z","counter":1,"message":"Hello World"}
RedisåŽã確èªãã
connect-redisã䜿ã£ãŠRedisã«ä¿åããããŒã¿ãã©ããªã£ãŠããããã¡ãã£ãšæ°ã«ãªããšããã§ãã
確èªããŠã¿ãŸãããã
ãããªæãã§ããšããããã»ãã·ã§ã³ã2ã€äœããŸããã¢ã¯ã»ã¹ãããµãŒããŒãå€ããŠããã®ã¯ããªããšãªãã§ãã
$ curl -c cookie.txt -b cookie.txt -i localhost:3000/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-DLTqL9qW8d2JlmuOtlrCspXxUPU" Set-Cookie: session=s%3AdEMW9ngNDBJi_OUldTehMDEF4Igqplw3.NVZiKzWyVfNayzBJOPqRpg1bhB0Py8y4RisBQRxefzY; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:19:05 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:19:05.638Z","counter":1,"message":"Hello World"} $ curl -c cookie2.txt -b cookie2.txt -i localhost:3001/message HTTP/1.1 200 OK X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 82 ETag: W/"52-CdWxLXIyKUZWlDXpISXDUgSZtrA" Set-Cookie: session=s%3APxUkTEILqMR32DUkBV3f2cYu-sosa8-o.xgbiMNzt9VpbyMjRGMhUAFw8xz4JyOW9k4rF0yaTJjs; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly Date: Tue, 28 Dec 2021 17:19:05 GMT Connection: keep-alive Keep-Alive: timeout=5 {"firstAccessTime":"2021-12-28T17:19:05.647Z","counter":1,"message":"Hello World"}
ãã®ãŸãŸæŸã£ãŠãããšãã»ãã·ã§ã³ã®æå¹æéãåããŠããŸãã®ã§ãwatch
ã³ãã³ãã§ã¢ã¯ã»ã¹ãç¶ããããã«
ããŠãããŸãã
$ watch -n 1 'curl -c cookie.txt -b cookie.txt -i localhost:3000/message && curl -c cookie2.txt -b cookie2.txt -i localhost:3001/message'
ãã®è£ã§ãRedis CLIã§ãã°ã€ã³ã
$ bin/redis-cli -a redispass
ããŒã®äžèŠ§ãèŠãŠã¿ãŸãã
127.0.0.1:6379> keys * 1) "sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3" 2) "sess:PxUkTEILqMR32DUkBV3f2cYu-sosa8-o"
å
ã»ã©ã®Set-Cookie
ããããŒã®å
容ãèŠããšããªããšãªãã»ãã·ã§ã³IDãšRedisã®ããŒãçŽä»ãããããã§ããã
Set-Cookie: session=s%3AdEMW9ngNDBJi_OUldTehMDEF4Igqplw3.NVZiKzWyVfNayzBJOPqRpg1bhB0Py8y4RisBQRxefzY; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly Set-Cookie: session=s%3APxUkTEILqMR32DUkBV3f2cYu-sosa8-o.xgbiMNzt9VpbyMjRGMhUAFw8xz4JyOW9k4rF0yaTJjs; Path=/; Expires=Tue, 28 Dec 2021 17:19:15 GMT; HttpOnly
ããŒã«å¯ŸããTTLãèŠãŠã¿ãŸãã
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 9
Cookieã®maxAge
ãåæ ãããŠããããªæãã§ããã
ããŒã«å¯Ÿããå€ã¯ã©ãã§ããããã
127.0.0.1:6379> get sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 "{\"cookie\":{\"originalMaxAge\":10000,\"expires\":\"2021-12-28T17:21:11.044Z\",\"httpOnly\":true,\"path\":\"/\"},\"firstAccessTime\":\"2021-12-28T17:19:05.638Z\",\"counter\":112}"
Cookieãšã»ãã·ã§ã³ã®å 容ããJSONã«ãªã£ãŠå ¥ã£ãŠããæãã§ããã
ããã¯ãããã©ã«ãã®ã·ãªã¢ã©ã€ã¶ãŒãJSONã ããã§ãããã
connect-redis / RedisStore(options) / serializer
ããã§ãwatch
ã³ãã³ãã®å®è¡ãåæ¢ãããšãã¢ã¯ã»ã¹ããªããªãã®ã§TTLãæžå°ããŠãããŸãã
127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 7 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 6 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 4 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 3 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 2 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 1 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) 0 127.0.0.1:6379> ttl sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (integer) -2
æçµçã«0ã«ãªã£ãåŸã¯ãããŒã¿ããªããªã£ãŠããŸããŸãã
127.0.0.1:6379> keys * (empty array) 127.0.0.1:6379> get sess:dEMW9ngNDBJi_OUldTehMDEF4Igqplw3 (nil)
ããã§ãã»ãã·ã§ã³ã®æå¹æéãåããåŸã¯ãRedisãããããŒã¿ããªããªãããšã確èªã§ããŸããã
ãŸãšã
express-sessionã䜿ã£ãŠããµãŒããŒãµã€ãã®ã»ãã·ã§ã³ã䜿ãæ¹æ³ãšãRedisãçµã¿åãããæ¹æ³ã確èªããŠã¿ãŸããã
åºæ¬çãªç¥èãšããŠã¯æŒãããããã®ããªãšããæããããã®ãšãããã©ã«ãèšå®ã®èœãšãç©Žãªã©ãææ¡ã§ããŠ
è¯ãã£ãããªãšæããŸãã