javascript - services - consumir servicio rest angular 4
Autenticación AngularJS+API RESTful (4)
Esto está tomado de la publicación de mi blog sobre la autorización de rutas url y la seguridad de los elementos jonsamwell.com/url-route-authorization-and-security-in-angular pero resumiré brevemente los puntos principales :-)
La seguridad en la aplicación web frontend es simplemente una medida inicial para detener a Joe Public; sin embargo, cualquier usuario con algún conocimiento de la web puede eludirlo, por lo que siempre debe tener también seguridad en el servidor.
La principal preocupación en materia de seguridad en angular es la seguridad de la ruta, afortunadamente cuando se define una ruta en angular se crea un objeto, un objeto que puede tener otras propiedades. La piedra angular de mi enfoque es agregar un objeto de seguridad a este objeto de ruta que básicamente define los roles que debe tener el usuario para poder acceder a una ruta en particular.
// 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''
});
Todo el enfoque se centra en un servicio de autorización que básicamente verifica si el usuario tiene los permisos necesarios. Este servicio resume las preocupaciones de las otras partes de esta solución para hacer con el usuario y su permiso real que se habría recuperado del servidor durante el inicio de sesión. Si bien el código es bastante detallado, está completamente explicado en mi blog. Sin embargo, básicamente maneja la verificación de permisos y dos modos de autorización. El primero es que el usuario debe tener al menos uno de los permisos definidos, el segundo es que el usuario debe tener todos los permisos definidos.
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;
};
Ahora que una ruta tiene seguridad, necesita una forma de determinar si un usuario puede acceder a la ruta cuando se ha iniciado un cambio de ruta. Para ello, interceptaremos la solicitud de cambio de ruta, examinaremos el objeto de ruta (con nuestro nuevo objeto de acceso) y, si el usuario no puede acceder a la vista, reemplazaremos la ruta por otra.
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();
}
}
});
}]);
La clave aquí es realmente ''.replace ()'' ya que esto reemplaza la ruta actual (la que no tienen derecho a ver) con la ruta a la que los estamos redirigiendo. Esto detiene cualquiera y luego navega de regreso a la ruta no autorizada.
Ahora podemos interceptar rutas, podemos hacer algunas cosas interesantes, como redireccionar después de un inicio de sesión si un usuario aterrizó en una ruta en la que necesitaban iniciar sesión.
La segunda parte de la solución es poder ocultar / mostrar el elemento UI al usuario dependiendo de sus derechos. Esto se logra a través de una directiva simple.
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);
}
}
};
}]);
Entonces seguro que un elemento como ese:
<button type="button" access="CanEditUser, Admin" access-permission-type="AtLeastOne">Save User</button>
Lea mi jonsamwell.com/url-route-authorization-and-security-in-angular para obtener una visión general más detallada del enfoque.
Comunicación angular + RESTful del lado del cliente con API para auth / (re) enrutamiento
Esto se ha cubierto en algunas preguntas diferentes y en algunos tutoriales diferentes, pero todos los recursos previos que he encontrado no llegan al final.
En una cáscara de nuez, necesito
- Inicie sesión mediante POST desde
http://client.foo
ahttp://api.foo/login
- Tener un estado de GUI / componente "conectado" para el usuario que proporciona una ruta de
logout
- Ser capaz de "actualizar" la interfaz de usuario cuando el usuario cierra la sesión / cierra la sesión. Este ha sido el más frustrante
- Asegure mis rutas para verificar el estado autenticado (en caso de que lo necesiten) y redirija al usuario a la página de inicio de sesión según corresponda
Mis problemas son
- Cada vez que navego a una página diferente, necesito hacer la llamada a
api.foo/status
para determinar si el usuario ha iniciado sesión o no (ATM estoy usando Express para las rutas) Esto causa un tropiezo ya que Angular determina cosas comong-show="user.is_authenticated"
- Cuando inicio o cierro sesión con éxito, necesito actualizar la página (no quiero tener que hacer esto) para completar cosas como
{{user.first_name}}
, o en el caso de cerrar la sesión, vaciar ese valor fuera.
// Sample response from `/status` if successful
{
customer: {...},
is_authenticated: true,
authentication_timeout: 1376959033,
...
}
Lo que he intentado
- http://witoldsz.github.io/angular-http-auth/ 1
- http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/ 2
- https://github.com/mgonto/restangular (Por mi vida no pude encontrar la manera de
POST
conpost data
y noquery params
. Los documentos no dieron ningún resultado al respecto.
Por qué siento que estoy perdiendo la cabeza
- Parece que cada tutorial se basa en alguna base de datos (muchas de las soluciones Mongo, Couch, PHP + MySQL, ad infinitum), y ninguna depende exclusivamente de la comunicación con una API RESTful para persistir en los estados registrados. Una vez que haya iniciado sesión, se enviarán POSTs / GET adicionales conCredentials
withCredentials:true
, por lo que ese no es el problema - No puedo encontrar CUALQUIER ejemplo / tutoriales / repos que realicen Angular + REST + Auth, sin un lenguaje de fondo.
No soy muy orgulloso
Es cierto que soy nuevo en Angular, y no me sorprendería si me estoy acercando a esto de una manera ridícula; Estaría encantado si alguien sugiriera una alternativa, incluso si es una sopa a las nueces.
Estoy usando Express
principalmente porque realmente amo Jade
y Stylus
: no estoy casado con el enrutamiento Express
y lo abandonaré si lo que quiero hacer solo es posible con el enrutamiento de Angular.
Gracias de antemano por cualquier ayuda que alguien pueda brindar. Y por favor no me pidas que lo busque en Google, porque tengo alrededor de 26 páginas de enlaces morados. ;-)
1 Esta solución se basa en el simulacro $ httpBackend de Angular, y no está claro cómo hacerlo hablar con un servidor real.
2 Esto fue lo más cercano, pero dado que tengo una API existente con la que necesito autenticarme, no pude usar la''estrategia local ''del pasaporte, y me pareció una locura escribir un servicio OAUTH ... que solo yo tenía la intención de usar.
He creado un repositorio github resumiendo este artículo básicamente: https://medium.com/opinionated-angularjs/techniques-for-authentication-in-angularjs-applications-7bbf0346acec
Trataré de explicar lo mejor posible, espero ayudar a algunos de ustedes:
(1) app.js: creación de constantes de autenticación en la definición de la aplicación
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) Servicio de autenticación: todas las siguientes funciones se implementan en el servicio auth.js. El servicio $ http se usa para comunicarse con el servidor para los procedimientos de autenticación. También contiene funciones de autorización, es decir, si el usuario puede realizar una determinada acción.
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) Sesión: Un singleton para guardar los datos del usuario. La implementación aquí depende de ti.
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) Controlador principal: considere esto como la función "principal" de su aplicación, todos los controladores heredan de este controlador, y es la columna vertebral de la autenticación de esta aplicación.
<body ng-controller="ParentController">
[...]
</body>
(5) Control de acceso: para denegar el acceso en ciertas rutas, se deben implementar 2 pasos:
a) Agregue datos de las funciones permitidas para acceder a cada ruta, en el servicio $ stateProvider del enrutador ui, como se puede ver a continuación (lo mismo puede funcionar para ngRoute).
.config(function ($stateProvider, USER_ROLES) {
$stateProvider.state(''dashboard'', {
url: ''/dashboard'',
templateUrl: ''dashboard/index.html'',
data: {
authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor]
}
});
})
b) En $ rootScope. $ on (''$ stateChangeStart'') agregue la función para evitar el cambio de estado si el usuario no está autorizado.
$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) Interceptor de autenticación: esto se implementa, pero no se puede verificar en el alcance de este código. Después de cada solicitud $ http, este interceptor verifica el código de estado, si se devuelve uno de los siguientes, luego transmite un evento para obligar al usuario a iniciar sesión de nuevo.
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 Se puede evitar fácilmente un error con el autocompletado de datos de formulario como se indica en el primer artículo al agregar la directiva que se incluye en directives.js.
PS2 Este código puede ser ajustado fácilmente por el usuario, para permitir que se vean diferentes rutas, o para mostrar contenido que no debe mostrarse. La lógica DEBE implementarse desde el servidor, esta es solo una forma de mostrar las cosas correctamente en su aplicación ng.
No he usado $ resource porque estoy creando manualmente mis llamadas de servicio para mi aplicación. Dicho esto, he manejado el inicio de sesión al tener un servicio que depende de todos los otros servicios que obtienen algún tipo de datos de inicialización. Cuando el inicio de sesión tiene éxito, se desencadena la inicialización de todos los servicios.
Dentro del alcance de mi controlador miro el loginServiceInformation y llevo algunas propiedades del modelo en consecuencia (para activar el ng-show / hide apropiado). Con respecto al enrutamiento, estoy usando el enrutamiento integrado de Angular y simplemente tengo un ng-hide basado en el booleano loggedIn que se muestra aquí, muestra texto para solicitar el inicio de sesión o el div con el atributo ng-view (si no está conectado) Inmediatamente después de iniciar sesión estás en la página correcta, actualmente cargo datos para todas las vistas, pero creo que esto podría ser más selectivo si es necesario.
//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"]);
El 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>
El HTML base que usa las partes de arriba para completar la imagen:
<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>
Tengo el controlador de inicio de sesión definido con un ng-controller más arriba en el DOM para poder cambiar el área del cuerpo de mi página en función de la variable loggedIn.
Tenga en cuenta que aún no he implementado la validación del formulario. También es cierto que todavía está bastante fresco para Angular, por lo que cualquier sugerencia sobre las cosas en este post es bienvenida. Aunque esto no responde la pregunta directamente, ya que no se trata de una implementación basada en RESTful, creo que la misma se puede adaptar a $ recursos, ya que está construida sobre las llamadas de $ http.
UserApp un módulo AngularJS para UserApp que hace prácticamente todo lo que me pides. Usted podría:
- Modifique el módulo y adjunte las funciones a su propia API, o
- Use el módulo junto con la API de administración de usuarios, UserApp
https://github.com/userapp-io/userapp-angular
Admite rutas protegidas / públicas, el reencaminamiento al iniciar sesión / cerrar sesión, los latidos del corazón para las comprobaciones de estado, almacena el token de sesión en una cookie, eventos, etc.
Si desea probar UserApp, tome el curso en Codecademy .
Aquí hay algunos ejemplos de cómo funciona:
Formulario de inicio de sesión con manejo de errores:
<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>
Formulario de registro con manejo de errores:
<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>
Cómo especificar qué rutas deben ser públicas y qué ruta es la forma de inicio de sesión:
$routeProvider.when(''/login'', {templateUrl: ''partials/login.html'', public: true, login: true}); $routeProvider.when(''/signup'', {templateUrl: ''partials/signup.html'', public: true});
La ruta
.otherwise()
debe establecerse en el lugar donde desea redirigir a los usuarios después de iniciar sesión. Ejemplo:$routeProvider.otherwise({redirectTo: ''/home''});
Cerrar sesión de enlace:
<a href="#" ua-logout>Log Out</a>
(Finaliza la sesión y redirige a la ruta de inicio de sesión)
Acceder a las propiedades del usuario:
Se accede a la información del
user
utilizando el servicio deluser
, por ejemplo:user.current.email
O en la plantilla:
<span>{{ user.email }}</span>
Ocultar elementos que solo deberían ser visibles al iniciar sesión:
<div ng-show="user.authorized">Welcome {{ user.first_name }}!</div>
Mostrar un elemento basado en permisos:
<div ua-has-permission="admin">You are an admin</div>
Y para autenticarse en sus servicios de back-end, simplemente use user.token()
para obtener el token de sesión y enviarlo con la solicitud de AJAX. En el back-end, use la API de UserApp (si usa UserApp) para verificar si el token es válido o no.
Si necesitas ayuda, házmelo saber :)