javascript - start - autenticación de inicio de sesión angular ui-router
ui router change start (10)
Aquí es cómo salimos del bucle de enrutamiento infinito y todavía usamos $state.go
lugar de $location.path
if(''401'' !== toState.name) {
if (principal.isIdentityResolved()) authorization.authorize();
}
Soy nuevo en AngularJS, y estoy un poco confundido acerca de cómo puedo usar "enrutador ui" angular en el siguiente escenario:
Estoy construyendo una aplicación web que consta de dos secciones. La primera sección es la página de inicio con sus vistas de inicio de sesión y registro, y la segunda sección es el panel (después de un inicio de sesión exitoso).
He creado un index.html
para la sección de inicio con su aplicación angular y la configuración de ui-router
para manejar las vistas de /login
y /signup
, y hay otro archivo dashboard.html
para la sección de panel de control con su aplicación y la configuración de ui-router
para manejar muchas sub vistas.
Ahora terminé la sección del tablero y no sé cómo combinar las dos secciones con sus diferentes aplicaciones angulares. ¿Cómo podría decirle a la aplicación de inicio que redirija a la aplicación del panel de control?
Creé este módulo para ayudar a que este proceso sea fácil.
Puedes hacer cosas como:
$routeProvider
.state(''secret'',
{
...
permissions: {
only: [''admin'', ''god'']
}
});
O tambien
$routeProvider
.state(''userpanel'',
{
...
permissions: {
except: [''not-logged-in'']
}
});
Es nuevo, pero vale la pena echarle un vistazo!
Creo que necesita un service
que maneje el proceso de autenticación (y su almacenamiento).
En este servicio necesitarás algunos métodos básicos:
-
isAuthenticated()
-
login()
-
logout()
- etc ...
Este servicio debe ser inyectado en sus controladores de cada módulo:
- En la sección de su panel, use este servicio para verificar si el usuario está autenticado (método
service.isAuthenticated()
). Si no, redirigir a / login - En su sección de inicio de sesión, solo use los datos del formulario para autenticar al usuario a través del método
service.login()
Un buen y robusto ejemplo de este comportamiento es la angular-app del proyecto y, específicamente, su módulo de seguridad, que se basa en el impresionante Módulo de intercepción de autenticación HTTP.
Espero que esto ayude
Estoy en el proceso de hacer una demostración más agradable, así como de limpiar algunos de estos servicios en un módulo utilizable, pero esto es lo que he encontrado. Este es un proceso complejo para solucionar algunas advertencias, así que aguanta. Tendrá que dividir esto en varias piezas.
Echa un vistazo a este plunk .
Primero, necesitas un servicio para almacenar la identidad del usuario. Yo llamo a este principal
. Se puede verificar para ver si el usuario ha iniciado sesión y, si lo solicita, puede resolver un objeto que representa la información esencial sobre la identidad del usuario. Esto puede ser lo que necesite, pero lo esencial sería un nombre para mostrar, un nombre de usuario, posiblemente un correo electrónico y las funciones a las que pertenece un usuario (si esto se aplica a su aplicación). Principal también tiene métodos para hacer verificaciones de roles.
.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;
}
};
}
])
En segundo lugar, necesita un servicio que verifique el estado al que quiere ir el usuario, se asegure de que haya iniciado sesión (si es necesario; no es necesario para iniciar sesión, restablecer la contraseña, etc.) y luego verifica su función (si su aplicación necesita esto). Si no están autenticados, envíelos a la página de inicio de sesión. Si están autenticados, pero no pasan una verificación de roles, envíelos a una página de acceso denegado. Yo llamo a este servicio de 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'');
}
}
});
}
};
}
])
Ahora todo lo que necesita hacer es escuchar en $stateChangeStart
ui-router
. Esto le da la oportunidad de examinar el estado actual, el estado al que quieren ir e insertar su verificación de autorización. Si falla, puede cancelar la transición de la ruta o cambiar a una ruta diferente.
.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 parte difícil acerca de rastrear la identidad de un usuario es buscarla si ya se ha autenticado (por ejemplo, está visitando la página después de una sesión anterior y guardó un token de autenticación en una cookie, o tal vez actualizó una página). soltado en una URL desde un enlace). Debido a la forma en que funciona ui-router
, debe hacer su resolución de identidad una vez, antes de que se realicen las comprobaciones de autenticación. Puede hacerlo usando la opción de resolve
en la configuración de su estado. Tengo un estado principal para el sitio del que todos los estados heredan, lo que obliga a resolver el principal antes de que ocurra algo más.
$stateProvider.state(''site'', {
''abstract'': true,
resolve: {
authorize: [''authorization'',
function(authorization) {
return authorization.authorize();
}
]
},
template: ''<div ui-view />''
})
Hay otro problema aquí ... resolve
solo se llama una vez. Una vez que se complete su promesa de búsqueda de identidad, no se ejecutará nuevamente el delegado de resolución. Por lo tanto, tenemos que realizar sus comprobaciones de autenticación en dos lugares: una vez que se haya resuelto su compromiso de identidad, que cubra la primera vez que se cargue su aplicación, y una vez en $stateChangeStart
si se ha realizado la resolución, que abarca cada vez que navega estados
OK, entonces, ¿qué hemos hecho hasta ahora?
- Verificamos si la aplicación se carga si el usuario ha iniciado sesión.
- Rastreamos información sobre el usuario que ha iniciado sesión.
- Los redirigimos para que inicien sesión en los estados que requieren que el usuario inicie sesión.
- Los redirigimos a un estado de acceso denegado si no tienen autorización para acceder a él.
- Tenemos un mecanismo para redirigir a los usuarios al estado original que solicitaron, si los necesitamos para iniciar sesión.
- Podemos cerrar la sesión de un usuario (debe conectarse de forma conjunta con cualquier código de cliente o servidor que gestione su ticket de autenticación).
- No necesitamos enviar a los usuarios a la página de inicio de sesión cada vez que vuelven a cargar su navegador o colocan un enlace.
¿A dónde vamos desde aquí? Bueno, puede organizar sus estados en regiones que requieren inicio de sesión. Puede requerir usuarios autenticados / autorizados agregando data
con roles
a estos estados (o uno de sus padres, si desea usar la herencia). Aquí, restringimos un recurso a los administradores:
.state(''restricted'', {
parent: ''site'',
url: ''/restricted'',
data: {
roles: [''Admin'']
},
views: {
''content@'': {
templateUrl: ''restricted.html''
}
}
})
Ahora puede controlar estado por estado lo que los usuarios pueden acceder a una ruta. ¿Alguna otra preocupación? ¿Es posible que varíe solo una parte de una vista en función de si han iniciado sesión o no? No hay problema. Utilice principal.isAuthenticated()
o incluso principal.isInRole()
con cualquiera de las numerosas formas en que puede mostrar condicionalmente una plantilla o un elemento.
Primero, inyecte el principal
en un controlador o lo que sea, y péguelo en el alcance para que pueda usarlo fácilmente en su vista:
.scope(''HomeCtrl'', [''$scope'', ''principal'',
function($scope, principal)
{
$scope.principal = principal;
});
Mostrar u ocultar un elemento:
<div ng-show="principal.isAuthenticated()">
I''m logged in
</div>
<div ng-hide="principal.isAuthenticated()">
I''m not logged in
</div>
Etc., y así sucesivamente. De todos modos, en su aplicación de ejemplo, tendría un estado para la página de inicio que dejaría a los usuarios no autenticados. Podrían tener enlaces a los estados de inicio de sesión o registro, o tener esos formularios integrados en esa página. Lo que te venga bien.
Todas las páginas del panel de control podrían heredarse de un estado que requiere que los usuarios estén conectados y, digamos, ser un miembro de rol de User
. Todas las cosas de autorización que hemos discutido fluirían de allí.
La solución más sencilla es usar $stateChangeStart
y event.preventDefault()
para cancelar el cambio de estado cuando el usuario no está autenticado y redirigirlo al estado de autenticación que es la página de inicio de sesión.
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'');
}
});
}]
);
Las soluciones publicadas hasta ahora son innecesariamente complicadas, en mi opinión. Hay una forma más sencilla. La documentación de ui-router
dice escuchar $locationChangeSuccess
y usar $urlRouter.sync()
para verificar una transición de estado, detenerla o reanudarla. Pero incluso eso en realidad no funciona.
Sin embargo, aquí hay dos alternativas simples. Elegir uno:
Solución 1: escuchar en $locationChangeSuccess
Puede escuchar $locationChangeSuccess
y puede realizar alguna lógica, incluso lógica asíncrona allí. Basándose en esa lógica, puede dejar que la función vuelva indefinida, lo que hará que la transición de estado continúe con normalidad, o puede hacer $state.go(''logInPage'')
, si el usuario necesita ser autenticado. Aquí hay un ejemplo:
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'')
})
})
})
Tenga en cuenta que esto no impide que el estado de destino se cargue, pero se redirige a la página de inicio de sesión si el usuario no está autorizado. Eso está bien ya que la protección real está en el servidor, de todos modos.
Solución 2: utilizando la resolve
estado
En esta solución, utiliza la función de resolución de ui-router
.
Básicamente, usted rechaza la promesa de resolve
si el usuario no está autenticado y luego los redirige a la página de inicio de sesión.
Así es como va:
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()
}
}
}
)
A diferencia de la primera solución, esta solución en realidad evita que se cargue el estado de destino.
Primero, necesitará un servicio que pueda inyectar en sus controladores que tenga alguna idea del estado de autenticación de la aplicación. La persistencia de los detalles de autenticación con el almacenamiento local es una forma decente de abordarlo.
A continuación, deberá verificar el estado de autenticación justo antes de que cambie el estado. Ya que su aplicación tiene algunas páginas que necesitan ser autenticadas y otras que no, cree una ruta principal que verifique la autenticación, y haga que todas las demás páginas que requieren la misma sean un hijo de ese padre.
Finalmente, necesitará alguna forma de saber si su usuario que ha iniciado sesión actualmente puede realizar ciertas operaciones. Esto se puede lograr agregando una función ''can'' a su servicio de autenticación. Can toma dos parámetros: - acción - requerida - (es decir, ''manage_dashboards'' o ''create_new_dashboard'') - object - opcional - objeto que se está operando Por ejemplo, si tenía un objeto de panel de control, es posible que desee verificar si el panel de control.ownerId === ha registradoInsuario.id. (Por supuesto, la información transmitida desde el cliente nunca debe ser confiable y siempre debe verificarla en el servidor antes de escribirla en su base de datos).
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'');
}
}
}]
** DESCARGO DE RESPONSABILIDAD: El código anterior es un pseudo-código y viene sin garantías **
Quería compartir otra solución trabajando con el enrutador ui 1.0.0.X
Como ya sabrá, stateChangeStart y stateChangeSuccess ahora están en desuso. https://github.com/angular-ui/ui-router/issues/2655
En su lugar, debe usar $ transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html
Así es como lo logré:
Primero tengo y AuthService con algunas funciones útiles.
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;
}]);
Entonces tengo esta configuración:
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");
}
});
}]);
Puedes ver que yo uso
data: {
authRequired: true
}
Para marcar el estado solo es accesible si está autenticado.
luego, en el .run utilizo las transiciones para comprobar el estado autenticado
$transitions.onStart({
to: function (state) {
return state.data != null && state.data.authRequired === true;
}
}, function () {
if (!AuthService.isAuthenticated()) {
return $state.target("login");
}
});
Construyo este ejemplo usando un código que se encuentra en la documentación de $ transiciones. Soy bastante nuevo con el enrutador ui pero funciona.
Espero que pueda ayudar a nadie.
Tengo otra solución: esa solución funciona perfectamente cuando solo tiene contenido que desea mostrar cuando está conectado. Defina una regla en la que verifique si ha iniciado sesión y no la ruta de las rutas de la lista blanca.
$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'');
}
});
En mi ejemplo, pregunto si no he iniciado sesión y la ruta actual que quiero enrutar no es parte de `/ login '', porque mis rutas en la lista blanca son las siguientes
/login/signup // registering new user
/login/signin // login to app
así que tengo acceso instantáneo a estas dos rutas y se comprobará cualquier otra ruta si está en línea.
Aquí está todo mi archivo de enrutamiento para el módulo de inicio de sesión
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 */ }
es sintaxis ES6, use en su lugar la function() { /* code */ }
Use $ http Interceptor
Al usar un interceptor $ http, puede enviar encabezados a Back-end o al revés y hacer sus cheques de esa manera.
Gran artículo sobre $ interceptores http
Ejemplo:
$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);
}
};
});
Ponga esto en su función .config o .run.