AngularJS ui-router 登录认证

IT技术 javascript angularjs angular-ui-router
2021-02-07 13:40:48

我是 AngularJS 的新手,我对如何在以下场景中使用 angular-"ui-router" 感到有些困惑:

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

我已经index.html为 home 部分创建了一个angular 应用程序和ui-router配置来处理/login/signup查看,还有另一个dashboard.html用于仪表板部分的文件,它的应用程序和ui-router配置可以处理许多子视图。

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

6个回答

我正在制作一个更好的演示,并将其中的一些服务清理成一个可用的module,但这是我想出的。这是一个复杂的过程来解决一些警告,所以坚持下去。你需要把它分解成几个部分。

看看这个笨蛋

首先,您需要一个服务来存储用户的身份。我称之为principal. 可以检查用户是否已登录,并且根据请求,它可以解析代表有关用户身份的基本信息的对象。这可以是您需要的任何内容,但基本要素是显示名称、用户名、可能是电子邮件以及用户所属的角色(如果这适用于您的应用)。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');
              }
            }
          });
      }
    };
  }
])

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

.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状态配置中选项执行此操作我有一个站点的父状态,所有状态都继承自,这迫使主体在发生任何其他事情之前得到解决。

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

这里还有另一个问题......resolve只被调用一次。一旦您的身份查找Promise完成,它就不会再次运行解析委托。因此,我们必须在两个地方进行您的身份验证检查:一次根据您的身份Promise解析 in resolve,这涵盖了您的应用程序第一次加载,以及一次$stateChangeStart如果解析已完成,则涵盖您在各州之间导航的任何时间。

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

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

我们从这里去哪里?嗯,你可以组织你的状态到需要的标志地区。您可以通过添加需要身份验证/授权用户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角色成员的状态继承我们讨论过的所有授权内容都将从那里流出。

@Ir1sh 我会首先在控制器中解析身份并将其分配给$scope.user您的then函数。您仍然可以user在您的意见中引用解决后,视图将被更新。
2021-03-12 13:40:48
@HackedByChinese 我认为您的演示不再有效。
2021-03-19 13:40:48
@jvannistelrooy 我在使用 go() 时遇到了问题,但是在调用了这样的 noop 函数之后将其放入 then 之后$q.when(angular.noop).then(function(){$state.go('myState'),一切都按预期进行。如果我$state.go在另一个状态转换未完成时调用,那么它将不起作用(我认为这就是它不起作用的原因)。
2021-04-01 13:40:48
这是一个很好的答案,它对我帮助很大。当我在控制器中设置 user = principal 并尝试在我的视图中调用 say user.identity().name 以获取当前登录的用户名时,我似乎只得到了 promise 对象 {then: fn, catch: fn, finally :} 返回而不是实际的 _identity 对象。如果我使用 user.identity.then(fn(user)) 我可以获得用户对象,但这似乎是视图的很多代码我错过了什么吗?
2021-04-02 13:40:48
谢谢,这真的帮助我整理了自己的代码。附带说明一下,如果您遇到无限路由循环(UI 路由器错误),请尝试$location.path代替$state.go.
2021-04-07 13:40:48

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

但是,这里有两个简单的替代方案。选一个:

解决方案1:监听 $locationChangeSuccess

你可以听$locationChangeSuccess,你可以在那里执行一些逻辑,甚至异步逻辑。基于该逻辑,您可以让函数返回 undefined,这将导致状态转换继续正常进行$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-router解析功能

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

这是它的过程:

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

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

@PeterDraexDräxler 我主要是在关注文档。你注意到使用有$stateChangeStart什么不同吗?
2021-03-11 13:40:48
@FredLackey 说未经身份验证的用户在state A. 他们单击要转到的链接,protected state B但您想将他们重定向到logInPage. 如果没有$timeoutui-router将简单地停止所有状态转换,因此用户将被困在state A. $timeout允许ui-router先防止初期转移protected state B,因为决心被拒绝和这样做了之后,它重定向到logInPage
2021-03-16 13:40:48
@Imrayauthenticate函数作为参数传递给ui-router. 您不必自己调用它。ui-router称它。
2021-03-18 13:40:48
为什么要使用“$locationChangeSuccess”而不是“$stateChangeStart”?
2021-04-03 13:40:48
authenticate函数实际上在哪里调用?
2021-04-09 13:40:48

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

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');
        }
      });
    }]
  );
在我的情况下,已验证不是异步调用:` this.authenticaded = function() { if (this.currentAccountID !== null) { return true; 返回假;}; `
2021-03-20 13:40:48
如果 User.authenticaded() 是异步调用,我认为这不会起作用。这是每个人都追求的圣杯。例如,如果“登录”之外的每个状态都是安全的,我想加载任何状态之前确认用户仍然经过身份验证使用解析很糟糕,因为它们只解析一次,为了防止加载子状态,您必须将解析注入每个孩子
2021-03-29 13:40:48
根据:stackoverflow.com/a/38374313/849829,“运行”远高于“服务”,因此出现问题。检查 localstorage 的身份验证状态似乎是一个好方法。
2021-04-02 13:40:48

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

在此服务中,您将需要一些基本方法:

  • isAuthenticated()
  • login()
  • logout()
  • 等等 ...

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

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

这种行为的一个好的和健壮的例子是项目angular-app,特别是它的安全module,它基于很棒的HTTP 身份验证拦截器module

希望这可以帮助

我创建了这个module来帮助使这个过程小菜一碟

您可以执行以下操作:

$routeProvider
  .state('secret',
    {
      ...
      permissions: {
        only: ['admin', 'god']
      }
    });

或者也

$routeProvider
  .state('userpanel',
    {
      ...
      permissions: {
        except: ['not-logged-in']
      }
    });

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

https://github.com/Narzerus/angular-permission

有什么可以阻止我在运行时编辑源代码并删除您的“管理员” || “上帝”并继续?
2021-03-13 13:40:48
这不是为了安全,客户端授权从来都不是,因为您可以随时更改值。您甚至可以拦截来自服务器端的响应并将它们评估为“已授权”客户端的权限/授权点是为了避免让用户为 ux 目的做被禁止的事情。例如,如果您正在处理仅限管理员的操作,即使用户恶意欺骗客户端允许向服务器发送受限请求,服务器仍会返回 401 响应。这当然始终是正在实施的 api 的责任@BenRipley
2021-03-28 13:40:48
我希望任何需要授权的数据请求也在服务器上得到验证。
2021-04-03 13:40:48
很好地回答了拉斐尔的问题。始终保护 api,因为前端几乎是最可逆向工程、最容易被欺骗的东西。
2021-04-05 13:40:48
这个历史问题现在已经解决了很长一段时间@Bohdan。即使使用 ui-router 附加功能,您也可以安全地使用它。
2021-04-06 13:40:48