Logotipo de Zephyrnet

MEAN Stack: Cree una aplicación con Angular y la CLI angular

Fecha:

En este tutorial, veremos cómo administrar la autenticación de usuarios en la pila MEAN. Utilizaremos la arquitectura MEAN más común de tener una aplicación Angular de una sola página usando una API REST construida con Node, Express y MongoDB.

Al pensar en la autenticación de usuarios, debemos abordar las siguientes cosas:

  1. dejar que un usuario se registre
  2. guardar datos de usuario, pero nunca almacenar contraseñas directamente
  3. dejar que un usuario que regresa inicie sesión
  4. mantener viva la sesión de un usuario conectado entre visitas a la página
  5. tiene algunas páginas que solo pueden ver los usuarios registrados
  6. cambiar la salida a la pantalla según el estado de inicio de sesión (por ejemplo, un botón de "inicio de sesión" o un botón de "mi perfil").

Antes de sumergirnos en el código, dediquemos unos minutos para ver de alto nivel cómo funcionará la autenticación en la pila MEAN.

Para un conocimiento más profundo de JavaScript, lea nuestro libro, JavaScript: Novato a Ninja, 2a Edición.

El flujo de autenticación de pila MEAN

Entonces, ¿cómo se ve la autenticación en la pila MEAN?

Aún manteniendo esto en un nivel alto, estos son los componentes del flujo:

  • los datos del usuario se almacenan en MongoDB, con las contraseñas hash
  • Las funciones CRUD están integradas en una API Express: Crear (registrar), Leer (iniciar sesión, obtener perfil), Actualizar, Eliminar
  • una aplicación angular llama a la API y se ocupa de las respuestas
  • la API Express genera un Token web JSON (JWT, pronunciado "Jot") al registrarse o iniciar sesión, y pasa esto a la aplicación Angular
  • la aplicación Angular almacena el JWT para mantener la sesión del usuario
  • la aplicación Angular verifica la validez del JWT cuando muestra vistas protegidas
  • la aplicación Angular devuelve el JWT a Express cuando llama a rutas API protegidas.

Se prefieren los JWT a las cookies para mantener el estado de la sesión en el navegador. Las cookies son mejores para mantener el estado cuando se usa una aplicación del lado del servidor.

La aplicación de ejemplo

El código para este tutorial está disponible en GitHub. Para ejecutar la aplicación, necesitará tener Node.js instalado, junto con MongoDB. (Para obtener instrucciones sobre cómo instalar, consulte Documentación oficial de Mongo: Windows, Linux, macOS).

La aplicación angular

Para mantener el ejemplo en este tutorial simple, comenzaremos con una aplicación Angular con cuatro páginas:

  1. página de inicio
  2. página de registro
  3. Inicio de sesión
  4. página de perfil

Las páginas son bastante básicas y se ven así para empezar:

Capturas de pantalla de la aplicación.

La página de perfil solo será accesible para usuarios autenticados. Todos los archivos para la aplicación Angular están en una carpeta dentro de la aplicación CLI Angular llamada /client.

Usaremos la CLI angular para construir y ejecutar el servidor local. Si no está familiarizado con la CLI angular, consulte el Creación de una aplicación Todo con CLI angular tutorial para comenzar

La API REST

También comenzaremos con el esqueleto de una API REST construida con Node, Express y MongoDB, usando Mangosta para gestionar los esquemas. Esta API debería tener inicialmente tres rutas:

  1. /api/register (POST), para gestionar el registro de nuevos usuarios
  2. /api/login (POST), para manejar el inicio de sesión de los usuarios que regresan
  3. /api/profile/USERID (GET), para devolver detalles del perfil cuando se le da un USERID

Vamos a configurar eso ahora. Podemos usar el generador expreso herramienta para crear gran parte de la placa de caldera para nosotros. Si esto es nuevo para usted, tenemos un tutorial sobre cómo usarlo aquí.

Instalarlo con npm i -g express-generator. Luego, cree una nueva aplicación Express, eligiendo Pug como el motor de vista:

express -v pug mean-authentication

Cuando el generador se haya ejecutado, cambie al directorio del proyecto e instale las dependencias:

cd mean-authentication
npm i

Al momento de escribir, esto muestra una versión desactualizada de Pug. Vamos a arreglar eso:

npm i pug@latest

También podemos instalar Mongoose mientras estamos en ello:

npm i mongoose

Luego, necesitamos crear nuestra estructura de carpetas.

  • Eliminar el public carpeta: rm -rf public.
  • Crear una api directorio: mkdir api.
  • Créar un controllers, models, Y un routes directorio en el api directorio: mkdir -p api/{controllers,models,routes}.
  • Créar un authenication.js archivo y un profile.js presentar en el controllers directorio: touch api/controllers/{authentication.js,profile.js}.
  • Créar un db.js archivo y un users.js presentar en el models directorio: touch api/models/{db.js,users.js}.
  • Crear una index.js presentar en el routes directorio: touch api/routes/index.js.

Cuando hayas terminado, las cosas deberían verse así:

.
└── api ├── controllers │ ├── authentication.js │ └── profile.js ├── models │ ├── db.js │ └── users.js └── routes └── index.js

Ahora agreguemos la funcionalidad API. Reemplace el código en app.js con lo siguiente:

require('./api/models/db'); const cookieParser = require('cookie-parser');
const createError = require('http-errors');
const express = require('express');
const logger = require('morgan');
const path = require('path'); const routesApi = require('./api/routes/index'); const app = express(); // view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug'); app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); app.use('/api', routesApi); // catch 404 and forward to error handler
app.use((req, res, next) => { next(createError(404));
}); // error handler
app.use((err, req, res, next) => { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error');
}); module.exports = app;

Agregue lo siguiente a api/models/db.js:

require('./users');
const mongoose = require('mongoose');
const dbURI = 'mongodb://localhost:27017/meanAuth'; mongoose.set('useCreateIndex', true);
mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true
}); mongoose.connection.on('connected', () => { console.log(`Mongoose connected to ${dbURI}`);
});
mongoose.connection.on('error', (err) => { console.log(`Mongoose connection error: ${err}`);
});
mongoose.connection.on('disconnected', () => { console.log('Mongoose disconnected');
});

Agregue lo siguiente a api/routes/index.js:

const ctrlAuth = require('../controllers/authentication');
const ctrlProfile = require('../controllers/profile'); const express = require('express');
const router = express.Router(); // profile
router.get('/profile/:userid', ctrlProfile.profileRead); // authentication
router.post('/register', ctrlAuth.register);
router.post('/login', ctrlAuth.login); module.exports = router;

Agregue lo siguiente a api/controllers/profile.js:

module.exports.profileRead = (req, res) => { console.log(`Reading profile ID: ${req.params.userid}`); res.status(200); res.json({ message : `Profile read: ${req.params.userid}` });
};

Agregue lo siguiente a api/controllers/authentication.js:

module.exports.register = (req, res) => { console.log(`Registering user: ${req.body.email}`); res.status(200); res.json({ message : `User registered: ${req.body.email}` });
}; module.exports.login = (req, res) => { console.log(`Logging in user: ${req.body.email}`); res.status(200); res.json({ message : `User logged in: ${req.body.email}` });
};

Asegúrese de que Mongo se esté ejecutando y, finalmente, inicie el servidor con npm run start. Si todo está configurado correctamente, debería ver un mensaje en su terminal al que Mongoose está conectado mongodb://localhost:27017/meanAuth, y ahora debería poder realizar solicitudes y obtener respuestas de la API. Puede probar esto con una herramienta como Cartero.

Crear el esquema de datos MongoDB con Mongoose

A continuación, agreguemos un esquema a api/models/users.js. Define la necesidad de una dirección de correo electrónico, un nombre, un hash y una sal. Se utilizarán el hash y la sal en lugar de guardar una contraseña. los email está configurado como único, ya que lo usaremos para las credenciales de inicio de sesión. Aquí está el esquema:

const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ email: { type: String, unique: true, required: true }, name: { type: String, required: true }, hash: String, salt: String
}); mongoose.model('User', userSchema);

Administrar la contraseña sin guardarla

Guardar contraseñas de usuario es un gran no-no. Si un hacker obtiene una copia de su base de datos, debe asegurarse de que no pueda usarla para iniciar sesión en las cuentas. Aquí es donde entran el hash y la sal.

La sal es una cadena de caracteres exclusiva de cada usuario. El hash se crea combinando la contraseña proporcionada por el usuario y la sal, y luego aplicando un cifrado unidireccional. Como el hash no se puede descifrar, la única forma de autenticar a un usuario es tomar la contraseña, combinarla con la sal y volver a cifrarla. Si el resultado de esto coincide con el hash, la contraseña debe haber sido correcta.

Para hacer la configuración y la verificación de la contraseña, podemos usar métodos de esquema Mongoose. Estas son esencialmente funciones que agrega al esquema. Ambos harán uso de la Módulo criptográfico Node.js.

En la parte superior de la users.js archivo de modelo, requiere criptografía para que podamos usarlo:

const crypto = require('crypto');

Nada necesita instalación, ya que las criptomonedas se envían como parte de Node. Crypto en sí tiene varios métodos; estamos interesados ​​en bytes aleatorios para crear la sal al azar y pbkdf2Sync para crear el hash.

Establecer la contraseña

Para guardar la referencia a la contraseña, podemos crear un nuevo método llamado setPassword en userSchema esquema que acepta un parámetro de contraseña. El método luego usará crypto.randomBytes para poner la sal, y crypto.pbkdf2Sync para configurar el hash:

userSchema.methods.setPassword = function(password) { this.salt = crypto.randomBytes(16).toString('hex'); this.hash = crypto .pbkdf2Sync(password, this.salt, 1000, 64, 'sha512') .toString('hex');
};

Utilizaremos este método al crear un usuario. En lugar de guardar la contraseña en un password camino, podremos pasarlo al setPassword función para configurar el salt y hash rutas en el documento del usuario.

Comprobando la contraseña

Verificar la contraseña es un proceso similar, pero ya tenemos la sal del modelo Mongoose. Esta vez solo queremos encriptar la sal y la contraseña y ver si la salida coincide con el hash almacenado.

Agregue otro método nuevo al users.js archivo de modelo, llamado validPassword:

userSchema.methods.validPassword = function(password) { const hash = crypto .pbkdf2Sync(password, this.salt, 1000, 64, 'sha512') .toString('hex'); return this.hash === hash;
};

Generando un token web JSON (JWT)

Una cosa más que el modelo Mongoose debe poder hacer es generar un JWT, de modo que la API pueda enviarlo como respuesta. Un método Mongoose también es ideal aquí, ya que significa que podemos mantener el código en un lugar y llamarlo cuando sea necesario. Tendremos que llamarlo cuando un usuario se registre y cuando un usuario inicie sesión.

Para crear el JWT, usaremos un paquete llamado jsonwebtoken, que debe instalarse en la aplicación, así que ejecútelo en la línea de comando:

npm i jsonwebtoken

Entonces requiera esto en el users.js archivo modelo:

const jwt = require('jsonwebtoken');

Este módulo expone un sign método que podemos usar para crear un JWT, simplemente pasándole los datos que queremos incluir en el token, más un secreto que usará el algoritmo de hash. Los datos deben enviarse como un objeto JavaScript e incluir una fecha de caducidad en un exp propiedad.

Añadiendo un generateJwt método para userSchema para devolver un JWT se ve así:

userSchema.methods.generateJwt = function() { const expiry = new Date(); expiry.setDate(expiry.getDate() + 7); return jwt.sign( { _id: this._id, email: this.email, name: this.name, exp: parseInt(expiry.getTime() / 1000) }, 'MY_SECRET' ); // DO NOT KEEP YOUR SECRET IN THE CODE!
};

Nota: es importante que su secreto se mantenga seguro: solo el servidor de origen debe saber cuál es. Es una buena práctica establecer el secreto como una variable de entorno, y no tenerlo en el código fuente, especialmente si su código está almacenado en el control de versiones en alguna parte.

Y eso es todo lo que necesitamos hacer con la base de datos.

Configure el pasaporte para manejar la autenticación exprés

Passport es un módulo de nodo que simplifica el proceso de manejo de autenticación en Express. Proporciona una puerta de enlace común para trabajar con muchas "estrategias" de autenticación diferentes, como iniciar sesión con Facebook, Twitter u Oauth. La estrategia que usaremos se llama "local", ya que usa un nombre de usuario y contraseña almacenados localmente.

Para usar Passport, primero instálelo y la estrategia, guardándolos en package.json:

npm i passport passport-local

Configurar pasaporte

Dentro del api carpeta, cree una nueva carpeta config y crear un archivo allí llamado passport.js. Aquí es donde definimos la estrategia:

mkdir -p api/config
touch api/config/passport.js

Antes de definir la estrategia, este archivo debe requerir Pasaporte, la estrategia, Mangosta y el User modelo:

const mongoose = require('mongoose');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = mongoose.model('User');

Para una estrategia local, esencialmente solo necesitamos escribir una consulta Mongoose en el User modelo. Esta consulta debe encontrar un usuario con la dirección de correo electrónico especificada y luego llamar al validPassword método para ver si los hashes coinciden.

Solo hay una curiosidad de Passport con la que lidiar. Internamente, la estrategia local para Passport espera dos datos llamados username y password. Sin embargo, estamos usando email como nuestro identificador único, no username. Esto se puede configurar en un objeto de opciones con un usernameField propiedad en la definición de estrategia. Después de eso, termina con la consulta Mongoose.

Entonces, la definición de la estrategia se verá así:

passport.use( new LocalStrategy( { usernameField: 'email' }, function(username, password, done) { User.findOne({ email: username }, function(err, user) { if (err) { return done(err); } // Return if user not found in database if (!user) { return done(null, false, { message: 'User not found' }); } // Return if password is wrong if (!user.validPassword(password)) { return done(null, false, { message: 'Password is wrong' }); } // If credentials are correct, return the user object return done(null, user); }); } )
);

Tenga en cuenta cómo el validPassword método de esquema se llama directamente en el user ejemplo.

Ahora es necesario agregar el pasaporte a la aplicación. Entonces en app.js necesitamos requerir el módulo Passport, requerir la configuración de Passport e inicializar Passport como middleware. La colocación de todos estos elementos dentro app.js es bastante importante, ya que necesitan encajar en una secuencia determinada.

El módulo Pasaporte debe ser requerido en la parte superior del archivo con el otro general require declaraciones:

const cookieParser = require('cookie-parser');
const createError = require('http-errors');
const express = require('express');
const logger = require('morgan');
const passport = require('passport');
const path = require('path');

La configuración debe ser requerida después de Se requiere el modelo, ya que la configuración hace referencia al modelo.

require('./api/models/db');
require('./api/config/passport');

Finalmente, Passport debe inicializarse como middleware Express justo antes de agregar las rutas API, ya que estas rutas son la primera vez que se utilizará Passport:

app.use(passport.initialize());
app.use("/api", routesApi);

Ahora tenemos el esquema y el pasaporte configurados. A continuación, es hora de ponerlos en uso en las rutas y los controladores de la API.

Configurar puntos finales de API

Con la API tenemos dos cosas que hacer:

  1. hacer que los controladores sean funcionales
  2. asegurar el /api/profile ruta para que solo los usuarios autenticados puedan acceder a ella

Codifique los controladores de API de registro e inicio de sesión

En la aplicación de ejemplo, los controladores de registro e inicio de sesión están en /api/controladores/autenticación.js. Para que los controladores funcionen, el archivo debe requerir Passport, Mongoose y el modelo de usuario:

const mongoose = require('mongoose');
const passport = require('passport');
const User = mongoose.model('User');

El controlador de API de registro

El controlador de registro debe hacer lo siguiente:

  1. tomar los datos del formulario enviado y crear una nueva instancia de modelo Mongoose
  2. llama a setPassword método que creamos anteriormente para agregar la sal y el hash a la instancia
  3. guardar la instancia como un registro en la base de datos
  4. generar un JWT
  5. envía el JWT dentro de la respuesta JSON

En código, todo eso se ve así. Esto debería reemplazar el maniquí register función que codificamos anteriormente:

module.exports.register = (req, res) => { const user = new User(); user.name = req.body.name; user.email = req.body.email; user.setPassword(req.body.password); user.save(() => { const token = user.generateJwt(); res.status(200); res.json({ token: token }); });
};

Esto hace uso de setPassword y generateJwt métodos que creamos en la definición del esquema Mongoose. Vea cómo tener ese código en el esquema hace que este controlador sea más fácil de leer y comprender.

No olvide que, en realidad, este código tendría varias trampas de error, validando entradas de formulario y detectando errores en el save función. Se omiten aquí para resaltar la funcionalidad principal del código, pero si desea una actualización, consulte "Formularios, carga de archivos y seguridad con Node.js y Express.

El controlador de API de inicio de sesión

El controlador de inicio de sesión entrega casi todo el control a Passport, aunque puede (y debe) agregar alguna validación de antemano para verificar que se hayan enviado los campos requeridos.

Para que Passport haga su magia y ejecute la estrategia definida en la configuración, debemos llamar al authenticate método como se muestra a continuación. Este método llamará a una devolución de llamada con tres parámetros posibles err, user y info. Si user está definido, se puede utilizar para generar un JWT que se devolverá al navegador. Esto debería reemplazar el maniquí login método que definimos anteriormente:

module.exports.login = (req, res) => { passport.authenticate('local', (err, user, info) => { // If Passport throws/catches an error if (err) { res.status(404).json(err); return; } // If a user is found if (user) { const token = user.generateJwt(); res.status(200); res.json({ token: token }); } else { // If user is not found res.status(401).json(info); } })(req, res);
};

Asegurar una ruta API

Lo último que debe hacer en el back-end es asegurarse de que solo los usuarios autenticados puedan acceder a /api/profile ruta. La forma de validar una solicitud es asegurarse de que el JWT enviado con él sea genuino, utilizando el secreto nuevamente. Es por eso que debe mantenerlo en secreto y no colocarlo en el código.

Configurar la autenticación de ruta

Primero necesitamos instalar una pieza de middleware llamada expreso-jwt:

npm i express-jwt

Luego necesitamos requerirlo y configurarlo en el archivo donde se definen las rutas. En la aplicación de muestra, esto es /api/routes/index.js. La configuración es un caso de decirle el secreto y, opcionalmente, el nombre de la propiedad para crear en el req objeto que contendrá el JWT. Podremos usar esta propiedad dentro del controlador asociado con la ruta. El nombre predeterminado para la propiedad es user, pero este es el nombre de una instancia de nuestra Mangosta User modelo, así que lo configuraremos en payload para evitar confusión:

// api/routes/index.js const jwt = require('express-jwt'); const auth = jwt({ secret: 'MY_SECRET', userProperty: 'payload'
}); ...

Una vez más, ¡No guardes el secreto en el código!

Aplicación de la autenticación de ruta

Para aplicar este middleware, simplemente haga referencia a la función en el medio de la ruta a proteger, así:

router.get('/profile', auth, ctrlProfile.profileRead);

Note que hemos cambiado /profile/:userid a /profile, ya que la identificación se obtendrá del JWT.

Si alguien intenta acceder a esa ruta ahora sin un JWT válido, el middleware arrojará un error. Para asegurarse de que nuestra API funciona bien, detecte este error y devuelva una respuesta 401 agregando lo siguiente en la sección de controladores de errores de la página principal app.js archivo:

// catch 404 and forward to error handler
app.use((req, res, next) => { ... }); // Catch unauthorised errors
app.use((err, req, res) => { if (err.name === 'UnauthorizedError') { res.status(401); res.json({ message: `${err.name}: ${err.message}` }); }
});

En este punto, puede intentar OBTENER /api/profile punto final utilizando una herramienta como Cartero o en tu navegador, y deberías ver una respuesta 401.

Usando la autenticación de ruta

En este ejemplo, solo queremos que las personas puedan ver sus propios perfiles, por lo que obtenemos la ID de usuario del JWT y la usamos en una consulta de Mongoose.

El controlador para esta ruta está en /api/controladores/perfil.js. Todo el contenido de este archivo tiene este aspecto:

const mongoose = require('mongoose');
const User = mongoose.model('User'); module.exports.profileRead = (req, res) => { // If no user ID exists in the JWT return a 401 if (!req.payload._id) { res.status(401).json({ message: 'UnauthorizedError: private profile' }); } else { // Otherwise continue User.findById(req.payload._id).exec(function(err, user) { res.status(200).json(user); }); }
};

Naturalmente, esto debería desarrollarse con algo más de captura de errores, por ejemplo, si no se encuentra al usuario, pero este fragmento se mantiene breve para demostrar los puntos clave del enfoque.

Y eso es todo por la parte de atrás. La base de datos está configurada, tenemos puntos finales de API para registrar e iniciar sesión que generan y devuelven un JWT, y también una ruta protegida.

En el frente!

Inicializar la aplicación angular

Vamos a utilizar la CLI de Angluar en esta sección, así que antes de continuar, asegúrese de que esté instalado globalmente:

npm install -g @angular/cli

Luego, en el directorio raíz del proyecto, ejecute:

ng new client ? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS
...
✔ Packages installed successfully. Successfully initialized git.

Esto genera un nuevo client directorio con un AppModule y AppRoutingModule. Al responder "Sí" a "¿Desea agregar enrutamiento angular", el AppRoutingModule se crea e importa automáticamente a AppModule para nosotros.

Debido a que haremos uso de los formularios de Angular y el cliente HTTP de Angular, necesitamos importar Angular's Módulo de formularios y Módulo HttpClient. Cambiar el contenido de client/src/app/app.module.ts al igual que:

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core"; import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http"; @NgModule({ declarations: [ AppComponent ], imports: [BrowserModule, AppRoutingModule, FormsModule, HttpClientModule], providers: [], bootstrap: [AppComponent]
})
export class AppModule {}

Crear un servicio de autenticación angular

La mayor parte del trabajo en el front-end se puede poner en un servicio angular, creando métodos para administrar:

  • guardar el JWT en el almacenamiento local
  • leyendo el JWT desde el almacenamiento local
  • eliminar el JWT del almacenamiento local
  • llamar a los puntos finales de la API de registro e inicio de sesión
  • comprobar si un usuario está actualmente conectado
  • obtener los detalles del usuario conectado de JWT

Necesitaremos crear un nuevo servicio llamado AuthenticationService. Con la CLI, esto se puede hacer ejecutando:

$ cd client
$ ng generate service authentication
CREATE src/app/authentication.service.spec.ts (397 bytes)
CREATE src/app/authentication.service.ts (143 bytes)

En la aplicación de ejemplo, esto está en el archivo /cliente/src/app/autenticación.servicio.ts:

import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root"
})
export class AuthenticationService { constructor() {}
}

Almacenamiento local: guardar, leer y eliminar un JWT

Para mantener a un usuario conectado entre visitas, utilizamos localStorage en el navegador para guardar el JWT. Una alternativa es usar sessionStorage, que solo mantendrá el token durante la sesión actual del navegador.

Primero, queremos crear algunas interfaces para manejar los tipos de datos. Esto es útil para verificar el tipo de nuestra aplicación. El perfil devuelve un objeto formateado como UserDetails, y los puntos finales de inicio de sesión y registro esperan un TokenPayload durante la solicitud y devolver un TokenResponse :

export interface UserDetails { _id: string; email: string; name: string; exp: number; iat: number;
} interface TokenResponse { token: string;
} export interface TokenPayload { email: string; password: string; name?: string;
}

Este servicio utiliza el HttpClient servicio de Angular para realizar solicitudes HTTP a nuestra aplicación de servidor (que usaremos en un momento) y el Router servicio para navegar programáticamente. Debemos inyectarlos en nuestro constructor de servicios:

constructor(private http: HttpClient, private router: Router) {}

Luego definimos cuatro métodos que interactúan con el token JWT. Implementamos saveToken para manejar el almacenamiento de la ficha en localStorage y en el token propiedad, un getToken método para recuperar el token de localStorage o desde el token propiedad y un logout función que elimina el token JWT y redirige a la página de inicio.

Es importante tener en cuenta que este código no se ejecuta si está utilizando la representación del lado del servidor, porque las API como localStorage y window.atob no están disponibles Hay detalles sobre soluciones para abordar la representación del lado del servidor en la documentación angular.

Hasta ahora, esto nos da:

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Router } from "@angular/router";
import { Observable } from "rxjs";
import { map } from "rxjs/operators"; export interface UserDetails { _id: string; email: string; name: string; exp: number; iat: number;
} interface TokenResponse { token: string;
} export interface TokenPayload { email: string; password: string; name?: string;
} @Injectable({ providedIn: "root"
})
export class AuthenticationService { private token: string; constructor(private http: HttpClient, private router: Router) {} private saveToken(token: string): void { localStorage.setItem("mean-token", token); this.token = token; } private getToken(): string { if (!this.token) { this.token = localStorage.getItem("mean-token"); } return this.token; } public logout(): void { this.token = ""; window.localStorage.removeItem("mean-token"); this.router.navigateByUrl("/"); }
}

Ahora agreguemos un método para verificar este token, y la validez del token, para averiguar si el visitante ha iniciado sesión.

Obteniendo datos de un JWT

Cuando establecemos los datos para el JWT (en el generateJwt Método de mangosta) incluimos la fecha de vencimiento en un exp propiedad. Pero si observa un JWT, parece ser una cadena aleatoria, como este ejemplo siguiente:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI1NWQ0MjNjMTUxMzcxMmNkMzE3YTRkYTciLCJlbWFpbCI6InNpbW9uQGZ1bGxzdGFja3RyYWluaW5nLmNvbSIsIm5hbWUiOiJTaW1vbiBIb2xtZXMiLCJleHAiOjE0NDA1NzA5NDUsImlhdCI6MTQzOTk2NjE0NX0.jS50GlmolxLoKrA_24LDKaW3vNaY94Y9EqYAFvsTiLg

Entonces, ¿cómo se lee un JWT?

Un JWT en realidad está compuesto por tres cadenas separadas, separadas por un punto (.) Estas tres partes son:

  1. Encabezamiento: un objeto JSON codificado que contiene el tipo y el algoritmo de hashing utilizado
  2. carga útil: un objeto JSON codificado que contiene los datos, el cuerpo real del token
  3. Firma: un hash cifrado del encabezado y la carga útil, utilizando el conjunto "secreto" en el servidor.

Es la segunda parte que nos interesa aquí: la carga útil. Tenga en cuenta que esto es codificado en lugar de cifrado, lo que significa que podemos descodificar él.

Hay una función llamada de la A, a la B eso es nativo de los navegadores modernos, y que decodificará una cadena Base64 como esta.

Por lo tanto, necesitamos obtener la segunda parte del token, decodificarlo y analizarlo como JSON. Luego podemos verificar que la fecha de vencimiento no haya pasado.

Al final, el getUserDetails la función debe devolver un objeto de la UserDetails tipo o null, dependiendo de si se encuentra o no un token válido. En conjunto, se ve así:

public getUserDetails(): UserDetails { const token = this.getToken(); let payload; if (token) { payload = token.split(".")[1]; payload = window.atob(payload); return JSON.parse(payload); } else { return null; }
}

Los detalles del usuario que se proporcionan incluyen la información sobre el nombre del usuario, el correo electrónico y la caducidad del token, que usaremos para verificar si la sesión del usuario es válida.

Compruebe si un usuario ha iniciado sesión

Agregue un nuevo método llamado isLoggedIn al servicio Utiliza el getUserDetails método para obtener los detalles del token del token JWT y comprueba si la caducidad aún no ha pasado:

public isLoggedIn(): boolean { const user = this.getUserDetails(); if (user) { return user.exp > Date.now() / 1000; } else { return false; }
}

Si el token existe, el método devolverá si el usuario ha iniciado sesión como un valor booleano. Ahora podemos construir nuestras solicitudes HTTP para cargar datos, utilizando el token para la autorización.

Estructurando las Llamadas API

Para facilitar la realización de llamadas a la API, agregue el request método para el AuthenticationService, que puede construir y devolver la solicitud HTTP adecuada observable según el tipo específico de solicitud. Es un método privado, ya que solo lo utiliza este servicio, y existe solo para reducir la duplicación de código. Esto usará el angular HttpClient Servicio. Recuerde inyectar esto en el AuthenticationService si aún no está allí:

private request( method: "post" | "get", type: "login" | "register" | "profile", user?: TokenPayload
): Observable<any> { let base$; if (method === "post") { base$ = this.http.post(`/api/${type}`, user); } else { base$ = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` } }); } const request = base$.pipe( map((data: TokenResponse) => { if (data.token) { this.saveToken(data.token); } return data; }) ); return request;
}

Requiere el map operador de RxJS para interceptar y almacenar el token en el servicio si es devuelto por un inicio de sesión API o una llamada de registro. Ahora podemos implementar los métodos públicos para llamar a la API.

Llamar a los puntos finales de API de registro e inicio de sesión

Solo tres métodos para agregar. Necesitaremos una interfaz entre la aplicación Angular y la API, para llamar al login y register puntos finales y guardar el token devuelto, o el profile punto final para obtener los detalles del usuario:

public register(user: TokenPayload): Observable<any> { return this.request("post", "register", user);
} public login(user: TokenPayload): Observable<any> { return this.request("post", "login", user);
} public profile(): Observable<any> { return this.request("get", "profile");
}

Cada método devuelve un observable que manejará la solicitud HTTP para una de las llamadas a la API que debemos realizar. Eso finaliza el servicio; ahora es el momento de unir todo en la aplicación Angular.

Aplicar autenticación a la aplicación angular

Podemos usar el AuthenticationService dentro de la aplicación Angular de varias maneras para brindar la experiencia que buscamos:

  1. conectar los formularios de registro e inicio de sesión
  2. actualizar la navegación para reflejar el estado del usuario
  3. solo permite que los usuarios registrados accedan a /profile ruta
  4. llamar a los protegidos /api/profile Ruta API

Para comenzar, primero generamos los componentes que necesitamos usando Angular CLI:

$ ng generate component register
CREATE src/app/register/register.component.css (0 bytes)
CREATE src/app/register/register.component.html (23 bytes)
CREATE src/app/register/register.component.spec.ts (642 bytes)
CREATE src/app/register/register.component.ts (283 bytes)
UPDATE src/app/app.module.ts (458 bytes) $ ng generate component profile
CREATE src/app/profile/profile.component.css (0 bytes)
CREATE src/app/profile/profile.component.html (22 bytes)
CREATE src/app/profile/profile.component.spec.ts (635 bytes)
CREATE src/app/profile/profile.component.ts (279 bytes)
UPDATE src/app/app.module.ts (540 bytes) $ ng generate component login
CREATE src/app/login/login.component.css (0 bytes)
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (621 bytes)
CREATE src/app/login/login.component.ts (271 bytes)
UPDATE src/app/app.module.ts (614 bytes) $ ng generate component home
CREATE src/app/home/home.component.css (0 bytes)
CREATE src/app/home/home.component.html (19 bytes)
CREATE src/app/home/home.component.spec.ts (614 bytes)
CREATE src/app/home/home.component.ts (267 bytes)
UPDATE src/app/app.module.ts (684 bytes)

Conecte los controladores de registro e inicio de sesión

Ahora que nuestros componentes han sido creados, echemos un vistazo a los formularios de registro e inicio de sesión.

La página de registro

Primero, creemos el formulario de registro. Tiene NgModel directivas adjuntas a los campos, todas vinculadas a propiedades establecidas en el credentials propiedad del controlador. El formulario también tiene un (submit) evento vinculante para manejar el envío. En la aplicación de ejemplo, está en /cliente/src/app/registrar/registrar.componente.html y se ve así:

<form (submit)="register()"> <div class="form-group"> <label for="name">Full name</label> <input type="text" class="form-control" name="name" placeholder="Enter your name" [(ngModel)]="credentials.name" /> </div> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email" /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password" /> </div> <button type="submit" class="btn btn-default">Register!</button>
</form>

La primera tarea en el controlador es garantizar nuestra AuthenticationService y del Router se inyectan y están disponibles a través del constructor. A continuación, dentro del register controlador para enviar el formulario, llame al auth.register, pasándole las credenciales del formulario.

El register El método devuelve un observable, al que debemos suscribirnos para activar la solicitud. El observable emitirá éxito o falla, y si alguien se ha registrado con éxito, configuraremos la aplicación para redirigirlo a la página de perfil o registrar el error en la consola.

En la aplicación de muestra, el controlador está en /cliente/src/app/registrar/registrar.componente.ts y se ve así:

import { Component } from "@angular/core";
import { AuthenticationService, TokenPayload } from "../authentication.service";
import { Router } from "@angular/router"; @Component({ templateUrl: "./register.component.html", styleUrls: ["./register.component.css"]
})
export class RegisterComponent { credentials: TokenPayload = { email: "", name: "", password: "" }; constructor(private auth: AuthenticationService, private router: Router) {} register() { this.auth.register(this.credentials).subscribe( () => { this.router.navigateByUrl("/profile"); }, err => { console.error(err); } ); }
}

La página de inicio de sesión

La página de inicio de sesión es muy similar a la página de registro, pero en este formulario no pedimos el nombre, solo el correo electrónico y la contraseña. En la aplicación de muestra, está en /cliente/src/app/login/login.component.html y se ve así:

<form (submit)="login()"> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" name="email" placeholder="Enter email" [(ngModel)]="credentials.email" /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" name="password" placeholder="Password" [(ngModel)]="credentials.password" /> </div> <button type="submit" class="btn btn-default">Sign in!</button>
</form>

Una vez más, tenemos el controlador de envío de formularios y NgModel atributos para cada una de las entradas. En el controlador, queremos la misma funcionalidad que el controlador de registro, pero esta vez para llamar al login método de la AuthenticationService.

En la aplicación de muestra, el controlador está en /cliente/src/app/login/login.component.ts y se ve así:

import { Component } from "@angular/core";
import { AuthenticationService, TokenPayload } from "../authentication.service";
import { Router } from "@angular/router"; @Component({ templateUrl: "./login.component.html", styleUrls: ["./login.component.css"]
})
export class LoginComponent { credentials: TokenPayload = { email: "", password: "" }; constructor(private auth: AuthenticationService, private router: Router) {} login() { this.auth.login(this.credentials).subscribe( () => { this.router.navigateByUrl("/profile"); }, err => { console.error(err); } ); }
}

Ahora los usuarios pueden registrarse e iniciar sesión en la aplicación. Tenga en cuenta que, nuevamente, debe haber más validación en los formularios para garantizar que se llenen todos los campos obligatorios antes de enviarlos. Estos ejemplos se mantienen al mínimo para resaltar la funcionalidad principal.

Cambiar contenido según el estado del usuario

En la navegación, queremos mostrar el Iniciar Sesión enlace si un usuario no ha iniciado sesión, y su nombre de usuario con un enlace a la página de perfil si ha iniciado sesión. La barra de navegación se encuentra en el App componente.

Primero, veremos el App controlador de componentes. Podemos inyectar el AuthenticationService en el componente y llámelo directamente en nuestra plantilla. En la aplicación de muestra, el archivo está en /cliente/src/app/app.component.ts y se ve así:

import { Component } from "@angular/core";
import { AuthenticationService } from "./authentication.service"; @Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"]
}) export class AppComponent { constructor(public auth: AuthenticationService) {}
}

Ahora, en la plantilla asociada podemos usar auth.isLoggedIn() para determinar si se muestra el enlace de inicio de sesión o el enlace del perfil. Para agregar el nombre del usuario al enlace del perfil, podemos acceder a la propiedad de nombre de auth.getUserDetails()?.name. Recuerde que esto está obteniendo los datos del JWT. los ?. El operador es una forma especial de acceder a una propiedad en un objeto que puede estar indefinido, sin arrojar un error.

En la aplicación de muestra, el archivo está en /cliente/src/app/app.component.html y la parte actualizada se ve así:

<ul class="nav navbar-nav navbar-right"> <li *ngIf="!auth.isLoggedIn()"><a routerLink="/login">Sign in</a></li> <li *ngIf="auth.isLoggedIn()"> <a routerLink="/profile">{{ auth.getUserDetails()?.name }}</a> </li> <li *ngIf="auth.isLoggedIn()"><a (click)="auth.logout()">Logout</a></li>
</ul> <router-outlet></router-outlet>

Proteja una ruta solo para usuarios registrados

En este paso, veremos cómo hacer que una ruta sea accesible solo para los usuarios registrados, protegiendo /profile camino.

Angular le permite definir un protector de ruta, que puede ejecutar una verificación en varios puntos del ciclo de vida de la ruta para determinar si la ruta se puede cargar. Usaremos el CanActivate enganche para indicarle a Angular que cargue la ruta del perfil solo si el usuario ha iniciado sesión.

Para hacer esto, necesitamos crear una guardia de ruta:

$ ng generate guard auth
? Which interfaces would you like to implement? CanActivate
CREATE src/app/auth.guard.spec.ts (331 bytes)
CREATE src/app/auth.guard.ts (456 bytes)

Debe implementar el CanActivate interfaz y el asociado canActivate método. Este método devuelve un valor booleano del AuthenticationService.isLoggedIn método (básicamente verifica si el token se encuentra y sigue siendo válido), y si el usuario no es válido también lo redirige a la página de inicio.

In auth.guard.ts:

import { Injectable } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router
} from "@angular/router";
import { Observable } from "rxjs";
import { AuthenticationService } from "./authentication.service"; @Injectable({ providedIn: "root"
})
export class AuthGuard implements CanActivate { constructor(private auth: AuthenticationService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { if (!this.auth.isLoggedIn()) { this.router.navigateByUrl("/"); return false; } return true; }
}

Para habilitar esta protección, tenemos que declararla en la configuración de la ruta. Hay una propiedad de ruta llamada canActivate, que toma una serie de servicios que deberían llamarse antes de activar la ruta. Las rutas se definen en el Módulo de enrutamiento de aplicaciones, que contiene las rutas como ves aquí:

const routes: Routes = [ { path: "", component: HomeComponent }, { path: "login", component: LoginComponent }, { path: "register", component: RegisterComponent }, { path: "profile", component: ProfileComponent, canActivate: [AuthGuard] }
];

Todo el archivo debería verse así:

import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { LoginComponent } from "./login/login.component";
import { RegisterComponent } from "./register/register.component";
import { ProfileComponent } from "./profile/profile.component";
import { AuthGuard } from "./auth.guard"; const routes: Routes = [ { path: "", component: HomeComponent }, { path: "login", component: LoginComponent }, { path: "register", component: RegisterComponent }, { path: "profile", component: ProfileComponent, canActivate: [AuthGuard] }
]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule]
})
export class AppRoutingModule {}

Con esa protección de ruta en su lugar, ahora si un usuario no autenticado intenta visitar la página de perfil, Angular cancelará el cambio de ruta y lo redirigirá a la página de inicio, protegiéndolo así de usuarios no autenticados.

Llamar a una ruta API protegida

El /api/profile La ruta se ha configurado para buscar un JWT en la solicitud. De lo contrario, devolverá un error no autorizado 401.

Para pasar el token a la API, debe enviarse como un encabezado en la solicitud, llamada Authorization. El siguiente fragmento muestra la función principal del servicio de datos y el formato requerido para enviar el token. los AuthenticationService ya maneja esto, pero puedes encontrarlo en /cliente/src/app/autenticación.servicio.ts:

base$ = this.http.get(`/api/${type}`, { headers: { Authorization: `Bearer ${this.getToken()}` }
});

Recuerde que el código de fondo está validando que el token es genuino cuando se realiza la solicitud, utilizando el secreto conocido solo por el servidor emisor.

Para hacer uso de esto en la página de perfil, solo necesitamos actualizar el controlador, en /cliente/src/app/perfil/perfil.component.ts en la aplicación de muestra. Esto llenará un details propiedad cuando la API devuelve algunos datos, que deben coincidir con UserDetails interfaz:

import { Component, OnInit } from "@angular/core";
import { AuthenticationService, UserDetails } from "../authentication.service"; @Component({ templateUrl: "./profile.component.html", styleUrls: ["./profile.component.css"]
})
export class ProfileComponent implements OnInit { details: UserDetails; constructor(private auth: AuthenticationService) {} ngOnInit() { this.auth.profile().subscribe( user => { this.details = user; }, err => { console.error(err); } ); }
}

Luego, por supuesto, solo se trata de actualizar los enlaces en la vista (/src/app/profile/profile.component.html) De nuevo, el ?. es un operador de seguridad para las propiedades de enlace que no existen en el primer renderizado (ya que los datos deben cargarse primero):

<div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Full name</label> <p class="form-control-static">{{ details?.name }}</p> </div> <div class="form-group"> <label class="col-sm-3 control-label">Email</label> <p class="form-control-static">{{ details?.email }}</p> </div>
</div>

Ejecutando la aplicación angular

Para ejecutar la aplicación Angular, vamos a necesitar enrutar cualquier solicitud a /api a nuestro servidor Express que se ejecuta en http://localhost:3000/. Para hacer esto, cree un proxy.conf.json presentar en el client directorio:

touch proxy.conf.json

Agregue también el siguiente contenido:

{ "/api": { "target": "http://localhost:3000", "secure": false }
}

Finalmente, actualice el start guión en client/package.json:

"start": "ng serve --proxy-config proxy.conf.json",

Ahora, asegúrese de que Mongo se esté ejecutando, inicie la aplicación Express desde la raíz de nuestro proyecto usando npm start e inicie la aplicación Angular desde el client directorio usando el mismo comando.

Entonces, visita http://localhost:4200, para ver el producto (casi) terminado. Intente registrar una cuenta en http://localhost:4200/register e iniciar sesión, para asegurarse de que todo está funcionando como debería.

Algunos toques finales

Como sin duda habrás notado, la aplicación final no tiene ningún estilo. Como este es un tutorial un poco largo, no los he incluido aquí. Pero si echas un vistazo al código terminado en GitHub, puedes tomar todo desde allí. Los archivos a mirar son:

Si copia el marcado adicional de estos archivos, debería terminar con esto:

Captura de pantalla de la página de perfil.

Y así es cómo administrar la autenticación en la pila MEAN, desde asegurar las rutas API y administrar los detalles del usuario hasta trabajar con JWT y proteger rutas.

Fuente: https://www.sitepoint.com/mean-stack-angular-angular-cli/?utm_source=rss

punto_img

Información más reciente

punto_img