interface utilisateur angulaire-authentification de connexion du routeur
Je suis nouveau à AngularJS, et je suis un peu confus de la façon dont je peux utiliser angular- "ui-router" dans le scénario suivant:
Je construis une application web qui se compose de deux sections. La première section est la page d'accueil avec ses vues de connexion et d'inscription, et la deuxième section est le tableau de bord (après une connexion réussie).
, j'ai créé un index.html
pour la maison avec ses angulaire application et ui-router
config pour gérer /login
et /signup
vues,
et il y a un autre fichier dashboard.html
pour le section du tableau de bord avec son application et ui-router
config pour gérer de nombreuses sous-vues.
Maintenant, j'ai terminé la section Tableau de bord et je ne sais pas comment combiner les deux sections avec leurs différentes applications angulaires. Comment puis-je dire à l'application home de rediriger vers l'application dashboard?
10 réponses
Je suis en train de faire une démo plus agréable ainsi que de nettoyer certains de ces services dans un module utilisable, mais voici ce que j'ai trouvé. C'est un processus complexe pour contourner certaines mises en garde, alors accrochez-vous. Vous aurez besoin de décomposer cela en plusieurs morceaux.
Tout d'Abord, vous avez besoin d'un service pour stocker l'identité de l'utilisateur. J'appelle ça principal
. Il peut être vérifié pour voir si l'utilisateur est connecté, et sur demande, il peut résolvez un objet qui représente les informations essentielles sur l'identité de l'utilisateur. Cela peut être ce dont vous avez besoin, mais l'essentiel serait un nom d'affichage, un nom d'utilisateur, éventuellement un e-mail et les rôles auxquels un utilisateur appartient (si cela s'applique à votre application). Principal a également des méthodes pour faire des vérifications de rôle.
.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;
}
};
}
])
Deuxièmement, vous avez besoin d'un service qui vérifie l'état dans lequel l'utilisateur veut aller, s'assure qu'il est connecté (si nécessaire; pas nécessaire pour la connexion, la réinitialisation du mot de passe, etc.), et puis effectue une vérification de rôle (si votre application en a besoin). S'ils ne sont pas authentifiés, envoyez-les à la page de connexion. S'ils sont authentifiés, mais échouent à une vérification de rôle, envoyez-les à une page d'Accès refusé. J'appelle ce service 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');
}
}
});
}
};
}
])
Maintenant, tout ce que vous devez faire est de les écouter sur ui-router
's $stateChangeStart
. Cela vous donne une chance d'examiner l'état actuel, l'état où ils veulent aller, et insérez votre demande d'autorisation. Si elle échoue, vous pouvez annuler la transition de la route ou passer à un itinéraire différent.
.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();
});
}
]);
La partie délicate du suivi de l'identité d'un utilisateur est de la rechercher si vous vous êtes déjà authentifié (par exemple, vous visitez la page après une session précédente, et avez enregistré un jeton d'authentification dans un cookie, ou peut-être avez-vous actualisé une page ou déposé sur une URL à partir d'un lien). En raison de la façon dont ui-router
fonctionne, vous devez faire Votre résolution d'identité une fois, avant que vos vérifications d'authentification. Vous pouvez le faire en utilisant l'option resolve
dans votre configuration d'état. J'ai un état parent pour le site dont tous les États héritent, ce qui oblige le principal à être résolu avant que toute autre chose ne se produise.
$stateProvider.state('site', {
'abstract': true,
resolve: {
authorize: ['authorization',
function(authorization) {
return authorization.authorize();
}
]
},
template: '<div ui-view />'
})
Il y a un autre problème ici... resolve
n'est appelé qu'une seule fois. Une fois votre promesse de recherche d'identité terminée, le délégué de résolution ne sera plus exécuté. Nous devons donc effectuer vos vérifications d'authentification à deux endroits: une fois conformément à votre promesse d'identité résolue dans resolve
, qui couvre la première fois que votre application se charge, et une fois dans $stateChangeStart
si la résolution a été faite, qui couvre chaque fois que vous naviguez autour des États.
OK, alors, qu'avons-nous fait jusqu'à présent?
- On vérifie lorsque l'application se charge si l'utilisateur est connecté.
- Nous suivons les informations sur l'utilisateur connecté.
- nous les redirigeons vers l'état de connexion pour les États qui nécessitent que l'utilisateur soit connecté.
- nous les redirigeons vers un État d'Accès refusé s'ils n'ont pas l'autorisation d'y accéder.
- Nous avons un mécanisme pour rediriger les utilisateurs vers l'état d'origine qu'ils demandé, si nous avions besoin d'eux pour se connecter.
- nous pouvons déconnecter un utilisateur (doit être câblé de concert avec n'importe quel code client ou serveur qui gère votre ticket d'authentification).
- Nous n'avons pas besoin de renvoyer les utilisateurs vers la page de connexion chaque fois qu'ils rechargent leur navigateur ou déposent un lien.
Où allons-nous à partir d'ici? Eh bien, vous pouvez organiser vos états en régions qui nécessitent une connexion. Vous pouvez exiger des utilisateurs authentifiés / autorisés en ajoutant data
avec roles
à ces états (ou un parent de vous si vous voulez utiliser l'héritage). Ici, nous limitons une ressource aux administrateurs:
.state('restricted', {
parent: 'site',
url: '/restricted',
data: {
roles: ['Admin']
},
views: {
'content@': {
templateUrl: 'restricted.html'
}
}
})
Maintenant, vous pouvez contrôler état par état ce que les utilisateurs peuvent accéder à un itinéraire. D'autres préoccupations? Peut-être varier seulement une partie d'une vue selon qu'ils sont connectés ou non? Ce n'est rien. Utilisez le {[18] } ou même principal.isInRole()
avec l'une des nombreuses façons dont vous pouvez afficher conditionnellement un modèle ou un élément.
Tout d'abord, injectez principal
dans un contrôleur ou autre, et collez-le à la portée de sorte que vous pouvez l'utiliser facilement dans votre vue:
.scope('HomeCtrl', ['$scope', 'principal',
function($scope, principal)
{
$scope.principal = principal;
});
Afficher ou masquer un élément:
<div ng-show="principal.isAuthenticated()">
I'm logged in
</div>
<div ng-hide="principal.isAuthenticated()">
I'm not logged in
</div>
Etc., ainsi de suite, ainsi de suite. Quoi qu'il en soit, dans votre exemple d'application, vous auriez un État pour la page d'accueil qui permettrait aux utilisateurs non authentifiés de passer. Ils pourraient avoir des liens vers les états de connexion ou d'inscription, ou avoir ces formulaires intégrés dans cette page. Ce qui convient à vous.
Les pages du tableau de bord peuvent toutes hériter d'un État qui nécessite que les utilisateurs soient connectés et, par exemple, un User
membre de rôle. Toutes les choses d'autorisation dont nous avons parlé découleraient de là.
Les solutions affichées jusqu'à présent sont inutilement compliquées, à mon avis. Il y a un moyen plus simple. La documentation de ui-router
dit écouter {[3] } et utiliser $urlRouter.sync()
pour vérifier une transition d'état, l'arrêter ou la reprendre. Mais même cela ne fonctionne pas.
Cependant, voici deux alternatives simples. Choisissez un:
Solution 1: écoute sur $locationChangeSuccess
Vous pouvez écouter $locationChangeSuccess
et vous pouvez effectuer une certaine logique, même une logique asynchrone là-bas. Basé sur cette logique, vous pouvez laisser la fonction retourner undefined, ce qui fera que la transition d'état se poursuivra normalement, ou vous pouvez faire $state.go('logInPage')
, Si l'utilisateur doit être authentifié. Voici un exemple:
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')
})
})
})
Gardez à l'esprit que cela n'empêche pas réellement le chargement de l'état cible, mais qu'il redirige vers la page de connexion si l'utilisateur n'est pas autorisé. C'est correct puisque la protection réelle est sur le serveur, de toute façon.
Solution 2: utiliser l'état resolve
Dans cette solution, vous utilisez ui-router
Résoudre la fonctionnalité .
Vous rejetez essentiellement la promesse dans resolve
si l'utilisateur n'est pas authentifié, puis les redirigez vers la page de connexion.
Voici comment ça se passe:
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()
}
}
}
)
Contrairement à la première solution, cette solution empêche en fait le chargement de l'état cible.
La solution La plus simple est d'utiliser $stateChangeStart
et event.preventDefault()
pour annuler le changement d'état lorsque l'utilisateur n'est pas authentifié et rediriger lui à la auth état qui est la page de connexion.
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');
}
});
}]
);
Je pense que vous avez besoin d'un service
qui gère le processus d'authentification (et son stockage).
Dans ce service, vous aurez besoin de méthodes de base:
isAuthenticated()
login()
logout()
- etc...
Ce service doit être injecté dans vos contrôleurs de chaque module :
- dans la section de votre tableau de bord, utilisez ce service pour vérifier si l'utilisateur est authentifié (méthode
service.isAuthenticated()
). si ce n'est pas le cas, redirigez vers / login - dans votre section de connexion, juste utilisez les données du formulaire pour authentifier l'utilisateur via votre méthode
service.login()
Un bon exemple robuste de ce comportement est le projet angular-app et plus précisément son module de sécurité {[30] } qui est basé sur le module intercepteur HTTP Auth
J'espère que cela aide
J'ai créé ce module pour aider à faire de ce processus un morceau de gâteau
Vous pouvez faire des choses comme:
$routeProvider
.state('secret',
{
...
permissions: {
only: ['admin', 'god']
}
});
Ou aussi
$routeProvider
.state('userpanel',
{
...
permissions: {
except: ['not-logged-in']
}
});
Il est tout neuf, mais la peine de vérifier!
Je voulais partager une autre solution fonctionnant avec le routeur ui 1.0.0.X
Comme vous le savez peut-être, stateChangeStart et stateChangeSuccess sont maintenant obsolètes. https://github.com/angular-ui/ui-router/issues/2655
À la place, vous devriez utiliser $ transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html
C'est ainsi que je l'ai réalisé:
D'abord j'AI et AuthService avec quelques utiles fonctions
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;
}]);
Alors j'ai cette configuration:
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");
}
});
}]);
, Vous pouvez voir que j'utilise
data: {
authRequired: true
}
Pour marquer l'état accessible uniquement s'il est authentifié.
Ensuite, sur le .run j'utilise les transitions pour vérifier l'état autheticated
$transitions.onStart({
to: function (state) {
return state.data != null && state.data.authRequired === true;
}
}, function () {
if (!AuthService.isAuthenticated()) {
return $state.target("login");
}
});
Je construis cet exemple en utilisant du code trouvé dans la documentation $transitions. Je suis assez nouveau avec le routeur d'interface utilisateur mais cela fonctionne.
J'espère que cela peut aider n'importe qui.
Voici comment nous sommes sortis de la boucle de routage infinie et avons toujours utilisé $state.go
au lieu de $location.path
if('401' !== toState.name) {
if (principal.isIdentityResolved()) authorization.authorize();
}
, j'ai une autre solution: cette solution fonctionne parfaitement lorsque vous avez uniquement le contenu que vous souhaitez afficher lorsque vous êtes connecté. Définissez une règle où vous vérifiez si vous êtes connecté et que ce n'est pas le chemin des routes de la liste blanche.
$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');
}
});
Dans mon exemple, je demande si Je ne suis pas connecté et que l'itinéraire actuel que je veux acheminer ne fait pas partie de `/login', car mes itinéraires de liste blanche sont les suivants
/login/signup // registering new user
/login/signin // login to app
J'ai donc un accès instantané à ces deux routes et tous les autres itinéraires seront vérifiés si vous sont en ligne.
Voici tout mon fichier de routage pour le module de connexion
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 */ }
est la syntaxe ES6, utilisez à la place function() { /* code */ }
D'abord, vous aurez besoin d'un service que vous pouvez injecter dans vos contrôleurs qui a une idée de l'état d'authentification de l'application. La persistance des détails d'authentification avec le stockage local est un moyen décent de l'aborder.
Ensuite, vous devrez vérifier l'état d'auth juste avant que l'état ne change. Étant donné que votre application a des pages qui doivent être authentifiées et d'autres qui ne le font pas, créez une route parent qui vérifie l'authentification et faites en sorte que toutes les autres pages qui nécessitent la même chose soient un enfant de ce parent.
Enfin, vous aurez besoin d'une façon de savoir si votre utilisateur actuellement connecté peut effectuer certaines opérations. Cela peut être réalisé en ajoutant une fonction' can ' à votre service d'authentification. Peut prend deux paramètres: - action requis (ie 'manage_dashboards" ou "create_new_dashboard') - objet-optionnel-objet en cours d'utilisation. Par exemple, si vous aviez un objet tableau de bord, vous pouvez vérifier si le tableau de bord.ownerId === loggedInUser.id. (bien sûr, les informations transmises par le client ne doit jamais être approuvé et vous devez toujours vérifier cela sur le serveur avant de l'écrire dans votre base de données).
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');
}
}
}]
** avertissement: le code ci - dessus Est pseudo-code et est livré sans garantie * *
Utilisez $http Intercepteur
En utilisant un intercepteur $ http, vous pouvez envoyer des en-têtes au Back-end ou l'inverse et effectuer vos vérifications de cette façon.
Excellent article sur l' $http intercepteurs
Exemple:
$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);
}
};
});
Mettez ceci dans votre .config ou .la fonction d'exécution.