AngularJS ui-router 登录认证解决方案 1:监听$locationChangeSuccess 解决方案 2:使用状态resolve

2022-08-29 23:36:13

我是AngularJS的新手,我对如何在以下场景中使用angular-“ui-router”有点困惑:

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

我已经为主页部分创建了一个,其中包含其角度应用程序和配置来处理和视图,并且仪表板部分还有另一个文件,其应用程序和配置用于处理许多子视图。index.htmlui-router/login/signupdashboard.htmlui-router

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


答案 1

我正在制作一个更好的演示,并将其中一些服务清理成一个可用的模块,但这是我想到的。这是一个复杂的过程,可以绕过一些警告,所以坚持下去。您需要将其分解为几个部分。

看看这个 plunk

首先,您需要一个服务来存储用户的标识。我称之为.可以检查它以查看用户是否已登录,并根据请求,它可以解析表示有关用户标识的基本信息的对象。这可以是你需要的任何内容,但基本要素可以是显示名称、用户名、可能是电子邮件以及用户所属的角色(如果这适用于你的应用)。主体还具有执行角色检查的方法。principal

.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

.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');
              }
            }
          });
      }
    };
  }
])

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

.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-routerresolve

$stateProvider.state('site', {
  'abstract': true,
  resolve: {
    authorize: ['authorization',
      function(authorization) {
        return authorization.authorize();
      }
    ]
  },
  template: '<div ui-view />'
})

这里还有另一个问题... 只被调用一次。标识查找承诺完成后,它将不会再次运行解析委托。因此,我们必须在两个位置进行身份验证检查:一次是根据你的身份承诺解析 in ,它涵盖了你的应用首次加载的时间,一次是(如果解决方案已完成),它涵盖了你在各州导航的任何时间。resolveresolve$stateChangeStart

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

  1. 我们检查应用何时加载(如果用户已登录)。
  2. 我们跟踪有关登录用户的信息。
  3. 我们将他们重定向到需要用户登录的状态的登录状态。
  4. 如果他们无权访问,我们会将其重定向到拒绝访问状态。
  5. 我们有一种机制,可以将用户重定向回他们请求的原始状态,如果我们需要他们登录。
  6. 我们可以将用户注销(需要与管理您的身份验证票证的任何客户端或服务器代码一起连接)。
  7. 我们不需要在用户每次重新加载浏览器或删除链接时将用户送回登录页面。

我们该何去何从?好吧,您可以将州组织到需要登录的区域中。您可以通过向这些状态添加 with 来要求经过身份验证/授权的用户(如果要使用继承,则可以添加这些状态的父级)。在这里,我们将资源限制为管理员:dataroles

.state('restricted', {
    parent: 'site',
    url: '/restricted',
    data: {
      roles: ['Admin']
    },
    views: {
      'content@': {
        templateUrl: 'restricted.html'
      }
    }
  })

现在,您可以逐个状态控制哪些用户可以访问路由。还有其他问题吗?也许根据它们是否登录而仅改变视图的一部分?没关系。使用,甚至与可以条件显示模板或元素的众多方法中的任何一种一起使用。principal.isAuthenticated()principal.isInRole()

首先,注入控制器或其他任何东西,并将其粘贴到示波器上,以便您可以在视图中轻松使用它:principal

.scope('HomeCtrl', ['$scope', 'principal', 
    function($scope, principal)
{
  $scope.principal = principal;
});

显示或隐藏元素:

<div ng-show="principal.isAuthenticated()">
   I'm logged in
</div>
<div ng-hide="principal.isAuthenticated()">
  I'm not logged in
</div>

等等,等等,等等。无论如何,在你的示例应用中,你的主页将有一个状态,允许未经身份验证的用户顺便访问。它们可以具有指向登录或注册状态的链接,或者将这些表单内置到该页面中。任何适合您的东西。

仪表板页面都可以从要求用户登录的状态继承,例如,成为角色成员。我们讨论过的所有授权内容都将从那里流出。User


答案 2

在我看来,到目前为止发布的解决方案是不必要的复杂。有一种更简单的方法。ui路由器的文档说监听并用于检查状态转换,停止或恢复状态转换。但即使这样实际上也行不通。$locationChangeSuccess$urlRouter.sync()

但是,这里有两个简单的选择。选择一项:

解决方案 1:监听$locationChangeSuccess

您可以监听并执行一些逻辑,甚至是异步逻辑。基于该逻辑,您可以让函数返回未定义,这将导致状态转换正常继续,或者如果用户需要进行身份验证,则可以这样做。下面是一个示例:$locationChangeSuccess$state.go('logInPage')

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 路由器解析功能

如果用户未通过身份验证,您基本上会拒绝承诺,然后将他们重定向到登录页面。resolve

事情是这样的:

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()
      }
    }
  }
)

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