2

I'm currently making an app on Laravel (5.3) and require a list of all logged-in users. I was originally going to store them in MySQL but was advised that Redis would be better for the job. Once I looked at Redis docs I was going to store all the users in a set, but then realised you can't set an expire time on individual members, and so opted for namespaced strings instead.

I have written some code that I believe is functioning correctly, but would like advice on improving it/fixing any problems there might be.

So first, here are the two functions I added in LoginController.php

// Overriding the authenticated method from  Illuminate\Foundation\Auth\AuthenticatesUsers
protected function authenticated(Request $request, $user)
{
    $id = $user->id;

    // Getting the expiration from the session config file. Converting to seconds
    $expire = config('session.lifetime') * 60;

    // Setting redis using id as namespace and value
    Redis::SET('users:'.$id,$id);
    Redis::EXPIRE('users:'.$id,$expire);
}

//Overriding the logout method from Illuminate\Foundation\Auth\AuthenticatesUsers
public function logout(Request $request)
{
    // Deleting user from redis database when they log out
    $id = Auth::user()->id;
    Redis::DEL('users:'.$id);

    $this->guard()->logout();

    $request->session()->flush();

    $request->session()->regenerate();

    return redirect('/');
}

Next I wrote Middleware called 'RefreshRedis' in order to refresh the expiration on the Redis when the user does something that refreshes their session.

public function handle($request, Closure $next)
{
    //refreshing the expiration of users key
    if(Auth::check()){
        $id = Auth::user()->id;
        $expire = config('session.lifetime') * 60;
        Redis::EXPIRE('users:'.$id,$expire);
    }
    return $next($request);
}

I then registered the Middleware in $middlewareGroups just after the StartSession Middleware

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \App\Http\Middleware\RefreshRedis::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

In order to get a list of all the users I used a modified version of the function found in this thread.

class Team extends AbstractWidget
{

/**
 * Treat this method as a controller action.
 * Return view() or other content to display.
 */
public function run()
{
    //Find all logged users id's from redis
    $users = $this->loggedUsers('users:*');

    return view('widgets.team',compact('users'));
}


protected function loggedUsers($pattern, $cursor=null, $allResults=array())
{
    // Zero means full iteration
    if ($cursor==="0"){
        $users = array ();

        foreach($allResults as $result){
            $users[] = User::where('id',Redis::Get($result))->first();
        }
        return $users;
    }

    // No $cursor means init
    if ($cursor===null){
        $cursor = "0";
    }

    // The call
    $result = Redis::scan($cursor, 'match', $pattern);

    // Append results to array
    $allResults = array_merge($allResults, $result[1]);

    // Get rid of duplicated values
    $allResults = array_unique($allResults);

    // Recursive call until cursor is 0
    return $this->loggedUsers($pattern, $result[0], $allResults);
}
}
Community
  • 1
  • 1
Dalek
  • 111
  • 4
  • 12
  • 1
    When a user's session expires their login automatically expires. So there is no reason for you to implement this logic. You can config this in `config/session.php` under `'lifetime' => 120` default is 2 hours – WebKenth Feb 27 '17 at 14:12
  • Wait, how would I get a list of all currently logged-in users without this logic? – Dalek Feb 27 '17 at 14:17
  • 1
    Not saying you can't make a list of active users, just saying you don't need to write the experiration logic. You only need to add a user when they login and remove them when their session expires, then you can freely at any time query the list to get active users. – WebKenth Feb 27 '17 at 14:20
  • Sounds good, how can I remove them when their session expires? As in, where would that code go? – Dalek Feb 27 '17 at 14:23
  • Been digging and i would have thought laravel would broadcast an event, but sadly it doesn't. However you could attach your expire logic to a middleware that updates whenever a user sends a request to your server. So when a user performs a login, you add them via Redis and set the expiration to `config('session.lifetime')`and when they pass through your middleware you update their expiration :) This will leave you with a list that only contains users who have been active – WebKenth Feb 27 '17 at 14:35
  • Thank's for the help, but isn't that literally what i'm doing right now? Or are you saying my Middleware is in the wrong place? – Dalek Feb 27 '17 at 14:39

1 Answers1

1

You can expire Redis set keys by using the [multi] command that would finalize certain Redis command(s) by using the [exec] command.

protected function authenticated(Request $request, $user)
{
     $id = $user->id;

    // Getting the expiration from the session config file. Converting      to seconds
    $expire = config('session.lifetime') * 60;

    // In using the [multi] and [exec] command
    $oLibRedis = $this->callRedisLibrary(); // Initialize your Predis\Client class on this method
    $oLibRedis->multi();
    $oLibRedis->set('users:'.$id, $id);
    $oLibRedis->expire('users:'.$id, $expire);
    $oLibRedis->get('users:'.$id);
    $aResults = $oLibRedis->exec();
    return $aResults
 }

from which it would generate the ff. results

array(3) { [0]=> object(Predis\Response\Status)#224 (1) { ["payload":"Predis\Response\Status":private]=> string(2) "OK" } [1]=> int(1) [2]=> string(5) "admin" }

The results will show up as an array based on what commands you have called so far on Redis (e.g set, expire and get commands)

By the time you wanted to check if the session is expired or not from another controller. You can use the [ttl] command in Redis.

$sRedisUserKey = $oLibRedis->ttl($sSampleUserKey); // int(7184)

Try to refresh the page to check the countdown for it. Hope this helps for you

Marylyn Lajato
  • 1,171
  • 1
  • 10
  • 15