关于javascript:AngularJS ui-router登录验证

AngularJS ui-router login authentication

我刚接触过AngularJS,我对如何在以下场景中使用Angular-"UI路由器"有点困惑:

我正在构建一个由两部分组成的Web应用程序。第一部分是主页及其登录和注册视图,第二部分是仪表板(成功登录后)。

我为主页部分创建了一个index.html,它有一个角度应用程序和ui-router配置来处理/login/signup视图,还有另一个文件dashboard.html用于仪表板部分,其应用程序和ui-router配置用于处理许多子视图。

现在我完成了仪表板部分,不知道如何将这两个部分与它们不同角度的应用程序结合起来。我如何才能告诉家庭应用程序重定向到仪表板应用程序?


我正在制作一个更好的演示,并将其中的一些服务清理到一个可用的模块中,但下面是我想到的。这是一个复杂的过程,需要解决一些注意事项,所以请坚持下去。你得把它分成几块。好的。

看看这个垃圾。好的。

首先,您需要一个服务来存储用户的身份。我把这个叫做principal。可以检查用户是否登录,并根据请求解析表示用户身份基本信息的对象。这可以是您需要的任何东西,但基本信息是显示名称、用户名、可能是电子邮件,以及用户所属的角色(如果这适用于您的应用程序)。委托人也有进行角色检查的方法。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
.factory('principal', ['$q', '$http', '$timeout',
  function($q, $http, $timeout) {
    var _identity = undefined,
      _authenticated = false;

    return {
      isIdentityResolved: function() {
        return angular.isDefined(_identity);
      },
      isAuthenticated: function() {
        return _authenticated;
      },
      isInRole: function(role) {
        if (!_authenticated || !_identity.roles) return false;

        return _identity.roles.indexOf(role) != -1;
      },
      isInAnyRole: function(roles) {
        if (!_authenticated || !_identity.roles) return false;

        for (var i = 0; i < roles.length; i++) {
          if (this.isInRole(roles[i])) return true;
        }

        return false;
      },
      authenticate: function(identity) {
        _identity = identity;
        _authenticated = identity != null;
      },
      identity: function(force) {
        var deferred = $q.defer();

        if (force === true) _identity = undefined;

        // check and see if we have retrieved the
        // identity data from the server. if we have,
        // reuse it by immediately resolving
        if (angular.isDefined(_identity)) {
          deferred.resolve(_identity);

          return deferred.promise;
        }

        // otherwise, retrieve the identity data from the
        // server, update the identity object, and then
        // resolve.
        //           $http.get('/svc/account/identity',
        //                     { ignoreErrors: true })
        //                .success(function(data) {
        //                    _identity = data;
        //                    _authenticated = true;
        //                    deferred.resolve(_identity);
        //                })
        //                .error(function () {
        //                    _identity = null;
        //                    _authenticated = false;
        //                    deferred.resolve(_identity);
        //                });

        // for the sake of the demo, fake the lookup
        // by using a timeout to create a valid
        // fake identity. in reality,  you'll want
        // something more like the $http request
        // commented out above. in this example, we fake
        // looking up to find the user is
        // not logged in
        var self = this;
        $timeout(function() {
          self.authenticate(null);
          deferred.resolve(_identity);
        }, 1000);

        return deferred.promise;
      }
    };
  }
])

第二,您需要一个服务来检查用户想要进入的状态,确保他们已经登录(如果需要;不需要登录、密码重置等),然后进行角色检查(如果您的应用程序需要这样做的话)。如果它们没有经过身份验证,请将它们发送到登录页。如果它们经过身份验证,但角色检查失败,请将它们发送到拒绝访问页面。我把这项服务叫做authorization。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
.factory('authorization', ['$rootScope', '$state', 'principal',
  function($rootScope, $state, principal) {
    return {
      authorize: function() {
        return principal.identity()
          .then(function() {
            var isAuthenticated = principal.isAuthenticated();

            if ($rootScope.toState.data.roles
                && $rootScope.toState
                             .data.roles.length > 0
                && !principal.isInAnyRole(
                   $rootScope.toState.data.roles))
            {
              if (isAuthenticated) {
                  // user is signed in but not
                  // authorized for desired state
                  $state.go('accessdenied');
              } else {
                // user is not authenticated. Stow
                // the state they wanted before you
                // send them to the sign-in state, so
                // you can return them when you're done
                $rootScope.returnToState
                    = $rootScope.toState;
                $rootScope.returnToStateParams
                    = $rootScope.toStateParams;

                // now, send them to the signin state
                // so they can log in
                $state.go('signin');
              }
            }
          });
      }
    };
  }
])

现在你所要做的就是听听ui-router$stateChangeStart。这使您有机会检查当前状态、它们要转到的状态,并插入您的授权检查。如果失败,您可以取消路线转换,或更改为其他路线。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.run(['$rootScope', '$state', '$stateParams',
      'authorization', 'principal',
    function($rootScope, $state, $stateParams,
             authorization, principal)
{
      $rootScope.$on('$stateChangeStart',
          function(event, toState, toStateParams)
      {
        // track the state the user wants to go to;
        // authorization service needs this
        $rootScope.toState = toState;
        $rootScope.toStateParams = toStateParams;
        // if the principal is resolved, do an
        // authorization check immediately. otherwise,
        // it'll be done when the state it resolved.
        if (principal.isIdentityResolved())
            authorization.authorize();
      });
    }
  ]);

跟踪用户身份的棘手部分是,如果您已经过身份验证(例如,您在上一个会话之后访问了该页,并在cookie中保存了身份验证令牌,或者您硬刷新了一个页面,或者从链接中放到了一个URL上),则会查找它。由于ui-router的工作方式,在进行身份验证检查之前,您需要执行一次身份解析。您可以使用状态配置中的resolve选项来完成此操作。对于所有状态都继承自的站点,我有一个父状态,它强制在发生任何其他事情之前解决主体。好的。

1
2
3
4
5
6
7
8
9
10
11
$stateProvider.state('site', {
  'abstract': true,
  resolve: {
    authorize: ['authorization',
      function(authorization) {
        return authorization.authorize();
      }
    ]
  },
  template: ''
})

这里还有一个问题…resolve只被调用一次。一旦您的身份查找承诺完成,它将不会再次运行解析委托。因此,我们必须在两个地方进行您的身份验证检查:一是根据您在resolve中的身份承诺解决问题,该解决方案涵盖应用程序首次加载,另一个是在$stateChangeStart中解决问题(如果解决方案已完成),该解决方案涵盖您在各个州的任何时间。好的。

好吧,那么到目前为止我们做了什么?好的。

  • 如果用户登录,我们会查看应用程序何时加载。
  • 我们跟踪有关登录用户的信息。
  • 我们将它们重定向到需要用户登录的状态的登录状态。
  • 如果他们没有访问权限,我们会将他们重定向到拒绝访问状态。
  • 如果需要用户登录,我们有一种机制可以将他们重定向回他们请求的原始状态。
  • 我们可以注销一个用户(需要与管理您的身份验证票据的任何客户机或服务器代码一起连接)。
  • 我们不需要每次用户重新加载浏览器或链接时都将其发送回登录页面。
  • 我们从这里去哪里?嗯,您可以将州组织成需要登录的区域。您可以通过将dataroles添加到这些状态(或者如果您想使用继承,则添加它们的父级)来要求经过身份验证/授权的用户。这里,我们将资源限制为管理员:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .state('restricted', {
        parent: 'site',
        url: '/restricted',
        data: {
          roles: ['Admin']
        },
        views: {
          'content@': {
            templateUrl: 'restricted.html'
          }
        }
      })

    现在,您可以按状态控制用户可以访问路由的内容。还有其他问题吗?可能只根据是否登录而改变视图的一部分?没问题。使用principal.isAuthenticated()或甚至principal.isInRole()可以有条件地显示模板或元素的多种方法之一。好的。

    首先,将EDOCX1[4]注入控制器或其他对象,并将其固定在范围内,以便在您的视图中轻松使用:好的。

    1
    2
    3
    4
    5
    .scope('HomeCtrl', ['$scope', 'principal',
        function($scope, principal)
    {
      $scope.principal = principal;
    });

    显示或隐藏元素:好的。

    1
    2
    3
    4
       I'm logged in


      I'
    m not logged in

    等等,等等,等等。无论如何,在您的示例应用程序中,主页的状态将允许未经身份验证的用户访问。他们可以链接到登录或注册状态,或者将这些表单内置到该页面中。任何适合你的。好的。

    仪表板页面可能都继承自一个状态,该状态要求用户登录,例如,是User角色成员。我们讨论过的所有授权信息都会从那里流出。好的。好啊。


    我认为,目前公布的解决方案不必要复杂。有一个简单的方法。ui-router的文件说,听$locationChangeSuccess并使用$urlRouter.sync()来检查状态转换、停止或恢复。但即使这样也行不通。

    然而,这里有两个简单的选择。挑选一个:

    解决方案1:收听$locationChangeSuccess

    你可以听$locationChangeSuccess,你可以执行一些逻辑,甚至异步逻辑。基于该逻辑,您可以让函数返回Undefined,这将导致状态转换正常继续,或者如果用户需要认证,您可以执行$state.go('logInPage')。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    angular.module('App', ['ui.router'])

    // In the run phase of your Angular application  
    .run(function($rootScope, user, $state) {

      // Listen to '$locationChangeSuccess', not '$stateChangeStart'
      $rootScope.$on('$locationChangeSuccess', function() {
        user
          .logIn()
          .catch(function() {
            // log-in promise failed. Redirect to log-in page.
            $state.go('logInPage')
          })
      })
    })

    请记住,这实际上并不能阻止加载目标状态,但如果用户未经授权,它确实会重定向到登录页。这没关系,因为服务器上有真正的保护。

    解决方案2:使用状态resolve

    在这个解决方案中,您使用ui-router解析功能。

    如果用户没有通过身份验证,那么您基本上拒绝resolve中的承诺,然后将其重定向到登录页面。

    具体情况如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    angular.module('App', ['ui.router'])

    .config(
      function($stateProvider) {
        $stateProvider
          .state('logInPage', {
            url: '/logInPage',
            templateUrl: 'sections/logInPage.html',
            controller: 'logInPageCtrl',
          })
          .state('myProtectedContent', {
            url: '/myProtectedContent',
            templateUrl: 'sections/myProtectedContent.html',
            controller: 'myProtectedContentCtrl',
            resolve: { authenticate: authenticate }
          })
          .state('alsoProtectedContent', {
            url: '/alsoProtectedContent',
            templateUrl: 'sections/alsoProtectedContent.html',
            controller: 'alsoProtectedContentCtrl',
            resolve: { authenticate: authenticate }
          })

        function authenticate($q, user, $state, $timeout) {
          if (user.isAuthenticated()) {
            // Resolve the promise successfully
            return $q.when()
          } else {
            // The next bit of code is asynchronously tricky.

            $timeout(function() {
              // This code runs after the authentication promise has been rejected.
              // Go to the log-in page
              $state.go('logInPage')
            })

            // Reject the authentication promise to prevent the state from loading
            return $q.reject()
          }
        }
      }
    )

    与第一个解决方案不同,此解决方案实际上阻止加载目标状态。


    最简单的解决方案是使用$stateChangeStartevent.preventDefault()在用户未通过身份验证时取消状态更改,并将其重定向到登录页的身份验证状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    angular
      .module('myApp', [
        'ui.router',
      ])
        .run(['$rootScope', 'User', '$state',
        function ($rootScope, User, $state) {
          $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
            if (toState.name !== 'auth' && !User.authenticaded()) {
              event.preventDefault();
              $state.go('auth');
            }
          });
        }]
      );


    我认为您需要一个处理身份验证过程(及其存储)的service

    在这项服务中,您需要一些基本方法:

    • isAuthenticated()
    • login()
    • logout()
    • 等。。。

    此服务应注入每个模块的控制器中:

    • 在Dashboard部分,使用此服务检查用户是否经过身份验证(service.isAuthenticated()方法)。如果没有,重定向到/登录
    • 在您的登录部分,只需使用表单数据通过您的service.login()方法对用户进行身份验证。

    这个行为的一个好的、健壮的例子是ProjectAngular应用程序,特别是它的安全模块,它基于可怕的HTTP认证拦截器模块。

    希望这有帮助


    我创建这个模块是为了帮助制作这个过程。

    你可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    $routeProvider
      .state('secret',
        {
          ...
          permissions: {
            only: ['admin', 'god']
          }
        });

    或者也

    1
    2
    3
    4
    5
    6
    7
    8
    $routeProvider
      .state('userpanel',
        {
          ...
          permissions: {
            except: ['not-logged-in']
          }
        });

    这是全新的,但值得一看!

    https://github.com/narzerus/angular-permission网站


    我想与用户界面路由器1.0.0.x共享另一个解决方案

    如您所知,StateChangeStart和StateChangeSsuccess现在已被弃用。https://github.com/angular-ui/ui-router/issues/2655

    相反,您应该使用$transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html

    我就是这样做到的:

    首先,我有和AuthService具有一些有用的功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    angular.module('myApp')

            .factory('AuthService',
                    ['$http', '$cookies', '$rootScope',
                        function ($http, $cookies, $rootScope) {
                            var service = {};

                            // Authenticates throug a rest service
                            service.authenticate = function (username, password, callback) {

                                $http.post('api/login', {username: username, password: password})
                                        .success(function (response) {
                                            callback(response);
                                        });
                            };

                            // Creates a cookie and set the Authorization header
                            service.setCredentials = function (response) {
                                $rootScope.globals = response.token;

                                $http.defaults.headers.common['Authorization'] = 'Bearer ' + response.token;
                                $cookies.put('globals', $rootScope.globals);
                            };

                            // Checks if it's authenticated
                            service.isAuthenticated = function() {
                                return !($cookies.get('globals') === undefined);
                            };

                            // Clear credentials when logout
                            service.clearCredentials = function () {
                                $rootScope.globals = undefined;
                                $cookies.remove('globals');
                                $http.defaults.headers.common.Authorization = 'Bearer ';
                            };

                            return service;
                        }]);

    然后我有了这个配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    angular.module('myApp', [
        'ui.router',
        'ngCookies'
    ])
            .config(['$stateProvider', '$urlRouterProvider',
                function ($stateProvider, $urlRouterProvider) {
                    $urlRouterProvider.otherwise('/resumen');
                    $stateProvider
                            .state("dashboard", {
                                url:"/dashboard",
                                templateUrl:"partials/dashboard.html",
                                controller:"dashCtrl",
                                data: {
                                    authRequired: true
                                }
                            })
                            .state("login", {
                                url:"/login",
                                templateUrl:"partials/login.html",
                                controller:"loginController"
                            })
                }])

            .run(['$rootScope', '$transitions', '$state', '$cookies', '$http', 'AuthService',
                function ($rootScope, $transitions, $state, $cookies, $http, AuthService) {

                    // keep user logged in after page refresh
                    $rootScope.globals = $cookies.get('globals') || {};
                    $http.defaults.headers.common['Authorization'] = 'Bearer ' + $rootScope.globals;

                    $transitions.onStart({
                        to: function (state) {
                            return state.data != null && state.data.authRequired === true;
                        }
                    }, function () {
                        if (!AuthService.isAuthenticated()) {
                            return $state.target("login");
                        }
                    });
                }]);

    你可以看到我用的

    1
    2
    3
    data: {
       authRequired: true
    }

    将状态标记为只有经过身份验证才能访问。

    然后,在.run上,我使用转换检查授权状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $transitions.onStart({
        to: function (state) {
            return state.data != null && state.data.authRequired === true;
        }
    }, function () {
        if (!AuthService.isAuthenticated()) {
            return $state.target("login");
        }
    });

    我使用$transitions文档中的一些代码构建了这个示例。我对用户界面路由器还很熟悉,但它能正常工作。

    希望它能帮助任何人。


    下面是我们如何摆脱无限路由循环,仍然使用$state.go而不是$location.path

    1
    2
    3
    if('401' !== toState.name) {
      if (principal.isIdentityResolved()) authorization.authorize();
    }


    使用$HTTP拦截器

    通过使用$HTTP拦截器,您可以将头发送到后端或其他方式,并以这种方式进行检查。

    关于$HTTP拦截器的伟大文章

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    $httpProvider.interceptors.push(function ($q) {
            return {
                'response': function (response) {

                    // TODO Create check for user authentication. With every request send"headers" or do some other check
                    return response;
                },
                'responseError': function (reject) {

                    // Forbidden
                    if(reject.status == 403) {
                        console.log('This page is forbidden.');
                        window.location = '/';
                    // Unauthorized
                    } else if(reject.status == 401) {
                        console.log("You're not authorized to view this page.");
                        window.location = '/';
                    }

                    return $q.reject(reject);
                }
            };
        });

    把它放到.config或.run函数中。


    首先,您需要一个可以注入到控制器中的服务,该服务对应用程序身份验证状态有一定的了解。用本地存储持久化身份验证详细信息是一种很好的方法。

    接下来,您需要在状态更改之前检查auth的状态。由于您的应用程序有一些需要验证的页面和其他不需要验证的页面,请创建一个检查验证的父路由,并使所有其他需要验证的页面都成为该父路由的子路由。

    最后,您需要某种方法来判断当前登录的用户是否可以执行某些操作。这可以通过向认证服务添加"can"功能来实现。可以接受两个参数:-操作-必需(即"管理仪表板"或"创建新仪表板")。-对象-可选-正在操作的对象。例如,如果您有一个Dashboard对象,您可能希望检查Dashboard.OwnerID===loggedinuser.id。(当然,从客户机传递的信息永远不应该被信任,在将其写入数据库之前,您应该在服务器上对此进行验证)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    angular.module('myApp', ['ngStorage']).config([
       '$stateProvider',
    function(
       $stateProvider
    ) {
       $stateProvider
         .state('home', {...}) //not authed
         .state('sign-up', {...}) //not authed
         .state('login', {...}) //not authed
         .state('authed', {...}) //authed, make all authed states children
         .state('authed.dashboard', {...})
    }])
    .service('context', [
       '$localStorage',
    function(
       $localStorage
    ) {
       var _user = $localStorage.get('user');
       return {
          getUser: function() {
             return _user;
          },
          authed: function() {
             return (_user !== null);
          },
          // server should return some kind of token so the app
          // can continue to load authenticated content without having to
          // re-authenticate each time
          login: function() {
             return $http.post('/login.json').then(function(reply) {
                if (reply.authenticated === true) {
                   $localStorage.set(_userKey, reply.user);
                }
             });
          },
          // this request should expire that token, rendering it useless
          // for requests outside of this session
          logout: function() {
             return $http.post('logout.json').then(function(reply) {
                if (reply.authenticated === true) {
                   $localStorage.set(_userKey, reply.user);
                }
             });
          },
          can: function(action, object) {
             if (!this.authed()) {
                return false;
             }

             var user = this.getUser();

             if (user && user.type === 'admin') {
                 return true;
             }

             switch(action) {
                case 'manage_dashboards':
                   return (user.type === 'manager');
             }

             return false;


          }
       }
    }])
    .controller('AuthCtrl', [
       'context',
       '$scope',
    function(
       context,
       $scope
    ) {
       $scope.$root.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
          //only require auth if we're moving to another authed page
          if (toState && toState.name.indexOf('authed') > -1) {
             requireAuth();
          }
       });

       function requireAuth() {
          if (!context.authed()) {
             $state.go('login');
          }
       }
    }]

    **免责声明:以上代码为伪代码,不作任何保证**


    我还有另一个解决方案:当您只有您想在登录时显示的内容时,该解决方案非常有效。定义一个规则,在该规则中检查您是否已登录,而不是白名单路由的路径。

    1
    2
    3
    4
    5
    6
    7
    8
    $urlRouterProvider.rule(function ($injector, $location) {
       var UserService = $injector.get('UserService');
       var path = $location.path(), normalized = path.toLowerCase();

       if (!UserService.isLoggedIn() && path.indexOf('login') === -1) {
         $location.path('/login/signin');
       }
    });

    在我的示例中,我询问我是否没有登录,并且我要路由的当前路由不是`/login'的一部分,因为我的白名单路由如下

    1
    2
    /login/signup // registering new user
    /login/signin // login to app

    所以我可以即时访问这两条路线,如果你在线的话,每一条路线都会被检查。

    这是我登录模块的整个路由文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    export default (
      $stateProvider,
      $locationProvider,
      $urlRouterProvider
    ) => {

      $stateProvider.state('login', {
        parent: 'app',
        url: '/login',
        abstract: true,
        template: '<ui-view></ui-view>'
      })

      $stateProvider.state('signin', {
        parent: 'login',
        url: '/signin',
        template: '<login-signin-directive></login-signin-directive>'
      });

      $stateProvider.state('lock', {
        parent: 'login',
        url: '/lock',
        template: '<login-lock-directive></login-lock-directive>'
      });

      $stateProvider.state('signup', {
        parent: 'login',
        url: '/signup',
        template: '<login-signup-directive></login-signup-directive>'
      });

      $urlRouterProvider.rule(function ($injector, $location) {
        var UserService = $injector.get('UserService');
        var path = $location.path();

        if (!UserService.isLoggedIn() && path.indexOf('login') === -1) {
             $location.path('/login/signin');
        }
      });

      $urlRouterProvider.otherwise('/error/not-found');
    }

    () => { /* code */ }是es6语法,用function() { /* code */ }代替。