Auto logout with Angularjs based on idle user

2019-01-08 04:49发布

问题:

Is it possible to determine if a user is inactive and automatically log them out after say 10 minutes of inactivity using angularjs?

I was trying to avoid using jQuery, but I cannot find any tutorials or articles on how to do this in angularjs. Any help would be appreciated.

回答1:

I wrote a module called Ng-Idle that may be useful to you in this situation. Here is the page which contains instructions and a demo.

Basically, it has a service that starts a timer for your idle duration that can be disrupted by user activity (events, such as clicking, scrolling, typing). You can also manually interrupt the timeout by calling a method on the service. If the timeout is not disrupted, then it counts down a warning where you could alert the user they are going to be logged out. If they do not respond after the warning countdown reaches 0, an event is broadcasted that your application can respond to. In your case, it could issue a request to kill their session and redirect to a login page.

Additionally, it has a keep-alive service that can ping some URL at an interval. This can be used by your app to keep a user's session alive while they are active. The idle service by default integrates with the keep-alive service, suspending the pinging if they become idle, and resuming it when they return.

All the info you need to get started is on the site with more details in the wiki. However, here's a snippet of config showing how to sign them out when they time out.

angular.module('demo', ['ngIdle'])
// omitted for brevity
.config(function(IdleProvider, KeepaliveProvider) {
  IdleProvider.idle(10*60); // 10 minutes idle
  IdleProvider.timeout(30); // after 30 seconds idle, time the user out
  KeepaliveProvider.interval(5*60); // 5 minute keep-alive ping
})
.run(function($rootScope) {
    $rootScope.$on('IdleTimeout', function() {
        // end their session and redirect to login
    });
});


回答2:

View Demo which is using angularjs and see your's browser log

<!DOCTYPE html>
<html ng-app="Application_TimeOut">
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script>
</head>

<body>
</body>

<script>

var app = angular.module('Application_TimeOut', []);
app.run(function($rootScope, $timeout, $document) {    
    console.log('starting run');

    // Timeout timer value
    var TimeOutTimerValue = 5000;

    // Start a timeout
    var TimeOut_Thread = $timeout(function(){ LogoutByTimer() } , TimeOutTimerValue);
    var bodyElement = angular.element($document);

    /// Keyboard Events
    bodyElement.bind('keydown', function (e) { TimeOut_Resetter(e) });  
    bodyElement.bind('keyup', function (e) { TimeOut_Resetter(e) });    

    /// Mouse Events    
    bodyElement.bind('click', function (e) { TimeOut_Resetter(e) });
    bodyElement.bind('mousemove', function (e) { TimeOut_Resetter(e) });    
    bodyElement.bind('DOMMouseScroll', function (e) { TimeOut_Resetter(e) });
    bodyElement.bind('mousewheel', function (e) { TimeOut_Resetter(e) });   
    bodyElement.bind('mousedown', function (e) { TimeOut_Resetter(e) });        

    /// Touch Events
    bodyElement.bind('touchstart', function (e) { TimeOut_Resetter(e) });       
    bodyElement.bind('touchmove', function (e) { TimeOut_Resetter(e) });        

    /// Common Events
    bodyElement.bind('scroll', function (e) { TimeOut_Resetter(e) });       
    bodyElement.bind('focus', function (e) { TimeOut_Resetter(e) });    

    function LogoutByTimer()
    {
        console.log('Logout');

        ///////////////////////////////////////////////////
        /// redirect to another page(eg. Login.html) here
        ///////////////////////////////////////////////////
    }

    function TimeOut_Resetter(e)
    {
        console.log('' + e);

        /// Stop the pending timeout
        $timeout.cancel(TimeOut_Thread);

        /// Reset the timeout
        TimeOut_Thread = $timeout(function(){ LogoutByTimer() } , TimeOutTimerValue);
    }

})
</script>

</html>

Below code is pure javascript version

<html>
    <head>
        <script type="text/javascript">         
            function logout(){
                console.log('Logout');
            }

            function onInactive(millisecond, callback){
                var wait = setTimeout(callback, millisecond);               
                document.onmousemove = 
                document.mousedown = 
                document.mouseup = 
                document.onkeydown = 
                document.onkeyup = 
                document.focus = function(){
                    clearTimeout(wait);
                    wait = setTimeout(callback, millisecond);                       
                };
            }           
        </script>
    </head> 
    <body onload="onInactive(5000, logout);"></body>
</html>

UPDATE

I updated my solution as @Tom suggestion.

<!DOCTYPE html>
<html ng-app="Application_TimeOut">
<head>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.20/angular.min.js"></script>
</head>

<body>
</body>

<script>
var app = angular.module('Application_TimeOut', []);
app.run(function($rootScope, $timeout, $document) {    
    console.log('starting run');

    // Timeout timer value
    var TimeOutTimerValue = 5000;

    // Start a timeout
    var TimeOut_Thread = $timeout(function(){ LogoutByTimer() } , TimeOutTimerValue);
    var bodyElement = angular.element($document);

    angular.forEach(['keydown', 'keyup', 'click', 'mousemove', 'DOMMouseScroll', 'mousewheel', 'mousedown', 'touchstart', 'touchmove', 'scroll', 'focus'], 
    function(EventName) {
         bodyElement.bind(EventName, function (e) { TimeOut_Resetter(e) });  
    });

    function LogoutByTimer(){
        console.log('Logout');
        ///////////////////////////////////////////////////
        /// redirect to another page(eg. Login.html) here
        ///////////////////////////////////////////////////
    }

    function TimeOut_Resetter(e){
        console.log(' ' + e);

        /// Stop the pending timeout
        $timeout.cancel(TimeOut_Thread);

        /// Reset the timeout
        TimeOut_Thread = $timeout(function(){ LogoutByTimer() } , TimeOutTimerValue);
    }

})
</script>
</html>

Click here to see at Plunker for updated version



回答3:

There should be different ways to do it and each approach should fit a particular application better than another. For most apps, you can simply just handle key or mouse events and enable/disable a logout timer appropriately. That said, on the top of my head, a "fancy" AngularJS-y solution is monitoring the digest loop, if none has been triggered for the last [specified duration] then logout. Something like this.

app.run(function($rootScope) {
  var lastDigestRun = new Date();
  $rootScope.$watch(function detectIdle() {
    var now = new Date();
    if (now - lastDigestRun > 10*60*60) {
       // logout here, like delete cookie, navigate to login ...
    }
    lastDigestRun = now;
  });
});


回答4:

Played with Boo's approach, however don't like the fact that user got kicked off only once another digest is run, which means user stays logged in until he tries to do something within the page, and then immediatelly kicked off.

I am trying to force the logoff using interval which checks every minute if last action time was more than 30 minutes ago. I hooked it on $routeChangeStart, but could be also hooked on $rootScope.$watch as in Boo's example.

app.run(function($rootScope, $location, $interval) {

    var lastDigestRun = Date.now();
    var idleCheck = $interval(function() {
        var now = Date.now();            
        if (now - lastDigestRun > 30*60*1000) {
           // logout
        }
    }, 60*1000);

    $rootScope.$on('$routeChangeStart', function(evt) {
        lastDigestRun = Date.now();  
    });
});


回答5:

You could also accomplish using angular-activity-monitor in a more straight forward way than injecting multiple providers and it uses setInterval() (vs. angular's $interval) to avoid manually triggering a digest loop (which is important to prevent keeping items alive unintentionally).

Ultimately, you just subscribe to a few events that determine when a user is inactive or becoming close. So if you wanted to log out a user after 10 minutes of inactivity, you could use the following snippet:

angular.module('myModule', ['ActivityMonitor']);

MyController.$inject = ['ActivityMonitor'];
function MyController(ActivityMonitor) {
  // how long (in seconds) until user is considered inactive
  ActivityMonitor.options.inactive = 600;

  ActivityMonitor.on('inactive', function() {
    // user is considered inactive, logout etc.
  });

  ActivityMonitor.on('keepAlive', function() {
    // items to keep alive in the background while user is active
  });

  ActivityMonitor.on('warning', function() {
    // alert user when they're nearing inactivity
  });
}


回答6:

I tried out Buu's approach and couldn't get it quite right due to the sheer number of events that trigger the digester to execute, including $interval and $timeout functions executing. This leaves the application in a state where it never be idle regardless of user input.

If you actually need to track user idle time I am not sure that there is a good angular approach. I would suggest that a better approach is represented by Witoldz here https://github.com/witoldsz/angular-http-auth. This approach will prompt the user to reauthenticate when an action is taken that requires their credentials. After the user has authenticated the previous failed request is reprocessed and the application continues on as if nothing happened.

This handles the concern that you might have of letting the user's session expire while they are active since even if their authentication expires they are still able to retain the application state and not lose any work.

If you have some kind of session on your client (cookies, tokens, etc) you could watch them as well and trigger your logout process if they expire.

app.run(['$interval', function($interval) {
  $interval(function() {
    if (/* session still exists */) {
    } else {
      // log out of client
    }
  }, 1000);
}]);

UPDATE: Here is a plunk that demonstrates the concern. http://plnkr.co/edit/ELotD8W8VAeQfbYFin1W. What this demonstates is that the digester run time is updated only when the interval ticks. Once the interval reaches it max count then the digester will no longer run.



回答7:

ng-Idle looks like the way to go, but I could not figure out Brian F's modifications and wanted to timeout for a sleeping session too, also I had a pretty simple use case in mind. I pared it down to the code below. It hooks events to reset a timeout flag (lazily placed in $rootScope). It only detects the timeout has happened when the user returns (and triggers an event) but that's good enough for me. I could not get angular's $location to work here but again, using document.location.href gets the job done.

I stuck this in my app.js after the .config has run.

app.run(function($rootScope,$document) 
{
  var d = new Date();
  var n = d.getTime();  //n in ms

    $rootScope.idleEndTime = n+(20*60*1000); //set end time to 20 min from now
    $document.find('body').on('mousemove keydown DOMMouseScroll mousewheel mousedown touchstart', checkAndResetIdle); //monitor events

    function checkAndResetIdle() //user did something
    {
      var d = new Date();
      var n = d.getTime();  //n in ms

        if (n>$rootScope.idleEndTime)
        {
            $document.find('body').off('mousemove keydown DOMMouseScroll mousewheel mousedown touchstart'); //un-monitor events

            //$location.search('IntendedURL',$location.absUrl()).path('/login'); //terminate by sending to login page
            document.location.href = 'https://whatever.com/myapp/#/login';
            alert('Session ended due to inactivity');
        }
        else
        {
            $rootScope.idleEndTime = n+(20*60*1000); //reset end time
        }
    }
});


回答8:

I think Buu's digest cycle watch is genius. Thanks for sharing. As others have noted $interval also causes the digest cycle to run. We could for the purpose of auto logging the user out use setInterval which will not cause a digest loop.

app.run(function($rootScope) {
    var lastDigestRun = new Date();
    setInterval(function () {
        var now = Date.now();
        if (now - lastDigestRun > 10 * 60 * 1000) {
          //logout
        }
    }, 60 * 1000);

    $rootScope.$watch(function() {
        lastDigestRun = new Date();
    });
});


回答9:

I have used ng-idle for this and added a little logout and token null code and it is working fine, you can try this. Thanks @HackedByChinese for making such a nice module.

In IdleTimeout i have just deleted my session data and token.

Here is my code

$scope.$on('IdleTimeout', function () {
        closeModals();
        delete $window.sessionStorage.token;
        $state.go("login");
        $scope.timedout = $uibModal.open({
            templateUrl: 'timedout-dialog.html',
            windowClass: 'modal-danger'
        });
    });


回答10:

I would like to expand the answers to whoever might be using this in a bigger project, you could accidentally attach multiple event handlers and the program would behave weirdly.

To get rid of that, I used a singleton function exposed by a factory, from which you would call inactivityTimeoutFactory.switchTimeoutOn() and inactivityTimeoutFactory.switchTimeoutOff() in your angular application to respectively activate and deactivate the logout due to inactivity functionality.

This way you make sure you are only running a single instance of the event handlers, no matter how many times you try to activate the timeout procedure, making it easier to use in applications where the user might login from different routes.

Here is my code:

'use strict';

angular.module('YOURMODULENAME')
  .factory('inactivityTimeoutFactory', inactivityTimeoutFactory);

inactivityTimeoutFactory.$inject = ['$document', '$timeout', '$state'];

function inactivityTimeoutFactory($document, $timeout, $state)  {
  function InactivityTimeout () {
    // singleton
    if (InactivityTimeout.prototype._singletonInstance) {
      return InactivityTimeout.prototype._singletonInstance;
    }
    InactivityTimeout.prototype._singletonInstance = this;

    // Timeout timer value
    const timeToLogoutMs = 15*1000*60; //15 minutes
    const timeToWarnMs = 13*1000*60; //13 minutes

    // variables
    let warningTimer;
    let timeoutTimer;
    let isRunning;

    function switchOn () {
      if (!isRunning) {
        switchEventHandlers("on");
        startTimeout();
        isRunning = true;
      }
    }

    function switchOff()  {
      switchEventHandlers("off");
      cancelTimersAndCloseMessages();
      isRunning = false;
    }

    function resetTimeout() {
      cancelTimersAndCloseMessages();
      // reset timeout threads
      startTimeout();
    }

    function cancelTimersAndCloseMessages () {
      // stop any pending timeout
      $timeout.cancel(timeoutTimer);
      $timeout.cancel(warningTimer);
      // remember to close any messages
    }

    function startTimeout () {
      warningTimer = $timeout(processWarning, timeToWarnMs);
      timeoutTimer = $timeout(processLogout, timeToLogoutMs);
    }

    function processWarning() {
      // show warning using popup modules, toasters etc...
    }

    function processLogout() {
      // go to logout page. The state might differ from project to project
      $state.go('authentication.logout');
    }

    function switchEventHandlers(toNewStatus) {
      const body = angular.element($document);
      const trackedEventsList = [
        'keydown',
        'keyup',
        'click',
        'mousemove',
        'DOMMouseScroll',
        'mousewheel',
        'mousedown',
        'touchstart',
        'touchmove',
        'scroll',
        'focus'
      ];

      trackedEventsList.forEach((eventName) => {
        if (toNewStatus === 'off') {
          body.off(eventName, resetTimeout);
        } else if (toNewStatus === 'on') {
          body.on(eventName, resetTimeout);
        }
      });
    }

    // expose switch methods
    this.switchOff = switchOff;
    this.switchOn = switchOn;
  }

  return {
    switchTimeoutOn () {
      (new InactivityTimeout()).switchOn();
    },
    switchTimeoutOff () {
      (new InactivityTimeout()).switchOff();
    }
  };

}