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:
- dejar que un usuario se registre
- guardar datos de usuario, pero nunca almacenar contraseñas directamente
- dejar que un usuario que regresa inicie sesión
- mantener viva la sesión de un usuario conectado entre visitas a la página
- tiene algunas páginas que solo pueden ver los usuarios registrados
- 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:
- página de inicio
- página de registro
- Inicio de sesión
- página de perfil
Las páginas son bastante básicas y se ven así para empezar:
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:
/api/register
(POST), para gestionar el registro de nuevos usuarios/api/login
(POST), para manejar el inicio de sesión de los usuarios que regresan/api/profile/USERID
(GET), para devolver detalles del perfil cuando se le da unUSERID
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 unroutes
directorio en elapi
directorio:mkdir -p api/{controllers,models,routes}
. - Créar un
authenication.js
archivo y unprofile.js
presentar en elcontrollers
directorio:touch api/controllers/{authentication.js,profile.js}
. - Créar un
db.js
archivo y unusers.js
presentar en elmodels
directorio:touch api/models/{db.js,users.js}
. - Crear una
index.js
presentar en elroutes
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:
- hacer que los controladores sean funcionales
- 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:
- tomar los datos del formulario enviado y crear una nueva instancia de modelo Mongoose
- llama a
setPassword
método que creamos anteriormente para agregar la sal y el hash a la instancia - guardar la instancia como un registro en la base de datos
- generar un JWT
- 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:
- Encabezamiento: un objeto JSON codificado que contiene el tipo y el algoritmo de hashing utilizado
- carga útil: un objeto JSON codificado que contiene los datos, el cuerpo real del token
- 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:
- conectar los formularios de registro e inicio de sesión
- actualizar la navegación para reflejar el estado del usuario
- solo permite que los usuarios registrados accedan a
/profile
ruta - 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:
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