Session Management in Nodejs Using Redis as Session Store

We have covered session management in ExpressJs using a global variable technique which of course will not work in the case of shared server or concurrent execution of HTTP requests which is the most familiar production scenario.

Codeforgeek readers requested to provide a solution for this issue and the optimal one is to use external session storage which is not dependent on application requests, the answer is Redis cause this is the lightweight and easy to use NoSQL database.

In this tutorial I am going to explain how to design and code session-oriented express web applications by using Redis as external session storage.

DOWNLOAD

To get familiar with Session handling in ExpressJS I recommend to read our first article here.

Getting started with Redis :

If you have already installed Redis please go to the next section. For those of you who are not familiar with Redis here is a little introduction.

Redis is a key-value pair cache and store. it is also referred to as a data structure server cause keys can contain List, Hash, sets, sorted sets, etc.

Redis is fast cause it works in-memory data set i.e it by default stores data in your memory than disk and if you from a CS background you very well know CRUD operation on memory is way faster than disk, so is Redis.

if you restart Redis or shut it down you may lose all data unless you enable the option to dump those data on the hard disk. Be careful!

Installation:

1 : On mac

On Mac if you have brew install then just open up your terminal and type

brew install redis

Make sure you have command line tools installed cause it needs GCC to compile it.

If you don’t have brew then please install it. It’s awesome!

2 : On ubuntu

Run the following command on Ubuntu and the rest will be done.

sudo apt-get install redis-server

3: On Windows

Well, Redis does not support Windows! Hard luck.

Basic REDIS command :

I am going to mention those command which I need to go with this tutorial. For detailed information please visit the nice demo built by awesome Redis team to make you pro in Redis.

1 : Starting Redis server.

Run this command on terminal.

redis-server &

2 : Open Redis CLI too.

Run this command on terminal.

redis-cli

3 : List all Keys.
Run this command on terminal.

KEYS *

4 : Retrieve information regarding particular key.
Run this command on terminal.

GET <key name>

Once you have installed Redis, by running the first command you should something like this.
Redis start sceeen

Express session with Redis

To add support of Redis you have to use Redis client and connect-redis. Create express-session and pass it to connect-redis object as parameter. This will initialize it.

Then in session middleware, pass the Redis store information such as host, port, and other required parameters.

Here is a sample express code with Redis support. Have a look.

Express Session using Redis :
var express = require('express');
var redis   = require("redis");
var session = require('express-session');
var redisStore = require('connect-redis')(session);
var bodyParser = require('body-parser');
var client  = redis.createClient();
var app = express();

app.set('views', __dirname + '/views');
app.engine('html', require('ejs').renderFile);

app.use(session({
    secret: 'ssshhhhh',
    // create new redis store.
    store: new redisStore({ host: 'localhost', port: 6379, client: client,ttl :  260}),
    saveUninitialized: false,
    resave: false
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

app.get('/',function(req,res){ 
    // create new session object.
    if(req.session.key) {
        // if email key is sent redirect.
        res.redirect('/admin');
    } else {
        // else go to home page.
        res.render('index.html');
    }
});

app.post('/login',function(req,res){
    // when user login set the key to redis.
    req.session.key=req.body.email;
    res.end('done');
});

app.get('/logout',function(req,res){
    req.session.destroy(function(err){
        if(err){
            console.log(err);
        } else {
            res.redirect('/');
        }
    });
});

app.listen(3000,function(){
    console.log("App Started on PORT 3000");
});

Notice the code where we are initiating the session. Have a look.

app.use(session({
    secret: 'ssshhhhh',
    // create new redis store.
    store: new redisStore({ host: 'localhost', port: 6379, client: client}),
    saveUninitialized: false,
    resave: false
}));

If Redis server running, then this is the default configuration. Once you have configured it. Store your session key in the way we were doing in the previous example.

req.session.key_name = value to set
// this will be set to redis, value may contain User ID, email or any information which you need across your application.

Fetch the information from redis session key.

req.session.key["keyname"]

Our project:

To demonstrate this I have developed the web application which will allow you to register and login and post some status. It’s simple but it will demonstrate to you how to handle session using external storage.

Create project folder and copy these code to package.json.

Our package.json :

package.json
{
  "name": "Node-Session-Redis",
  "version": "0.0.1",
  "scripts": {
    "start": "node ./bin"
  },
  "dependencies": {
    "async": "^1.2.1",
    "body-parser": "^1.13.0",
    "connect-redis": "^2.3.0",
    "cookie-parser": "^1.3.5",
    "ejs": "^2.3.1",
    "express": "^4.12.4",
    "express-session": "^1.11.3",
    "mysql": "^2.7.0",
    "redis": "^0.12.1"
  }
}

Install dependencies by running following command.

npm install

Our database:

Once completed let’s design simple database to support our application. Here is the diagram.
database diagram

The database is simple and straight, please create a database in MySQL and run following DDL queries.

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';

CREATE SCHEMA IF NOT EXISTS `redis_demo` DEFAULT CHARACTER SET latin1 ;
USE `redis_demo` ;

-- -----------------------------------------------------
-- Table `redis_demo`.`user_login`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `redis_demo`.`user_login` (
  `user_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '',
  `user_email` VARCHAR(50) NOT NULL COMMENT '',
  `user_password` VARCHAR(50) NOT NULL COMMENT '',
  `user_name` VARCHAR(50) NOT NULL COMMENT '',
  PRIMARY KEY (`user_id`)  COMMENT '',
  UNIQUE INDEX `user_email` (`user_email` ASC)  COMMENT '')
ENGINE = InnoDB
AUTO_INCREMENT = 7
DEFAULT CHARACTER SET = latin1;

-- -----------------------------------------------------
-- Table `redis_demo`.`user_status`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `redis_demo`.`user_status` (
  `user_id` INT(11) NOT NULL COMMENT '',
  `user_status` TEXT NOT NULL COMMENT '',
  `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '',
  INDEX `user_id` (`user_id` ASC)  COMMENT '',
  CONSTRAINT `user_status_ibfk_1`
    FOREIGN KEY (`user_id`)
    REFERENCES `redis_demo`.`user_login` (`user_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = latin1;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

Our Server code

Server file contains application routes, database support and Redis session support. We will first create connect to database and initialize Redis then according to Routes particular action will happen.

/bin/index.js
/**
  Loading all dependencies.
**/

var express         =     require("express");
var redis           =     require("redis");
var mysql           =     require("mysql");
var session         =     require('express-session');
var redisStore      =     require('connect-redis')(session);
var bodyParser      =     require('body-parser');
var cookieParser    =     require('cookie-parser');
var path            =     require("path");
var async           =     require("async");
var client          =     redis.createClient();
var app             =     express();
var router          =     express.Router();

// Always use MySQL pooling.
// Helpful for multiple connections.

var pool    =   mysql.createPool({
    connectionLimit : 100,
    host     : 'localhost',
    user     : 'root',
    password : '',
    database : 'redis_demo',
    debug    :  false
});

app.set('views', path.join(__dirname,'../','views'));
app.engine('html', require('ejs').renderFile);

// IMPORTANT
// Here we tell Express to use Redis as session store.
// We pass Redis credentials and port information.
// And express does the rest !

app.use(session({
    secret: 'ssshhhhh',
    store: new redisStore({ host: 'localhost', port: 6379, client: client,ttl :  260}),
    saveUninitialized: false,
    resave: false
}));
app.use(cookieParser("secretSign#143_!223"));
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());

// This is an important function.
// This function does the database handling task.
// We also use async here for control flow.

function handle_database(req,type,callback) {
   async.waterfall([
    function(callback) {
        pool.getConnection(function(err,connection){
          if(err) {
                   // if there is error, stop right away.
                   // This will stop the async code execution and goes to last function.
            callback(true);
          } else {
            callback(null,connection);
          }
        });
    },
    function(connection,callback) {
      var SQLquery;
      switch(type) {
       case "login" :
        SQLquery = "SELECT * from user_login WHERE user_email='"+req.body.user_email+"' AND `user_password`='"+req.body.user_password+"'";
        break;
            case "checkEmail" :
             SQLquery = "SELECT * from user_login WHERE user_email='"+req.body.user_email+"'";
            break;
        case "register" :
        SQLquery = "INSERT into user_login(user_email,user_password,user_name) VALUES ('"+req.body.user_email+"','"+req.body.user_password+"','"+req.body.user_name+"')";
        break;
        case "addStatus" :
        SQLquery = "INSERT into user_status(user_id,user_status) VALUES ("+req.session.key["user_id"]+",'"+req.body.status+"')";
        break;
        case "getStatus" :
        SQLquery = "SELECT * FROM user_status WHERE user_id="+req.session.key["user_id"];
        break;
        default :
        break;
    }
    callback(null,connection,SQLquery);
    },
    function(connection,SQLquery,callback) {
       connection.query(SQLquery,function(err,rows){
           connection.release();
        if(!err) {
            if(type === "login") {
              callback(rows.length === 0 ? false : rows[0]);
            } else if(type === "getStatus") {
                          callback(rows.length === 0 ? false : rows);
                        } else if(type === "checkEmail") {
                          callback(rows.length === 0 ? false : true);
                        } else {
                      callback(false);
            }
        } else {
             // if there is error, stop right away.
            // This will stop the async code execution and goes to last function.
            callback(true);
         }
    });
       }],
       function(result){
      // This function gets call after every async task finished.
      if(typeof(result) === "boolean" && result === true) {
        callback(null);
      } else {
        callback(result);
      }
    });
}

/**
    --- Router Code begins here.
**/


router.get('/',function(req,res){
    res.render('index.html');
});

router.post('/login',function(req,res){
    handle_database(req,"login",function(response){
        if(response === null) {
            res.json({"error" : "true","message" : "Database error occured"});
        } else {
            if(!response) {
              res.json({
                             "error" : "true",
                             "message" : "Login failed ! Please register"
                           });
            } else {
               req.session.key = response;
                   res.json({"error" : false,"message" : "Login success."});
            }
        }
    });
});

router.get('/home',function(req,res){
    if(req.session.key) {
        res.render("home.html",{ email : req.session.key["user_name"]});
    } else {
        res.redirect("/");
    }
});

router.get("/fetchStatus",function(req,res){
  if(req.session.key) {
    handle_database(req,"getStatus",function(response){
      if(!response) {
        res.json({"error" : false, "message" : "There is no status to show."});
      } else {
        res.json({"error" : false, "message" : response});
      }
    });
  } else {
    res.json({"error" : true, "message" : "Please login first."});
  }
});

router.post("/addStatus",function(req,res){
    if(req.session.key) {
      handle_database(req,"addStatus",function(response){
        if(!response) {
          res.json({"error" : false, "message" : "Status is added."});
        } else {
          res.json({"error" : false, "message" : "Error while adding Status"});
        }
      });
    } else {
      res.json({"error" : true, "message" : "Please login first."});
    }
});

router.post("/register",function(req,res){
    handle_database(req,"checkEmail",function(response){
      if(response === null) {
        res.json({"error" : true, "message" : "This email is already present"});
      } else {
        handle_database(req,"register",function(response){
          if(response === null) {
            res.json({"error" : true , "message" : "Error while adding user."});
          } else {
            res.json({"error" : false, "message" : "Registered successfully."});
          }
        });
      }
    });
});

router.get('/logout',function(req,res){
    if(req.session.key) {
    req.session.destroy(function(){
      res.redirect('/');
    });
    } else {
        res.redirect('/');
    }
});

app.use('/',router);

app.listen(3000,function(){
    console.log("I am running at 3000");
});

Explanation:

When the user provides login credentials then we are checking it against our database. If it’s successful then we are setting database response in our Redis key store. This is where Session has been started.

Now as soon as User go to homepage, we are validating the session key and if it is there, then retrieving the user id from it to fire further MySQL queries.

When user click on Logout, we are calling req.session.destroy() function which in turn deletes the key from Redis and ends the session.

Views ( Index.html and Home.html )

Here is our home page code.

/view/index.html
<html>
<head>
<title>Home</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script>
$(document).ready(function(){
    $("#username").hide();
    $('#login-submit').click(function(e){
      if($(this).attr('value') === 'Register') {
        $.post("http://localhost:3000/register",{
               user_name : $("#username").val(),
               user_email : $("#useremail").val(),
               user_password : $("#password").val()
             },function(data){
        if(data.error) {
            alert(data.message);
        } else {
            $("#username").hide();
            $("#login-submit").prop('value','Log in');
        }
    });
    } else {
        $.post("http://localhost:3000/login",{
                   user_email : $("#useremail").val(),
                   user_password : $("#password").val()
                   },function(data){
            if(!data.error) {
                window.location.href = "/home";
            } else {
                alert(data.message);
            }
        });
    }
    });
    $("#reg").click(function(event){
        $("#username").show('slow');
        $("#login-submit").prop('value','Register');
        event.preventDefault();
    });
});
</script>
    </head>
    <body>
    <nav class="navbar navbar-default navbar-fixed-top">
    <div class="navbar-header">
    <a class="navbar-brand" href="#">
        <p>Redis session demo</p>
    </a>
    </div>
  <div class="container">
    <p class="navbar-text navbar-right">Please sign in</a></p>
  </div>
</nav>
<div class="form-group" style="margin-top: 100px; width : 400px; margin-left : 50px;">
    <input type="text" id="username" placeholder="Name" class="form-control"><br>
    <input type="text" id="useremail" placeholder="Email" class="form-control"><br>
    <input type="password" id="password" placeholder="Password" class="form-control"><br>
    <input type="button" id="login-submit" value="Log In" class="btn btn-primary">&nbsp;<a href="" id="reg">Sign up here </a>
    </div>
    </body>
</html>

Here is how it looks.

Home page - Redis session

/view/home.html
<html>
<head>
<title>Home</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script type="text/javascript">
    $(document).ready(function(){
        $.get("http://localhost:3000/fetchStatus",function(res){
            $.each(res.message,function(index,value) {
                $("#showStatus").append('You have posted <br> <p>'+value.user_status+'</p><hr>');
            });
        });
    $("#addNewStatus").click(function(e){
        e.preventDefault();
            if($("#statusbox").text !== "") {
                $.post("/addStatus",
                                  { status : $("#statusbox").val() },
                                   function(res){
                    if(!res.error) {
                        alert(res.message);
                    }
                })
            }
        });
});
</script>
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
<a class="navbar-brand" href="#">
<p>Redis session demo</p>
</a></div><div class="container">
<p class="navbar-text navbar-right">Hi you are login as <b><%= email %></b> (<a href="/logout/">Logout</a>)</p>
</div>
</nav>
<div style="margin-top:100px;margin-left:50px;width:400px">
<textarea rows="10" cols="5" id="statusbox" class="form-control"></textarea><br>
<input type="submit" id="addNewStatus" value="Post" class="btn btn-primary"><br>
<div id="showStatus" style="border : 2px grey; border-radius : 4px;">
</div>
</div>
</body>
</html>

Here is how it looks.

Home page - redis session

How to run:

Download code from Github and extract the file. Make sure you have installed Redis and created database.

If your database name and MySQL password is different then please update it on bin/index.js file.

Once done type npm start on terminal to start the project.

Start the script

Now open your browser and go to localhost:3000 to view the app. Register with your email id and password and then Login with same. After login, have a look to Redis using commands mentioned above.

Redis key set

Now logout from System and check for the same key. It should not be there.

After logout

This is it. The express session with Redis is working.

Conclusion:

Redis is one of the popular key storage database systems. Using it to store session is not only beneficial in production environment but also it helps to improve the performance of the system.

Pankaj Kumar
Pankaj Kumar
Articles: 209