71

Auth/(re)Routing 用 API を使用した Angular+RESTful クライアント側通信

これは、いくつかの異なる質問といくつかの異なるチュートリアルでカバーされていますが、私が遭遇した以前のリソースはすべて、頭に釘を打つものではありません.

一言で言えば、私はする必要があります

  • http://client.fooからへの POST 経由でログインhttp://api.foo/login
  • logoutルートを提供するユーザーに「ログイン済み」の GUI/コンポーネント状態を持たせる
  • ユーザーがログアウト/ログアウトしたときに UI を「更新」できるようにします。 これが一番イライラした
  • ルートを保護して認証済み状態を確認し (必要な場合)、それに応じてユーザーをログイン ページにリダイレクトします。

私の問題は

  • 別のページに移動するたびに、api.foo/statusユーザーがログインしているかどうかを判断するために呼び出しを行う必要があります (ルートに Express を使用している ATM)。ng-show="user.is_authenticated"
  • ログイン/ログアウトに成功したら、ページを更新する必要があります (これを行う必要はありません) {{user.first_name}}
// Sample response from `/status` if successful 

{
   customer: {...},
   is_authenticated: true,
   authentication_timeout: 1376959033,
   ...
}

私が試したこと

心が折れそうになる理由

  • すべてのチュートリアルは何らかのデータベース (多くの Mongo、Couch、PHP+MySQL、無限) ソリューションに依存しているように見えますが、ログイン状態を維持するために RESTful API との通信だけに依存しているものはありません。ログインすると、追加の POST/GET が とともに送信されるwithCredentials:trueため、それは問題ではありません
  • バックエンド言語を除いて、Angular + REST + Authを実行する例/チュートリアル/リポジトリが見つかりません。

自慢しすぎない

確かに、私は Angular の初心者であり、ばかげた方法でこれに取り組んでいても驚かないでしょう。誰かが別の方法を提案してくれたら嬉しいです。

私が ' を使用しているのは、Express主に私が本当に愛しJadeStylusいるからです — 私はExpress' ルーティングと結婚しておらず、やりたいことが Angular のルーティングでのみ可能である場合はあきらめます。

誰でも提供できるヘルプを事前に感謝します。紫色のリンクが 26 ページほどあるので、Google で検索しないでください。;-)


1このソリューションは Angular の $httpBackend モックに依存しており、実際のサーバーと通信する方法が不明です。

2これが最も近かったのですが、認証する必要のある既存の API があるため、passport の「localStrategy」を使用できず、私だけが使用するつもりだった OAUTH サービスを作成するのは非常識に思えました。

4

4 に答える 4

35

これは、URL ルートの承認と要素のセキュリティに関するブログ投稿から引用したものですが、要点を簡単に要約します :-)

フロントエンド Web アプリケーションのセキュリティは、Joe Public を阻止するための開始手段にすぎませんが、Web の知識があれば回避できるため、常にサーバー側のセキュリティも確保する必要があります。

angular のセキュリティ関連の主な懸念事項はルート セキュリティです。幸いなことに、angular でルートを定義するときは、オブジェクト、つまり他のプロパティを持つことができるオブジェクトを作成します。私のアプローチの基礎は、このルート オブジェクトにセキュリティ オブジェクトを追加することです。これは、基本的に、ユーザーが特定のルートにアクセスできるようにするために必要なロールを定義します。

 // route which requires the user to be logged in and have the 'Admin' or 'UserManager' permission
    $routeProvider.when('/admin/users', {
        controller: 'userListCtrl',
        templateUrl: 'js/modules/admin/html/users.tmpl.html',
        access: {
            requiresLogin: true,
            requiredPermissions: ['Admin', 'UserManager'],
            permissionType: 'AtLeastOne'
        });

全体のアプローチは、基本的にユーザーが必要な権限を持っているかどうかを確認する認証サービスに焦点を当てています。このサービスは、このソリューションの他の部分から懸念を抽象化し、ユーザーと、ログイン中にサーバーから取得される実際のアクセス許可を処理します。コードは非常に冗長ですが、私のブログ投稿で完全に説明されています。ただし、基本的には、許可チェックと 2 つの許可モードを処理します。1 つ目は、ユーザーが少なくとも 1 つの定義された権限を持っている必要があるということです。2 つ目は、ユーザーが定義されたすべての権限を持っている必要があるということです。

angular.module(jcs.modules.auth.name).factory(jcs.modules.auth.services.authorization, [  
'authentication',  
function (authentication) {  
 var authorize = function (loginRequired, requiredPermissions, permissionCheckType) {
    var result = jcs.modules.auth.enums.authorised.authorised,
        user = authentication.getCurrentLoginUser(),
        loweredPermissions = [],
        hasPermission = true,
        permission, i;

    permissionCheckType = permissionCheckType || jcs.modules.auth.enums.permissionCheckType.atLeastOne;
    if (loginRequired === true && user === undefined) {
        result = jcs.modules.auth.enums.authorised.loginRequired;
    } else if ((loginRequired === true && user !== undefined) &&
        (requiredPermissions === undefined || requiredPermissions.length === 0)) {
        // Login is required but no specific permissions are specified.
        result = jcs.modules.auth.enums.authorised.authorised;
    } else if (requiredPermissions) {
        loweredPermissions = [];
        angular.forEach(user.permissions, function (permission) {
            loweredPermissions.push(permission.toLowerCase());
        });

        for (i = 0; i < requiredPermissions.length; i += 1) {
            permission = requiredPermissions[i].toLowerCase();

            if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.combinationRequired) {
                hasPermission = hasPermission && loweredPermissions.indexOf(permission) > -1;
                // if all the permissions are required and hasPermission is false there is no point carrying on
                if (hasPermission === false) {
                    break;
                }
            } else if (permissionCheckType === jcs.modules.auth.enums.permissionCheckType.atLeastOne) {
                hasPermission = loweredPermissions.indexOf(permission) > -1;
                // if we only need one of the permissions and we have it there is no point carrying on
                if (hasPermission) {
                    break;
                }
            }
        }

        result = hasPermission ?
                 jcs.modules.auth.enums.authorised.authorised :
                 jcs.modules.auth.enums.authorised.notAuthorised;
    }

    return result;
};

ルートにセキュリティが設定されたので、ルート変更が開始されたときにユーザーがルートにアクセスできるかどうかを判断する方法が必要です。これを行うには、ルート変更リクエストをインターセプトし、(新しいアクセス オブジェクトを含む) ルート オブジェクトを調べます。ユーザーがビューにアクセスできない場合は、ルートを別のものに置き換えます。

angular.module(jcs.modules.auth.name).run([  
    '$rootScope',
    '$location',
    jcs.modules.auth.services.authorization,
    function ($rootScope, $location, authorization) {
        $rootScope.$on('$routeChangeStart', function (event, next) {
            var authorised;
            if (next.access !== undefined) {
                authorised = authorization.authorize(next.access.loginRequired,
                                                     next.access.permissions,
                                                     next.access.permissionCheckType);
                if (authorised === jcs.modules.auth.enums.authorised.loginRequired) {
                    $location.path(jcs.modules.auth.routes.login);
                } else if (authorised === jcs.modules.auth.enums.authorised.notAuthorised) {
                    $location.path(jcs.modules.auth.routes.notAuthorised).replace();
                }
            }
        });
    }]);

ここで重要なのは、'.replace()' です。これは、現在のルート (参照する権利がないルート) をリダイレクト先のルートに置き換えるためです。これにより、許可されていないルートに戻ることができなくなります。

これで、ユーザーがログインする必要のあるルートに着陸した場合に、ログイン後にリダイレクトするなど、非常に多くのクールなことを実行できるルートをインターセプトできます。

ソリューションの 2 番目の部分は、ユーザーの権限に応じて UI 要素を非表示/表示できるようにすることです。これは、単純なディレクティブを介して実現されます。

angular.module(jcs.modules.auth.name).directive('access', [  
        jcs.modules.auth.services.authorization,
        function (authorization) {
            return {
              restrict: 'A',
              link: function (scope, element, attrs) {
                  var makeVisible = function () {
                          element.removeClass('hidden');
                      },
                      makeHidden = function () {
                          element.addClass('hidden');
                      },
                      determineVisibility = function (resetFirst) {
                          var result;
                          if (resetFirst) {
                              makeVisible();
                          }

                          result = authorization.authorize(true, roles, attrs.accessPermissionType);
                          if (result === jcs.modules.auth.enums.authorised.authorised) {
                              makeVisible();
                          } else {
                              makeHidden();
                          }
                      },
                      roles = attrs.access.split(',');


                  if (roles.length > 0) {
                      determineVisibility(true);
                  }
              }
            };
        }]);

次に、次のような要素を確認します。

 <button type="button" access="CanEditUser, Admin" access-permission-type="AtLeastOne">Save User</button>

このアプローチのより詳細な概要については、私の完全なブログ投稿をお読みください。

于 2014-08-02T08:17:36.647 に答える
5

$resource を使用していないのは、アプリケーションのサービス呼び出しを手作りしているだけだからです。つまり、ある種の初期化データを取得する他のすべてのサービスに依存するサービスを持つことで、ログインを処理しました。ログインが成功すると、すべてのサービスの初期化がトリガーされます。

コントローラー スコープ内で loginServiceInformation を監視し、それに応じてモデルのいくつかのプロパティを入力します (適切な ng-show/hide をトリガーするため)。ルーティングに関しては、Angular のビルトイン ルーティングを使用しており、ここに示す LoggedIn ブール値に基づいて ng-hide を持っているだけで、ログインを要求するテキストまたは ng-view 属性を持つ div が表示されます (ログインしていない場合ログイン直後は正しいページにいます。現在、すべてのビューのデータをロードしていますが、必要に応じてより選択的になる可能性があると思います)

//Services
angular.module("loginModule.services", ["gardenModule.services",
                                        "surveyModule.services",
                                        "userModule.services",
                                        "cropModule.services"
                                        ]).service(
                                            'loginService',
                                            [   "$http",
                                                "$q",
                                                "gardenService",
                                                "surveyService",
                                                "userService",
                                                "cropService",
                                                function (  $http,
                                                            $q,
                                                            gardenService,
                                                            surveyService,
                                                            userService,
                                                            cropService) {

    var service = {
        loginInformation: {loggedIn:false, username: undefined, loginAttemptFailed:false, loggedInUser: {}, loadingData:false},

        getLoggedInUser:function(username, password)
        {
            service.loginInformation.loadingData = true;
            var deferred = $q.defer();

            $http.get("php/login/getLoggedInUser.php").success(function(data){
                service.loginInformation.loggedIn = true;
                service.loginInformation.loginAttemptFailed = false;
                service.loginInformation.loggedInUser = data;

                gardenService.initialize();
                surveyService.initialize();
                userService.initialize();
                cropService.initialize();

                service.loginInformation.loadingData = false;

                deferred.resolve(data);
            }).error(function(error) {
                service.loginInformation.loggedIn = false;
                deferred.reject(error);
            });

            return deferred.promise;
        },
        login:function(username, password)
        {
            var deferred = $q.defer();

            $http.post("php/login/login.php", {username:username, password:password}).success(function(data){
                service.loginInformation.loggedInUser = data;
                service.loginInformation.loggedIn = true;
                service.loginInformation.loginAttemptFailed = false;

                gardenService.initialize();
                surveyService.initialize();
                userService.initialize();
                cropService.initialize();

                deferred.resolve(data);
            }).error(function(error) {
                service.loginInformation.loggedInUser = {};
                service.loginInformation.loggedIn = false;
                service.loginInformation.loginAttemptFailed = true;
                deferred.reject(error);
            });

            return deferred.promise;
        },
        logout:function()
        {
            var deferred = $q.defer();

            $http.post("php/login/logout.php").then(function(data){
                service.loginInformation.loggedInUser = {};
                service.loginInformation.loggedIn = false;
                deferred.resolve(data);
            }, function(error) {
                service.loginInformation.loggedInUser = {};
                service.loginInformation.loggedIn = false;
                deferred.reject(error);
            });

            return deferred.promise;
        }
    };
    service.getLoggedInUser();
    return service;
}]);

//Controllers
angular.module("loginModule.controllers", ['loginModule.services']).controller("LoginCtrl", ["$scope", "$location", "loginService", function($scope, $location, loginService){

    $scope.loginModel = {
                        loadingData:true,
                        inputUsername: undefined,
                        inputPassword: undefined,
                        curLoginUrl:"partials/login/default.html",
                        loginFailed:false,
                        loginServiceInformation:{}
                        };

    $scope.login = function(username, password) {
        loginService.login(username,password).then(function(data){
            $scope.loginModel.curLoginUrl = "partials/login/logoutButton.html";
        });
    }
    $scope.logout = function(username, password) {
        loginService.logout().then(function(data){
            $scope.loginModel.curLoginUrl = "partials/login/default.html";
            $scope.loginModel.inputPassword = undefined;
            $scope.loginModel.inputUsername = undefined;
            $location.path("home");
        });
    }
    $scope.switchUser = function(username, password) {
        loginService.logout().then(function(data){
            $scope.loginModel.curLoginUrl = "partials/login/loginForm.html";
            $scope.loginModel.inputPassword = undefined;
            $scope.loginModel.inputUsername = undefined;
        });
    }
    $scope.showLoginForm = function() {
        $scope.loginModel.curLoginUrl = "partials/login/loginForm.html";
    }
    $scope.hideLoginForm = function() {
        $scope.loginModel.curLoginUrl = "partials/login/default.html";
    }

    $scope.$watch(function(){return loginService.loginInformation}, function(newVal) {
        $scope.loginModel.loginServiceInformation = newVal;
        if(newVal.loggedIn)
        {
            $scope.loginModel.curLoginUrl = "partials/login/logoutButton.html";
        }
    }, true);
}]);

angular.module("loginModule", ["loginModule.services", "loginModule.controllers"]);

HTML

<div style="height:40px;z-index:200;position:relative">
    <div class="well">
        <form
            ng-submit="login(loginModel.inputUsername, loginModel.inputPassword)">
            <input
                type="text"
                ng-model="loginModel.inputUsername"
                placeholder="Username"/><br/>
            <input
                type="password"
                ng-model="loginModel.inputPassword"
                placeholder="Password"/><br/>
            <button
                class="btn btn-primary">Submit</button>
            <button
                class="btn"
                ng-click="hideLoginForm()">Cancel</button>
        </form>
        <div
            ng-show="loginModel.loginServiceInformation.loginAttemptFailed">
            Login attempt failed
        </div>
    </div>
</div>

上記のパーツを使用して画像を完成させるベース HTML:

<body ng-controller="NavigationCtrl" ng-init="initialize()">
        <div id="outerContainer" ng-controller="LoginCtrl">
            <div style="height:20px"></div>
            <ng-include src="'partials/header.html'"></ng-include>
            <div  id="contentRegion">
                <div ng-hide="loginModel.loginServiceInformation.loggedIn">Please login to continue.
                <br/><br/>
                This new version of this site is currently under construction.
                <br/><br/>
                If you need the legacy site and database <a href="legacy/">click here.</a></div>
                <div ng-view ng-show="loginModel.loginServiceInformation.loggedIn"></div>
            </div>
            <div class="clear"></div>
            <ng-include src="'partials/footer.html'"></ng-include>
        </div>
    </body>

DOM の上位に ng-controller を使用してログイン コントローラーを定義し、loggedIn 変数に基づいてページのボディ領域を変更できるようにしました。

ここではまだフォーム検証を実装していないことに注意してください。また、確かにAngularはまだかなり新しいので、この投稿の内容へのポインタは大歓迎です. これは RESTful ベースの実装ではないため、質問に直接答えることはできませんが、$http 呼び出しの上に構築されているため、$resources にも同じことが適用できると思います。

于 2013-08-20T02:27:07.373 に答える
5

私はUserApp用のAngularJS モジュールを作成しました。これは、あなたが求めるほとんどすべてのことを行います。次のいずれかを実行できます。

  1. モジュールを変更して関数を独自の API にアタッチする、または
  2. モジュールをユーザー管理 API、UserAppと組み合わせて使用​​する

https://github.com/userapp-io/userapp-angular

保護された/パブリック ルート、ログイン/ログアウト時の再ルーティング、ステータス チェック用のハートビート、Cookie へのセッション トークンの保存、イベントなどをサポートします。

UserApp を試してみたい場合は、Codecademy のコースを受講してください。

これがどのように機能するかの例を次に示します。

  • エラー処理付きのログインフォーム:

    <form ua-login ua-error="error-msg">
        <input name="login" placeholder="Username"><br>
        <input name="password" placeholder="Password" type="password"><br>
        <button type="submit">Log in</button>
        <p id="error-msg"></p>
    </form>
    
  • エラー処理付きの登録フォーム:

    <form ua-signup ua-error="error-msg">
      <input name="first_name" placeholder="Your name"><br>
      <input name="login" ua-is-email placeholder="Email"><br>
      <input name="password" placeholder="Password" type="password"><br>
      <button type="submit">Create account</button>
      <p id="error-msg"></p>
    </form>
    
  • どのルートをパブリックにするか、どのルートをログイン フォームにするかを指定する方法:

    $routeProvider.when('/login', {templateUrl: 'partials/login.html', public: true, login: true});
    $routeProvider.when('/signup', {templateUrl: 'partials/signup.html', public: true});
    

    ルートは、.otherwise()ログイン後にユーザーをリダイレクトする場所に設定する必要があります。例:

    $routeProvider.otherwise({redirectTo: '/home'});

  • ログアウト リンク:

    <a href="#" ua-logout>Log Out</a>

    (セッションを終了し、ログイン ルートにリダイレクトします)

  • ユーザー プロパティにアクセスします。

    ユーザー情報は、userサービスを使用してアクセスされます。例:user.current.email

    またはテンプレートで:<span>{{ user.email }}</span>

  • ログイン時にのみ表示される要素を非表示にします。

    <div ng-show="user.authorized">Welcome {{ user.first_name }}!</div>

  • 権限に基づいて要素を表示します。

    <div ua-has-permission="admin">You are an admin</div>

また、バックエンド サービスに対して認証するuser.token()には、セッション トークンを取得して AJAX 要求と共に送信するだけです。バックエンドで、UserApp API (UserApp を使用している場合) を使用して、トークンが有効かどうかを確認します。

何か助けが必要な場合は、私に知らせてください:)

于 2013-12-17T00:39:22.410 に答える
4

この記事を基本的にまとめた github リポジトリを作成しました: https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec

ng-login Github リポジトリ

プランカー

できる限り詳しく説明するように努めます。皆さんの助けになれば幸いです。

(1) app.js:アプリ定義での認証定数の作成

var loginApp = angular.module('loginApp', ['ui.router', 'ui.bootstrap'])
/*Constants regarding user login defined here*/
.constant('USER_ROLES', {
    all : '*',
    admin : 'admin',
    editor : 'editor',
    guest : 'guest'
}).constant('AUTH_EVENTS', {
    loginSuccess : 'auth-login-success',
    loginFailed : 'auth-login-failed',
    logoutSuccess : 'auth-logout-success',
    sessionTimeout : 'auth-session-timeout',
    notAuthenticated : 'auth-not-authenticated',
    notAuthorized : 'auth-not-authorized'
})

(2) Auth サービス: auth.js サービスには、以下のすべての機能が実装されています。$http サービスは、認証手順のためにサーバーと通信するために使用されます。ユーザーが特定のアクションの実行を許可されている場合、承認に関する機能も含まれています。

angular.module('loginApp')
.factory('Auth', [ '$http', '$rootScope', '$window', 'Session', 'AUTH_EVENTS', 
function($http, $rootScope, $window, Session, AUTH_EVENTS) {

authService.login() = [...]
authService.isAuthenticated() = [...]
authService.isAuthorized() = [...]
authService.logout() = [...]

return authService;
} ]);

(3) セッション:ユーザーデータを保持するためのシングルトン。ここでの実装はあなた次第です。

angular.module('loginApp').service('Session', function($rootScope, USER_ROLES) {

    this.create = function(user) {
        this.user = user;
        this.userRole = user.userRole;
    };
    this.destroy = function() {
        this.user = null;
        this.userRole = null;
    };
    return this;
});

(4) 親コントローラー:これをアプリケーションの「メイン」機能と見なします。すべてのコントローラーはこのコントローラーから継承し、このアプリの認証のバックボーンです。

<body ng-controller="ParentController">
[...]
</body>

(5) アクセス制御:特定のルートでのアクセスを拒否するには、2 つの手順を実装する必要があります。

a) 以下に示すように、UI ルーターの $stateProvider サービスで、各ルートへのアクセスを許可されたロールのデータを追加します (同じことが ngRoute でも機能します)。

.config(function ($stateProvider, USER_ROLES) {
  $stateProvider.state('dashboard', {
    url: '/dashboard',
    templateUrl: 'dashboard/index.html',
    data: {
      authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
    }
  });
})

b) $rootScope.$on('$stateChangeStart') で、ユーザーが承認されていない場合に状態の変更を防ぐ関数を追加します。

$rootScope.$on('$stateChangeStart', function (event, next) {
    var authorizedRoles = next.data.authorizedRoles;
    if (!Auth.isAuthorized(authorizedRoles)) {
      event.preventDefault();
      if (Auth.isAuthenticated()) {
        // user is not allowed
        $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
      } else {d
        // user is not logged in
        $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated);
      }
    }
});

(6) 認証インターセプター:これは実装されていますが、このコードのスコープではチェックできません。各 $http リクエストの後、このインターセプターはステータス コードをチェックし、以下のいずれかが返された場合、イベントをブロードキャストして、ユーザーに再度ログインを強制します。

angular.module('loginApp')
.factory('AuthInterceptor', [ '$rootScope', '$q', 'Session', 'AUTH_EVENTS',
function($rootScope, $q, Session, AUTH_EVENTS) {
    return {
        responseError : function(response) {
            $rootScope.$broadcast({
                401 : AUTH_EVENTS.notAuthenticated,
                403 : AUTH_EVENTS.notAuthorized,
                419 : AUTH_EVENTS.sessionTimeout,
                440 : AUTH_EVENTS.sessionTimeout
            }[response.status], response);
            return $q.reject(response);
        }
    };
} ]);

PS 1回目の記事で述べたフォームデータ自動入力のバグは、directives.jsに含まれるディレクティブを追加することで簡単に回避できます。

PS2このコードは、ユーザーが簡単に微調整して、さまざまなルートを表示したり、表示されることを意図していないコンテンツを表示したりできます。ロジックはサーバー側で実装する必要があります。これは、ng-app で適切に表示する方法にすぎません。

于 2014-10-05T01:46:25.467 に答える