connect-redis - how to protect the session object against race condition

梦想的初衷 提交于 2019-12-21 05:26:06

问题


I'm using nodejs with connect-redis to store the session data.

I'm saving user data in the session, and use it for in the session lifetime.

I've noticed that it's possible to have a race condition between two requests that changes the session data.

I've tried to use redis-lock to lock the session, but it's a bit problematic for me.

i don't want to lock the entire session, but instead lock only specific session variable.

I found it to be impossible, and I thought about direction to solve it:

to stop using the session object to store user data, and save the variable directly in the redis and lock before using it.

I know that it can work, but it will require me to manage all the objects manually instead of just accessing redis through the session object.

Can you please share with me the best practice and your suggestions?

Thanks, Lior


回答1:


Well, implementing your own storage might be the option for you. This documentation shows that all you need to do is to implement three methods: .get, .set and .destroy (see the last paragraph). It would be something like this (using node-redis library and modifying the original connect-redis store a bit):

var redis = require("redis"),
    redis_client = redis.createClient(),
    session_prefix = 'session::',
    lock_suffix = '::lock',
    threshold = 5000,
    wait_time = 250,
    oneDay = 86400;

/* If timeout is greater then threshold, then we assume that
   one of the Redis Clients is dead and he cannot realese
   the lock. */

function CustomSessionStore(opts) {
    opts = opts || {};
    var self = this;
    self.ttl = opts.ttl; // <---- used for setting timeout on session

    self.lock = function(sid, callback) {
        callback = callback || function(){};
        var key = session_prefix + sid + lock_suffix;
        // try setting the lock with current Date
        redis_client.setnx(key, Date.now( ), function(err, res) {
            // some error handling?
            if (res) {
                // Everything's fine, call callback.
                callback();
                return;
            }

            // setnx failed, look at timeout
            redis_client.get(key, function(err, res) {
                // some error handling?
                if (parseInt(res) + threshold > Date.now( )) {
                    // timeout, release the old lock and lock it
                    redis_client.getset(key, Date.now( ), function(err, date) {
                        if (parseInt(date) + threshold > Date.now()) {
                            // ups, some one else was faster in acquiring lock
                            setTimeout(function() {
                                self.lock(sid, callback);
                            }, wait_time);
                            return;
                        }
                        callback();
                    });
                    return;
                }
                // it is not time yet, wait and try again later
                setTimeout(function() {
                    self.lock(sid, callback);
                }, wait_time);
            });
        });
    };

    self.unlock = function(sid, callback) {
        callback = callback || function(){};
        var key = session_prefix + sid + lock_suffix;
        redis_client.del(key, function(err) {
            // some error handling?
            callback();
        });
    };

    self.get = function(sid, callback) {
        callback = callback || function(){};
        var key = session_prefix + sid;
        // lock the session
        self.lock(sid, function() {
            redis_client.get(key, function(err, data) {
                if (err) {
                    callback(err);
                    return;
                }
                try {
                    callback(null, JSON.parse(data));
                } catch(e) {
                    callback(e);
                }
            });
        });
    };

    self.set = function(sid, data, callback) {
        callback = callback || function(){};
        try {
            // ttl used for expiration of session
            var maxAge = sess.cookie.maxAge
              , ttl = self.ttl
              , sess = JSON.stringify(sess);

            ttl = ttl || ('number' == typeof maxAge
                  ? maxAge / 1000 | 0
                  : oneDay);

        } catch(e) {
            callback(e);
            return;
        }
        var key = session_prefix + sid;
        redis_client.setex(key, ttl, data, function(err) {
            // unlock the session
            self.unlock(sid, function(_err) {
                callback(err || _err);
            });
        });
    };

    self.destroy = function(sid, callback) {
        var key = session_prefix + sid;
        redis_client.del(key, function(err) {
            redis_client.unlock(sid, function(_err) {
                callback(err || _err);
            });
        });
    };
}

Side note: I didn't implement error handling for .lock and .unlock. I'm leaving this up to you! :) There might be some minor mistakes (I don't have NodeJS at the moment and I'm writing this from my memory :D ), but you should understand the idea. Here's the link which contains the discussion about how to use setnx for locking/unlocking Redis.

The other note: you would probably want to make some custom error handling for routes, because if any route throws an exception, then Redis session won't be unlocked. The .set method is always called as the last thing in route - opposite to .get method which Express calls at the very begining of the route (that's why I lock at .get and unlock at .set). Still you will be locked only for 5 seconds, so it does not have to be a problem though. Remember to tune it to your needs (especially threshold and wait_time variables).

Final note: with this mechanism your request handlers will only fire one after another per user. This means, that you will not be able to run concurrent handlers per user. This might be a problem, so the other idea is to held the data outside the session and handle locking/unlocking manually. After all, there are some things which have to be handled manually.

I hope it helps! Good luck!



来源:https://stackoverflow.com/questions/11420982/connect-redis-how-to-protect-the-session-object-against-race-condition

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!