如何在执行ui-router解析之前推迟StateChangeStart事件

时间:2021-08-25 12:30:25

I have an angular app that uses authorization logic and ui-router to bar unauthorized users for certain states/views. I follow the standard approach of listening for a stateChange event, which triggers my authorization logic. This all works well until the dreaded page re-load.

我有一个角度应用程序,它使用授权逻辑和ui-router来禁止未经授权的用户访问某些状态/视图。我遵循监听stateChange事件的标准方法,该事件触发我的授权逻辑。这一切都很好,直到可怕的页面重新加载。

I store session data (including authorization status) in local storage so that on page reloads I can use a parent state in ui-router to first resolve/get the authorization status from local storage prior to attempting to change views. Here is the configuration of my app parent state object:

我将会话数据(包括授权状态)存储在本地存储中,以便在页面重新加载时,我可以使用ui-router中的父状态在尝试更改视图之前首先从本地存储中解析/获取授权状态。这是我的应用程序父状态对象的配置:

$stateProvider.
state('app', {
  url: '/app',
  abstract: true,
  controller: 'appCtrl',
  data: {
    authorizedRoles: [USER_ROLES.all]
  },
  templateUrl: 'partials/app.html',
  resolve: {

    //Try to restore from the previous session before loading any of the app child states
    RestoredSession: ['SessionService',
             function(SessionService){
                return SessionService.restoreSession();
              }]
  }
})

...various app. child states

And here is my onStateChange listener:

这是我的onStateChange监听器:

//listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser
//is authorized to view that page

.run(         ['$rootScope', 'AUTH_EVENTS', 'SessionService', 
  function ($rootScope,   AUTH_EVENTS,   SessionService) {

  $rootScope.$on('$stateChangeStart', function (event, next) {
    var authorizedRoles = next.data.authorizedRoles;
    //If the requested page allows guest access, then continue to stateChange
    if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return;

    //If the requested page requires authorization, check login and auth privileges
    if (!SessionService.isAuthorized(authorizedRoles)) {

      event.preventDefault();

      if (SessionService.existingSession()) {

        // user is not allowed
        $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
        console.log("User attempted to access page for which he is not authorized");

      } else {

        // user is not logged in
        $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn);
        console.log("User attempted to access page when he is not logged in");

      }
    }
  });

}]);

My problem is that the stateChangeStart event is triggering prior to the app resolve such that the listener stops the state change (via the event.preventDefault), and then my resolve loads the stored session data, which often establishes that the user was authorized all along. If I could require execution of the resolve prior to the event triggering then I'd be golden.

我的问题是stateChangeStart事件在应用程序解析之前触发,以便侦听器停止状态更改(通过event.preventDefault),然后我的解析加载存储的会话数据,这通常确定用户一直被授权。如果我可以在事件触发之前要求执行解决方案,那么我就是金色的。

Any ideas out there???

有什么想法吗?

BTW, here is a similar SO question that went unanswered: Defer Angular UI Router $stateChangeStart until server authorization response receieved

顺便说一句,这是一个类似的SO问题没有答案:推迟Angular UI路由器$ stateChangeStart直到服务器授权响应收到

4 个解决方案

#1


1  

Turns out that all I needed to do was move the loading of config data to the .run() block instead of trying to do it in the parent app state's resolve.

事实证明,我需要做的就是将配置数据加载到.run()块,而不是尝试在父应用程序状态的解析中执行此操作。

//listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser
//is authorized to view that page
.run(         ['$rootScope', 'AUTH_EVENTS','SessionService', 'localStorageService',
  function ($rootScope,   AUTH_EVENTS,  SessionService,   localStorageService) 
  {
  $rootScope.$on('$stateChangeStart', function (event, next) {

    //function to check to see if the currentUser has one of the required roles to authorize the next state.
    var checkAuthorization = function(authorizedRoles){

         //If the requested page allows guest access, then continue to stateChange
         if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return;
         //If the requested page requires authorization, check login and auth privileges
         if (!SessionService.isAuthorized(authorizedRoles)) {
           event.preventDefault();
           if (SessionService.existingSession()) {
             // user is not allowed
             $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
             console.log("User attempted to access page for which he is not authorized");
           } else {
             // user is not logged in
             $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn);
             console.log("User attempted to access page when he is not logged in");
           }
         }
       };

    //Before calling checkAuthorization(), test to see if the state change was triggered by a reload
    //If so, load config data before triggering the `checkAuthorization()` function.
    if (SessionService.freshLoad === true  || typeof SessionService.freshLoad === 'undefined'){
      SessionService.freshLoad = false;
      var storedUser = localStorageService.get('currentUser');

      //If we have a stored user but no existing session, then we know that we have stored
      //user data to reload before the checkAuthorization() function.
      if (typeof storedUser !== "undefined" && storedUser !== null && !SessionService.existingSession()) {
        SessionService.restoreSession();
      }
    }

  checkAuthorization(next.data.authorizedRoles);

  });

}]);

#2


0  

I have found a good way of resolving a data asynchronously during $stateChangeStart in another answer here. Here is the code:

我在$ stateChangeStart中找到了一种在异步中异步解析数据的好方法。这是代码:

rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) {

    if (dataService.isInitialized()) {
        proceedAsUsual();
    } 
    else {

        event.preventDefault();

        dataService.intialize().success(function () {
                $state.go(toState, toParams);
        });
    }
});

Then you can just remember that your data is already initialized in the service the way you like, e.g.:

然后您可以记住,您的数据已经按照您喜欢的方式在服务中初始化,例如:

function dataService() {

    var initialized = false;

    return {
        initialize: initialize,
        isInitialized: isInitialized
    }

    function intialize() {

        return $http.get(...)
                    .success(function(response) {
                            initialized=true;
                    });

    }

    function isInitialized() {
        return initialized;
    }
};

#3


0  

Coming in a little late here, but I think this will help.

在这里来晚了一点,但我认为这会有所帮助。

The $on method returns a deregistration function for the listener. This allows cancellation of the event prior to custom handling in the listener.

$ on方法返回侦听器的注销函数。这允许在侦听器中进行自定义处理之前取消事件。

var setInterceptedListener = function($scope) {
    var removeListener = $rootScope.$on('$stateChangeStart',
        function (event, toState, toParams, fromState, fromParams) {   
            // cancel state change
            event.preventDefault();

            // mock prompt for user input
            Prompt.continue('Continue?').then(function(result) {
                // if yes then deregister the listener in order to proceed.
                if (result == 'yes') {
                    removeListener();
                    $state.go(toState, toParams);
                }
            });
        });

        // deregister on scope teardown
        $scope.$on("$destroy", removeListener);
    };

To use this, simple add this method to a service and call setInterceptedListener($scope).

要使用它,只需将此方法添加到服务并调用setInterceptedListener($ scope)。

#4


-1  

This is client side security which you can implement in regular Angular versions. I have tried and tested this. (Please find my article here:- http://www.codeproject.com/Tips/811782/AngularJS-Routing-Security ). In addition to client side route security, you need to secure access at server side also. Client side security helps in avoiding extra round trip to server. However, if someone tricks the browser , then server server side security should be able to reject unauthorized access.

这是客户端安全性,您可以在常规Angular版本中实现。我试过并试过这个。 (请在这里找到我的文章: - http://www.codeproject.com/Tips/811782/AngularJS-Routing-Security)。除了客户端路由安全性之外,您还需要在服务器端安全访问。客户端安全性有助于避免额外的服务器往返。但是,如果有人欺骗浏览器,那么服务器端安全性应该能够拒绝未经授权的访问。

Hope this helps!

希望这可以帮助!

Step 1: Define Global variables in app-module

第1步:在app-module中定义全局变量

-define roles for the application

- 定义应用程序的角色

  var roles = {
        superUser: 0,
        admin: 1,
        user: 2
    };

-Define route For Unauthorized Access for the application

- 针对应用程序的未授权访问的定义路由

 var routeForUnauthorizedAccess = '/SomeAngularRouteForUnauthorizedAccess';

Step 2: Define the service for authorization

第2步:定义授权服务

appModule.factory('authorizationService', function ($resource, $q, $rootScope, $location) {
    return {
    // We would cache the permission for the session, to avoid roundtrip to server for subsequent requests
    permissionModel: { permission: {}, isPermissionLoaded: false  },

    permissionCheck: function (roleCollection) {
    // we will return a promise .
            var deferred = $q.defer();

    //this is just to keep a pointer to parent scope from within promise scope.
            var parentPointer = this;

    //Checking if permisison object(list of roles for logged in user) is already filled from service
            if (this.permissionModel.isPermissionLoaded) {

    //Check if the current user has required role to access the route
                    this.getPermission(this.permissionModel, roleCollection, deferred);
} else {
    //if permission is not obtained yet, we will get it from  server.
    // 'api/permissionService' is the path of server web service , used for this example.

                    $resource('/api/permissionService').get().$promise.then(function (response) {
    //when server service responds then we will fill the permission object
                    parentPointer.permissionModel.permission = response;

    //Indicator is set to true that permission object is filled and can be re-used for subsequent route request for the session of the user
                    parentPointer.permissionModel.isPermissionLoaded = true;

    //Check if the current user has required role to access the route
                    parentPointer.getPermission(parentPointer.permissionModel, roleCollection, deferred);
}
                );
}
            return deferred.promise;
},

        //Method to check if the current user has required role to access the route
        //'permissionModel' has permission information obtained from server for current user
        //'roleCollection' is the list of roles which are authorized to access route
        //'deferred' is the object through which we shall resolve promise
    getPermission: function (permissionModel, roleCollection, deferred) {
        var ifPermissionPassed = false;

        angular.forEach(roleCollection, function (role) {
            switch (role) {
                case roles.superUser:
                    if (permissionModel.permission.isSuperUser) {
                        ifPermissionPassed = true;
                    }
                    break;
                case roles.admin:
                    if (permissionModel.permission.isAdministrator) {
                        ifPermissionPassed = true;
                    }
                    break;
                case roles.user:
                    if (permissionModel.permission.isUser) {
                        ifPermissionPassed = true;
                    }
                    break;
                default:
                    ifPermissionPassed = false;
            }
        });
        if (!ifPermissionPassed) {
            //If user does not have required access, we will route the user to unauthorized access page
            $location.path(routeForUnauthorizedAccess);
            //As there could be some delay when location change event happens, we will keep a watch on $locationChangeSuccess event
            // and would resolve promise when this event occurs.
            $rootScope.$on('$locationChangeSuccess', function (next, current) {
                deferred.resolve();
            });
        } else {
            deferred.resolve();
        }
    }

};
});

Step 3: Use security in routing: Lets use use all our hardword done so far, to secure the routes

步骤3:在路由中使用安全性:让我们使用到目前为止所做的所有硬编码来保护路由

var appModule = angular.module("appModule", ['ngRoute', 'ngResource'])
    .config(function ($routeProvider, $locationProvider) {
        $routeProvider
            .when('/superUserSpecificRoute', {
                templateUrl: '/templates/superUser.html',//path of the view/template of route
                caseInsensitiveMatch: true,
                controller: 'superUserController',//angular controller which would be used for the route
                resolve: {//Here we would use all the hardwork we have done above and make call to the authorization Service 
                    //resolve is a great feature in angular, which ensures that a route controller(in this case superUserController ) is invoked for a route only after the promises mentioned under it are resolved.
                    permission: function(authorizationService, $route) {
                        return authorizationService.permissionCheck([roles.superUser]);
                    },
                }
            })
        .when('/userSpecificRoute', {
            templateUrl: '/templates/user.html',
            caseInsensitiveMatch: true,
            controller: 'userController',
            resolve: {
                permission: function (authorizationService, $route) {
                    return authorizationService.permissionCheck([roles.user]);
                },
            }
           })
             .when('/adminSpecificRoute', {
                 templateUrl: '/templates/admin.html',
                 caseInsensitiveMatch: true,
                 controller: 'adminController',
                 resolve: {
                     permission: function(authorizationService, $route) {
                         return authorizationService.permissionCheck([roles.admin]);
                     },
                 }
             })
             .when('/adminSuperUserSpecificRoute', {
                 templateUrl: '/templates/adminSuperUser.html',
                 caseInsensitiveMatch: true,
                 controller: 'adminSuperUserController',
                 resolve: {
                     permission: function(authorizationService, $route) {
                         return authorizationService.permissionCheck([roles.admin,roles.superUser]);
                     },
                 }
             })
    });

#1


1  

Turns out that all I needed to do was move the loading of config data to the .run() block instead of trying to do it in the parent app state's resolve.

事实证明,我需要做的就是将配置数据加载到.run()块,而不是尝试在父应用程序状态的解析中执行此操作。

//listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser
//is authorized to view that page
.run(         ['$rootScope', 'AUTH_EVENTS','SessionService', 'localStorageService',
  function ($rootScope,   AUTH_EVENTS,  SessionService,   localStorageService) 
  {
  $rootScope.$on('$stateChangeStart', function (event, next) {

    //function to check to see if the currentUser has one of the required roles to authorize the next state.
    var checkAuthorization = function(authorizedRoles){

         //If the requested page allows guest access, then continue to stateChange
         if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return;
         //If the requested page requires authorization, check login and auth privileges
         if (!SessionService.isAuthorized(authorizedRoles)) {
           event.preventDefault();
           if (SessionService.existingSession()) {
             // user is not allowed
             $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
             console.log("User attempted to access page for which he is not authorized");
           } else {
             // user is not logged in
             $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn);
             console.log("User attempted to access page when he is not logged in");
           }
         }
       };

    //Before calling checkAuthorization(), test to see if the state change was triggered by a reload
    //If so, load config data before triggering the `checkAuthorization()` function.
    if (SessionService.freshLoad === true  || typeof SessionService.freshLoad === 'undefined'){
      SessionService.freshLoad = false;
      var storedUser = localStorageService.get('currentUser');

      //If we have a stored user but no existing session, then we know that we have stored
      //user data to reload before the checkAuthorization() function.
      if (typeof storedUser !== "undefined" && storedUser !== null && !SessionService.existingSession()) {
        SessionService.restoreSession();
      }
    }

  checkAuthorization(next.data.authorizedRoles);

  });

}]);

#2


0  

I have found a good way of resolving a data asynchronously during $stateChangeStart in another answer here. Here is the code:

我在$ stateChangeStart中找到了一种在异步中异步解析数据的好方法。这是代码:

rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) {

    if (dataService.isInitialized()) {
        proceedAsUsual();
    } 
    else {

        event.preventDefault();

        dataService.intialize().success(function () {
                $state.go(toState, toParams);
        });
    }
});

Then you can just remember that your data is already initialized in the service the way you like, e.g.:

然后您可以记住,您的数据已经按照您喜欢的方式在服务中初始化,例如:

function dataService() {

    var initialized = false;

    return {
        initialize: initialize,
        isInitialized: isInitialized
    }

    function intialize() {

        return $http.get(...)
                    .success(function(response) {
                            initialized=true;
                    });

    }

    function isInitialized() {
        return initialized;
    }
};

#3


0  

Coming in a little late here, but I think this will help.

在这里来晚了一点,但我认为这会有所帮助。

The $on method returns a deregistration function for the listener. This allows cancellation of the event prior to custom handling in the listener.

$ on方法返回侦听器的注销函数。这允许在侦听器中进行自定义处理之前取消事件。

var setInterceptedListener = function($scope) {
    var removeListener = $rootScope.$on('$stateChangeStart',
        function (event, toState, toParams, fromState, fromParams) {   
            // cancel state change
            event.preventDefault();

            // mock prompt for user input
            Prompt.continue('Continue?').then(function(result) {
                // if yes then deregister the listener in order to proceed.
                if (result == 'yes') {
                    removeListener();
                    $state.go(toState, toParams);
                }
            });
        });

        // deregister on scope teardown
        $scope.$on("$destroy", removeListener);
    };

To use this, simple add this method to a service and call setInterceptedListener($scope).

要使用它,只需将此方法添加到服务并调用setInterceptedListener($ scope)。

#4


-1  

This is client side security which you can implement in regular Angular versions. I have tried and tested this. (Please find my article here:- http://www.codeproject.com/Tips/811782/AngularJS-Routing-Security ). In addition to client side route security, you need to secure access at server side also. Client side security helps in avoiding extra round trip to server. However, if someone tricks the browser , then server server side security should be able to reject unauthorized access.

这是客户端安全性,您可以在常规Angular版本中实现。我试过并试过这个。 (请在这里找到我的文章: - http://www.codeproject.com/Tips/811782/AngularJS-Routing-Security)。除了客户端路由安全性之外,您还需要在服务器端安全访问。客户端安全性有助于避免额外的服务器往返。但是,如果有人欺骗浏览器,那么服务器端安全性应该能够拒绝未经授权的访问。

Hope this helps!

希望这可以帮助!

Step 1: Define Global variables in app-module

第1步:在app-module中定义全局变量

-define roles for the application

- 定义应用程序的角色

  var roles = {
        superUser: 0,
        admin: 1,
        user: 2
    };

-Define route For Unauthorized Access for the application

- 针对应用程序的未授权访问的定义路由

 var routeForUnauthorizedAccess = '/SomeAngularRouteForUnauthorizedAccess';

Step 2: Define the service for authorization

第2步:定义授权服务

appModule.factory('authorizationService', function ($resource, $q, $rootScope, $location) {
    return {
    // We would cache the permission for the session, to avoid roundtrip to server for subsequent requests
    permissionModel: { permission: {}, isPermissionLoaded: false  },

    permissionCheck: function (roleCollection) {
    // we will return a promise .
            var deferred = $q.defer();

    //this is just to keep a pointer to parent scope from within promise scope.
            var parentPointer = this;

    //Checking if permisison object(list of roles for logged in user) is already filled from service
            if (this.permissionModel.isPermissionLoaded) {

    //Check if the current user has required role to access the route
                    this.getPermission(this.permissionModel, roleCollection, deferred);
} else {
    //if permission is not obtained yet, we will get it from  server.
    // 'api/permissionService' is the path of server web service , used for this example.

                    $resource('/api/permissionService').get().$promise.then(function (response) {
    //when server service responds then we will fill the permission object
                    parentPointer.permissionModel.permission = response;

    //Indicator is set to true that permission object is filled and can be re-used for subsequent route request for the session of the user
                    parentPointer.permissionModel.isPermissionLoaded = true;

    //Check if the current user has required role to access the route
                    parentPointer.getPermission(parentPointer.permissionModel, roleCollection, deferred);
}
                );
}
            return deferred.promise;
},

        //Method to check if the current user has required role to access the route
        //'permissionModel' has permission information obtained from server for current user
        //'roleCollection' is the list of roles which are authorized to access route
        //'deferred' is the object through which we shall resolve promise
    getPermission: function (permissionModel, roleCollection, deferred) {
        var ifPermissionPassed = false;

        angular.forEach(roleCollection, function (role) {
            switch (role) {
                case roles.superUser:
                    if (permissionModel.permission.isSuperUser) {
                        ifPermissionPassed = true;
                    }
                    break;
                case roles.admin:
                    if (permissionModel.permission.isAdministrator) {
                        ifPermissionPassed = true;
                    }
                    break;
                case roles.user:
                    if (permissionModel.permission.isUser) {
                        ifPermissionPassed = true;
                    }
                    break;
                default:
                    ifPermissionPassed = false;
            }
        });
        if (!ifPermissionPassed) {
            //If user does not have required access, we will route the user to unauthorized access page
            $location.path(routeForUnauthorizedAccess);
            //As there could be some delay when location change event happens, we will keep a watch on $locationChangeSuccess event
            // and would resolve promise when this event occurs.
            $rootScope.$on('$locationChangeSuccess', function (next, current) {
                deferred.resolve();
            });
        } else {
            deferred.resolve();
        }
    }

};
});

Step 3: Use security in routing: Lets use use all our hardword done so far, to secure the routes

步骤3:在路由中使用安全性:让我们使用到目前为止所做的所有硬编码来保护路由

var appModule = angular.module("appModule", ['ngRoute', 'ngResource'])
    .config(function ($routeProvider, $locationProvider) {
        $routeProvider
            .when('/superUserSpecificRoute', {
                templateUrl: '/templates/superUser.html',//path of the view/template of route
                caseInsensitiveMatch: true,
                controller: 'superUserController',//angular controller which would be used for the route
                resolve: {//Here we would use all the hardwork we have done above and make call to the authorization Service 
                    //resolve is a great feature in angular, which ensures that a route controller(in this case superUserController ) is invoked for a route only after the promises mentioned under it are resolved.
                    permission: function(authorizationService, $route) {
                        return authorizationService.permissionCheck([roles.superUser]);
                    },
                }
            })
        .when('/userSpecificRoute', {
            templateUrl: '/templates/user.html',
            caseInsensitiveMatch: true,
            controller: 'userController',
            resolve: {
                permission: function (authorizationService, $route) {
                    return authorizationService.permissionCheck([roles.user]);
                },
            }
           })
             .when('/adminSpecificRoute', {
                 templateUrl: '/templates/admin.html',
                 caseInsensitiveMatch: true,
                 controller: 'adminController',
                 resolve: {
                     permission: function(authorizationService, $route) {
                         return authorizationService.permissionCheck([roles.admin]);
                     },
                 }
             })
             .when('/adminSuperUserSpecificRoute', {
                 templateUrl: '/templates/adminSuperUser.html',
                 caseInsensitiveMatch: true,
                 controller: 'adminSuperUserController',
                 resolve: {
                     permission: function(authorizationService, $route) {
                         return authorizationService.permissionCheck([roles.admin,roles.superUser]);
                     },
                 }
             })
    });