├── .gitignore ├── README.md ├── index.js ├── lib ├── cookie.js ├── session.js └── store.js ├── package.json └── test ├── app.js └── store_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-session 2 | 3 | ## Installation 4 | 5 | ```bash 6 | $ git clone https://github.com/geemo/redis-session.git 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```js 12 | const app = express(); 13 | const session = require('redis-session'); 14 | 15 | app.use(session({ 16 | url: 'redis://127.0.0.1:6379/0', 17 | sidKey: 'redis.sid', 18 | cookie: { 19 | maxAge: 24 * 60 * 60 * 1000 //ms 20 | }, 21 | ttl: 24 * 60 * 60, //sec 22 | resave: true, 23 | saveUninit: false 24 | })); 25 | ``` 26 | 27 | ### session(options) 28 | 29 | Create a session middleware with the given `options`. 30 | 31 | ##### url 32 | 33 | Redis server url. The default value is "redis://127.0.0.1:6379/0". 34 | 35 | ##### sidKey 36 | 37 | Session ID cookie name. The default value is "redis.sid". 38 | 39 | ##### cookie 40 | 41 | Settings for the session ID cookie. 42 | 43 | The default value is `{ path: '/', httpOnly: true, maxAge: 604800000 \*7 days*\}`. 44 | 45 | ##### ttl 46 | 47 | Redis session TTL (expiration) in seconds. The default value is one day. 48 | 49 | ##### resave 50 | 51 | Forces the session to be saved back to the session store. The default value is true. 52 | 53 | ##### saveUninit 54 | 55 | Forces a session that is "uninitialized" to be saved to the store. The default value is true. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*! 3 | * redis-session 4 | * Copyright(c) 2016 geemo 5 | * MIT Licensed 6 | */ 7 | 8 | const Store = require('./lib/store.js'); 9 | const Session = require('./lib/session.js'); 10 | const Cookie = require('./lib/cookie.js'); 11 | 12 | const urlParse = require('url').parse; 13 | 14 | module.exports = exports = session; 15 | 16 | exports.Store = Store; 17 | exports.Session = Session; 18 | exports.Cookie = Cookie; 19 | 20 | /* session({ 21 | url: 'redis://127.0.0.1:6379/0', 22 | sidKey: 'redis.sid', 23 | cookie: { 24 | maxAge: 24 * 60 * 60 * 1000 //ms 25 | }, 26 | ttl: 24 * 60 * 60 //sec 27 | }) 28 | */ 29 | 30 | function session(options) { 31 | let opts = options || {}, 32 | url = opts.url, 33 | sidKey = opts.sidKey || 'redis.sid', 34 | cookie = opts.cookie || {}, 35 | ttl = opts.ttl, 36 | resave = opts.resave, 37 | saveUninit = opts.saveUninit; 38 | 39 | if (resave === undefined) resave = true; 40 | if (saveUninit === undefined) saveUninit = true; 41 | 42 | let store = new Store({ 43 | url: url, 44 | ttl: ttl 45 | }); 46 | let storeReady = false; 47 | 48 | store.once('connect', () => storeReady = true); 49 | store.once('disconnect', () => console.log('store disconnect!')); 50 | store.once('error', err => console.log(err)); 51 | 52 | store.generate = function(req) { 53 | req.sessionId = genId(); 54 | req.session = new Session(req); 55 | req.session.cookie = new Cookie(cookie); 56 | } 57 | 58 | return (req, res, next) => { 59 | if (req.session) return next(); 60 | if (!storeReady) return next(); 61 | 62 | let pathname = urlParse(req.url).pathname; 63 | if (pathname.indexOf(cookie.path || '/') !== 0) return next(); 64 | 65 | req.sessionStore = store; 66 | let cookieObj = Cookie.parse(req.headers['cookie']) || {}; 67 | req.sessionId = cookieObj[sidKey]; 68 | 69 | let _writeHead = res.writeHead; 70 | let writed = false; 71 | let isExists = false; 72 | res.writeHead = function() { 73 | if (writed) return false; 74 | writed = true; 75 | 76 | if (!isExists) { 77 | res.setHeader('set-cookie', 78 | Cookie.serialize(sidKey, req.sessionId, req.session.cookie)); 79 | } 80 | 81 | _writeHead.apply(res, arguments); 82 | }; 83 | 84 | let _end = res.end; 85 | let ended = false; 86 | let originSess; 87 | res.end = function() { 88 | if (ended) return false; 89 | ended = true; 90 | 91 | let setCookie = res.getHeader('set-cookie'); 92 | if (!setCookie || setCookie.indexOf(sidKey) !== 0) { 93 | if (!isExists) { 94 | res.setHeader('set-cookie', 95 | Cookie.serialize(sidKey, req.sessionId, req.session.cookie)); 96 | } 97 | } 98 | 99 | if (saveUninit || (!saveUninit && originSess !== hash(req.session))) { 100 | if (!isExists) { 101 | req.session.save(); 102 | } else if (isExists && resave) { 103 | req.session.resetExpires(); 104 | req.session.save(); 105 | } 106 | } 107 | 108 | _end.apply(res, arguments); 109 | }; 110 | 111 | if (!req.sessionId) { 112 | generate(req); 113 | return next(); 114 | } 115 | 116 | store.get(req.sessionId, (err, sess) => { 117 | if (err) { 118 | generate(req); 119 | return next(err); 120 | } 121 | 122 | if (!sess) { 123 | generate(req); 124 | return next(); 125 | } 126 | 127 | isExists = true; 128 | store.createSession(req, sess); 129 | if (!saveUninit) { 130 | originSess = hash(req.session); 131 | } 132 | 133 | next(); 134 | }); 135 | 136 | function generate(req) { 137 | store.generate(req); 138 | if (!saveUninit) { 139 | originSess = hash(req.session); 140 | } 141 | 142 | } 143 | }; 144 | }; 145 | 146 | function genId() { 147 | return [Date.now(), Math.floor(Math.random() * 1000)].join(''); 148 | } 149 | 150 | function hash(sess) { 151 | return JSON.stringify(sess, (k, v) => { 152 | if (k !== 'cookie') { 153 | return v; 154 | } 155 | return; 156 | }); 157 | } 158 | -------------------------------------------------------------------------------- /lib/cookie.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = Cookie; 4 | 5 | function setTime(self, time) { 6 | if (time instanceof Date) self._expires = time; 7 | else if (typeof time === 'number') self._expires = new Date(Date.now() + time); 8 | } 9 | 10 | function Cookie(options) { 11 | if (!(this instanceof Cookie)) return new Cookie(options); 12 | this.path = '/'; 13 | this._expires = new Date(Date.now() + 604800000); // 7 days 14 | this.httpOnly = true; 15 | 16 | if (options && typeof options === 'object') { 17 | let prop; 18 | for (prop in options) { 19 | if (options.hasOwnProperty(prop)) { 20 | this[prop] = options[prop]; 21 | } 22 | } 23 | if (options['expires'] && !(options['expires'] instanceof Date)) { 24 | this._expires = new Date(options['expires']); 25 | } 26 | } 27 | } 28 | 29 | Cookie.parse = function(str){ 30 | if(!str || typeof str !== 'string') return null; 31 | let obj = {}; 32 | 33 | let pairs = str.split(';'); 34 | 35 | pairs.forEach(pair => { 36 | let kv = pair.trim().split('='); 37 | obj[kv[0]] = kv[1]; 38 | }); 39 | 40 | if(obj.hasOwnProperty('httpOnly')) obj['httpOnly'] = true; 41 | 42 | return obj; 43 | } 44 | 45 | Cookie.serialize = function(name, value, options){ 46 | if(!name) throw new Error('name is required!'); 47 | let attrs = [`${name}=${value ? value : ''}`]; 48 | 49 | if(options && typeof options === 'object'){ 50 | if(options['path']) attrs.push(`path=${options['path']}`); 51 | if(options['expires']) attrs.push(`expires=${options['expires'].toUTCString()}`); 52 | if(options['secure']) attrs.push(`secure=${options['secure']}`); 53 | if(options['domain']) attrs.push(`domain=${options['domain']}`); 54 | if(options['httpOnly']) attrs.push('httpOnly'); 55 | } 56 | 57 | return attrs.join('; '); 58 | } 59 | 60 | //public 61 | Cookie.prototype = { 62 | constructor: Cookie, 63 | 64 | get maxAge() { 65 | return this._expires.getTime() - Date.now(); 66 | }, 67 | 68 | set maxAge(ms) { 69 | setTime(this, ms); 70 | }, 71 | 72 | get expires() { 73 | return this._expires; 74 | }, 75 | 76 | set expires(date) { 77 | setTime(this, date); 78 | }, 79 | 80 | get data() { 81 | return { 82 | 'path': this.path, 83 | 'expires': this._expires, 84 | 'max-age': this._maxAge, 85 | 'httpOnly': this.httpOnly, 86 | 'secure': this.secure, 87 | 'domain': this.domain 88 | }; 89 | }, 90 | 91 | serialize: function(name, value) { 92 | Cookie.serialize(name, value, this.data); 93 | }, 94 | 95 | toJSON: function() { 96 | return this.data; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = Session; 4 | 5 | function Session(req, sess) { 6 | if (!(this instanceof Session)) return new Session(req, sess); 7 | 8 | if (!req) throw new Error('req is required!'); 9 | 10 | Object.defineProperty(this, 'req', { value: req }); 11 | 12 | this.expires = new Date(Date.now() + this.req.sessionStore.ttl * 1000); 13 | if (sess && typeof sess === 'object') { 14 | let prop; 15 | for(prop in sess){ 16 | if(sess.hasOwnProperty(prop)) 17 | this[prop] = sess[prop]; 18 | } 19 | } 20 | } 21 | 22 | Session.prototype.constructor = Session; 23 | 24 | Session.prototype.save = function(fn) { 25 | this.req.sessionStore.set(this.req.sessionId, this, fn); 26 | }; 27 | 28 | Session.prototype.destory = function(fn) { 29 | this.req.session = null; 30 | this.req.sessionStore.destory(this.id, fn); 31 | }; 32 | 33 | Session.prototype.resetExpires = function() { 34 | this.expires = new Date(Date.now() + this.req.sessionStore.ttl * 1000); 35 | } 36 | 37 | Session.prototype.reload = function(fn) { 38 | let store = this.req.sessionStore; 39 | let self = this; 40 | 41 | store.get(this.id, (err, sess) => { 42 | if (err) fn(err); 43 | if (!sess) fn(new Error('failed to reload session!')); 44 | 45 | store.createSession(self.req, sess); 46 | fn(null); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = Store; 4 | 5 | const redis = require('redis'); 6 | const util = require('util'); 7 | const EventEmitter = require('events').EventEmitter; 8 | 9 | const Cookie = require('./cookie.js'); 10 | const Session = require('./session.js'); 11 | 12 | const ONE_DAY = 86400; 13 | 14 | function Store(options) { 15 | if (!(this instanceof Store)) return new Store(options); 16 | 17 | this.url = 'redis://127.0.0.1:6379'; 18 | if (options && typeof options === 'object') { 19 | let prop; 20 | for (prop in options) { 21 | if (options.hasOwnProperty(prop)) 22 | this[prop] = options[prop]; 23 | } 24 | } 25 | this.ttl = (typeof this.ttl === 'number') ? this.ttl : ONE_DAY; 26 | 27 | let self = this; 28 | 29 | EventEmitter.call(this); 30 | this.client = redis.createClient(this.url, options); 31 | this.client.once('connect', () => self.emit('connect')); 32 | this.client.once('disconnect', () => self.emit('disconnect')); 33 | this.client.once('error', err => self.emit('error', err)); 34 | }; 35 | 36 | util.inherits(Store, EventEmitter); 37 | 38 | Store.prototype.constructor = Store; 39 | 40 | Store.prototype.get = function(sid, fn) { 41 | this.client.get(sid, (err, data) => { 42 | if (err) return fn(err); 43 | if (!data) return fn(); 44 | 45 | let result; 46 | try { 47 | result = JSON.parse(data); 48 | } catch (e) { 49 | return fn(e); 50 | } 51 | return fn(null, result); 52 | }); 53 | }; 54 | 55 | Store.prototype.set = function(sid, sess, fn) { 56 | let args = [sid]; 57 | let jsess; 58 | try { 59 | jsess = JSON.stringify(sess); 60 | } catch (e) { 61 | return fn(e); 62 | } 63 | args.push(jsess); 64 | args.push('EX', this.ttl); 65 | 66 | this.client.set(args, fn); 67 | }; 68 | 69 | Store.prototype.destory = function(sid, fn) { 70 | this.client.del(sid, fn); 71 | }; 72 | 73 | Store.prototype.resetExpires = function(sid, fn) { 74 | this.client.expire(sid, this.ttl, fn); 75 | }; 76 | 77 | Store.prototype.load = function(sid, req, fn){ 78 | let self = this; 79 | this.get(sid, (err, sess) => { 80 | if(err) fn(err); 81 | if(!sess) fn(); 82 | 83 | self.createSession(req, sess); 84 | fn(null, sess); 85 | }); 86 | }; 87 | 88 | Store.prototype.createSession = function(req, sess) { 89 | let cookie = new Cookie(sess.cookie); 90 | let session = new Session(req, sess); 91 | session.cookie = cookie; 92 | req.session = session; 93 | return session; 94 | }; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-session", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "node ./test/app.js" 11 | }, 12 | "author": "geemo", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express": "^4.13.4", 16 | "redis": "^2.6.0-0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const express = require('express'); 4 | const session = require('../index.js'); 5 | const PORT = process.env.PORT || 80; 6 | let app = express(); 7 | 8 | app.use(session({ 9 | url: 'redis://127.0.0.1:6379/0', 10 | sidKey: 'my.sid', 11 | ttl: 20 * 60 * 60, //sec 12 | resave: false, 13 | saveUninit: false 14 | })); 15 | 16 | app.get('/', (req, res) => { 17 | if(req.session.isVisited){ 18 | req.session.obj = { 19 | aa: 55, 20 | bb: 66 21 | }; 22 | 23 | res.end('asdfsad'); 24 | } else { 25 | req.session.isVisited = true; 26 | res.end('bbbb'); 27 | } 28 | }); 29 | 30 | app.listen(PORT, () => { 31 | console.log(`server start on port: ${PORT}!`); 32 | }); 33 | -------------------------------------------------------------------------------- /test/store_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Store = require('../lib/store.js'); 4 | 5 | 6 | let store = new Store({ 7 | url: 'redis://127.0.0.1:6379/1' 8 | }); 9 | 10 | store.on('connect', () => { 11 | console.log('connect'); 12 | }); 13 | 14 | store.on('error', err => { 15 | console.log(err); 16 | }); 17 | 18 | store.set('111', {a: 5, b: 6}, err => { 19 | console.log(err); 20 | }); 21 | 22 | store.destory('111', () => { 23 | store.get('111', (err, data) => { 24 | console.log(err, data); 25 | }); 26 | }) --------------------------------------------------------------------------------