如何在angularJS SPA中集成完整的登录流程?

时间:2017-03-29 08:22:21

标签: angularjs angular-ui-router

我在angularJS SPA中实现登录系统存在严重问题。这是整个应用程序开发中最难的部分,因为我已经处理过其他方面。我无法提出正确的服务设计(我认为我已经理解了承诺,但似乎还没那么好)。

app的唯一授权方法是FB登录与本机服务器端逻辑相结合。在FB JS SDK登录成功应用程序将使http发布到服务器,我使用FB PHP SDK从fb cookie获取访问令牌,获取fb id并检查我们是否有用户在数据库中具有该fb id。如果有 - 将所有用户凭据返回给应用程序,如果没有 - 返回注册标志。

对于注册我只需要一件FB没有提供的东西 - 用户将在地图上放置一个标记,坐标将被发送到服务器并保存在数据库中。

mysql> DESCRIBE users;
+---------+------------+------+-----+---------+----------------+
| Field   | Type       | Null | Key | Default | Extra          |
+---------+------------+------+-----+---------+----------------+
| id      | int(11)    | NO   | PRI | NULL    | auto_increment |
| fbid    | ...        | NO   |     | NULL    |                |
| lat     | ...        | NO   |     | NULL    |                |
| lng     | ...        | NO   |     | NULL    |                |
| name    | ...        | NO   |     | NULL    |                |
| ...     | ...        | NO   |     | NULL    |                |
+---------+------------+------+-----+---------+----------------+

我将使用fbid授权(识别)用户,我自己的id在app(客户端和服务器端)内工作,坐标来自寄存器以及FB中的所有其他内容。

我可以通过4种方式从app登录用户:

  1. 自动登录即可。在应用程序运行时,我检查fb登录statuss和if status === connected然后我自动登录用户。
  2. 关于用户操作。当用户点击“登录”btn时。
  3. 如果用户尝试访问需要登录的路由。调用用户操作上使用的相同方法,如果它返回成功,则用户可以继续保护路由,否则 - 转换被取消<​​/ li>
  4. 如果服务器返回401 statuss 。如果会话过期,则应该调用相同的方法,并且应该再次成功重新登录请求。
  5. 我如何看待它,每个案例都依赖于一种方法,让我们说AuthService.login

    1. 如果FB状态为AuthService.login
    2. ,则自动登录从app.run()方法调用connected
    3. 在用户点击AuthService.login时称为
    4. 我为transitionHookFn转换生命周期添加onStart并运行AuthService.login如果需要登录且用户未登录。成功登录后我继续,否则 - 取消转换(我使用ui-路由器)
    5. 我使用$httpProvider.interceptor通过AuthService.login拨打statuss 401 $ http响应。在成功登录时,我再次请求相同的请求,否则 - 让请求失败(并处理路由逻辑,以便失败的请求将取消转换)
    6. 我的核心问题在于AuthService.login。应该如何构建该方法,以便我可以在所有四种情况下重用它?

      到目前为止我有什么:

      我使用ui-router和this来集成FB JS SDK。 为了处理应用程序中的用户状态我有服务:

      (function(){
          angular.module('userService')
              .factory('UserService', userService);
      
          userService.$inject = [];
      
          function userService(){
              var user = {
                  isLogged: false,
                  info: ''
              };
              return {
                  setUser: setUser,
                  getUser: getUser,
                  clearUser: clearUser
              };
      
              function setUser(info){
                  user.isLogged = true;
                  user.info = info;
              }
              function getUser(){
                  return user;
              }
              function clearUser(){
                  user.isLogged = false;
                  user.info = {};
              }
          }
      })();
      

      我将其注入我需要了解用户身份的位置并使用userService.getUser

      授权我有服务:

      (function(){
          angular.module('authService')
              .factory('AuthService', AuthService);
      
          AuthService.$inject = ['UserService', 'Facebook', '$http', 'baseUrl', '$q'];
      
          function AuthService(UserService, Facebook, $http, baseUrl, $q){
              return {
                  autoLogin: autoLogin,
                  login: login
              };
              function autoLogin(){
                  Facebook.getLoginStatus(function(response) {
                      statusChangeCallback(response);
                  });
              }
              function login(){
                  Facebook.login(function(response) {
                      statusChangeCallback(response);
                  });
              }
              function statusChangeCallback(response) {
                  console.log('statusChangeCallback');
                  if (response.status === 'connected') {
                      $http({
                          method: 'POST',
                          url: /*login endpoint*/
                      }).then(/*Dont know what should do here*/)
                  }
              }
          }
      })();
      

      此时登录端点返回JSON {user: null, register: true},如果它是第一次登录并{user: {/*data*/}, register: false}但我在那里打开更改。我已经尝试了几种处理响应的方法,但在前面描述的四种情况之一中我总是遇到一些麻烦。基本上如果register: false,那么我应该使用userService.setUser(response.data.user),如果register:true - $state.target('register')

      对于案例3我有:

      $transition.onStart({ to: function(state) {
              return state.data != null && state.data.authRequired === true;
          }},function(trans){
              var AuthService = trans.injector().get('AuthService');
              /*Should run AuthService.login and:
              1.continue if completely logged in
              2.$state.target('register') if register needed
              3.cancel otherwise*/
          });
      

      对于案例4我有:

      var sessionRecoverer = {
          responseError: function(response) {
              if (response.status == 401){
                  var AuthService = $injector.get('AuthService');
                  var $http = $injector.get('$http');
                  var deferred = $q.defer();
                  /*again call login and redirect if register or do http request again if login success*/
              }
              return $q.reject(response); //Think that this would be correct to reject any other error
          }
      };
      

      总结一下 - 我理解$ http拦截器和ui-router转换是如何工作的。我不知道如何处理来自AuthService的承诺,因为服务器响应有两种不同的结果,我很难理解如何在AuthService登录流程中保留UserService.setUser方法。前两种情况(自动登录和用户需求登录)很简单 - AuthService需要设置用户信息或重定向到注册。最后两个选项出现问题,因为在这两个选项中,其他一些服务需要确定是否继续。

      更新:在我的脑海中只是一个想法 - 如果需要注册,我会返回自定义statuss代码或标头,使用http拦截器和重定向+失败请求捕获它。然后我会有简单的机制,如果AuthService返回承诺考虑登录成功否则失败。 ?

1 个答案:

答案 0 :(得分:3)

我正在处理一个类似的AngularJS项目的身份验证/授权工作流程。我没有使用PHP或Facebook IDP,而是使用ASP.NET Web API和Microsoft Azure B2C作为IDP。但是,让我们从AngularJS的角度来看这个问题,并对IDP和服务器端技术不可知。

最简单的流程涉及使应用程序的根(主页)成为未经过身份验证的页面。用户将单击“登录”按钮,该按钮将启动您的身份验证过程。这会调用您的AuthService.login()函数。此调用应该来自控制器,并且应该包含在承诺中以避免使用回调。 (注意:您可以在.run()块中完全拥有自动登录逻辑,而不是按下这两个按钮,因为它似乎是您想做的 - 逻辑将在.run()区块中保持不变。

function login(){
    var deferred = this.$q.defer();
    Facebook.login(function(response) {
        deferred.resolve(response);
    });
    return deferred.promise;
}

然后,从调用AuthService.login()的控制器,您需要处理对API的POST请求以更新/获取用户信息。这看起来像这样(注意我将你的HTTP调用重构为一个新的initUserData()函数):

AuthService.login().then(function(response) {
    if (response.status === 'connected') {
        AuthService.initUserData().then(function(userObject) {
            if(userObject.data.register === true) {
                 $state.go('register');
            } else {
                 userService.setUser(response.data.user);
                 $state.go('whereverUserShouldGoInThisCase');
            }
        });
    }
});

很好,到目前为止我们已经涵盖了#1和#2案例。我们来看看#3 。 如果我们不希望用户点击需要身份验证的路由,我们可以使用应用程序的.run()块中的UI-Router设置挂钩。

首先,我们需要声明哪些路由需要进行身份验证。

.state('home', {
     url: 'home',
     restricted: true
}) 

我们可以通过在您的应用程序中为您想要限制(通过身份验证)的所有路由添加任何名称的新属性(在本例中我们使用restricted)来实现此目的。

然后在.run()块中,我们通过$stateChangeStart挂钩到UI-Router。无论何时转换到状态(在转换的开始),包括抽象状态,都会调用此方法。

$rootScope.$on('$stateChangeStart', (event, next, nextParams, current, currentParams), function() {
        // Any time the user tries to hit a restricted route, we need to check if they're logged in
        if (next.restricted) {
            AuthService.login().then(function(response) {
                if(response.status !== 'connected') {
                     event.preventDefault(); // Prevent going to the transitioning state
                     $state.go('login'); // Go to the state we want to send the user to
                }
            });
        }
});

这应该涵盖#3,让我们看看#4 。 你肯定有使用HTTP拦截器的正确想法。在这里,我们可以复制与以前相同的逻辑来处理用户在401上重定向的位置。

responseError: function(response) {
    if (response.status == 401){
        var AuthService = $injector.get('AuthService');
        AuthService.login().then(function(response) {
                if(response.status === 'connected') {
                     AuthService.initUserData().then(function(userObject) {
                        if(userObject.data.register === true) {
                            $injector.get('$state').go('register');
                        } else {
                            userService.setUser(response.data.user);
                            $injector.get('$state').go('whereverUserShouldGoInThisCase');
                        }
                    });
                } 
         });
    }

    return $q.reject(response); // Yes, this will properly reject your response
 }

最后的一些说明:

  • 我会考虑使用本地存储来保存用户信息,而不是将其存储在服务变量中。即使页面被刷新,这也将使信息持续存在。
  • 注册流程/正常流程使一切变得更加困难。我在申请中遇到了同样的麻烦。您需要非常小心如何从这些不同的“角色”中引导应用程序中的流程。
  • 你肯定对#4使用HTTP拦截器是正确的想法,但对于#3,请确保使用$stateChangeStart。这个钩子非常强大,当你有不同的“角色”(即注册/非注册用户)时,你可以使用上一个/下一个路径,这对你的应用程序中的路由逻辑有帮助。
  • 我不确定您的Facebook SDK是否有登录错误回调,但我认为它会有一个。确保实现错误回调并在发生错误时执行deferred.reject()
  • 最后,我的目标不是为您编写代码。我相信你必须添加相当多的逻辑来处理你的应用程序的确切用例。但是,看起来大多数情况下你都有正确的想法,我认为你将能够从这里实现你的逻辑。