This shows you the differences between two versions of the page.
moby:backend:04 [2020/07/30 11:30] alexandru.hogea |
moby:backend:04 [2020/07/30 12:29] (current) alexandru.hogea |
||
---|---|---|---|
Line 1: | Line 1: | ||
===== Sesiuna 4 - Serviciul de Utilizatori si Autentificare/Autorizare. Interactiune intre microservicii ===== | ===== Sesiuna 4 - Serviciul de Utilizatori si Autentificare/Autorizare. Interactiune intre microservicii ===== | ||
- | Vom lega serviciul dezvoltat in sesiuna 3 la o baza de date care ruleaza in Docker. In continuare, vom dezvolta un serviciu care trimite mailuri. | + | Scopul sedintei: |
+ | * Intelegerea procesului de autentificare si autorizare | ||
+ | * Intelegerea conceptului de middlewares | ||
+ | * Realizarea serviciului de utilizatori | ||
+ | * Integrarea componentelor de autentificare si autorizare | ||
+ | * Interactiunea dintre 2 microservicii prin HTTP | ||
+ | |||
+ | ==== Resurse ==== | ||
+ | |||
+ | Filmuletul si prezentarea ppt pot fi gasite la linkul [[https://drive.google.com/drive/folders/1osCfsjUofAuBd8haWijvRBvJ00Vu5LXX?usp=sharing|acesta]] | ||
+ | |||
+ | Scheletul de cod pentru serviciile implementate poate fi gasit pe urmatorul [[https://gitlab.com/mobyworkshop|grup de Gitlab]] | ||
+ | |||
+ | ==== Practic ==== | ||
+ | |||
+ | Folosind cunostintele de Rest API invatate la laboratorul trecut, am dezvoltat serviciul de utilizatori, care se ocupa si de partea de autentificare si autorizare. | ||
+ | |||
+ | Multe componente sunt reutilizate complet, partea de securitate fiind, insa, noua. | ||
+ | |||
+ | === Interactiunea cu parolele (security/passwords/index.js) === | ||
+ | |||
+ | Cele doua functii scrise in acest fisier vor fi folosite la register (hash) si la login (compare). | ||
+ | |||
+ | <code javascript> | ||
+ | const bcryptjs = require('bcryptjs'); //pachet folosit la criptarea parolelor | ||
+ | |||
+ | const hash = async (plainTextPassword) => { // functia care va cripta o parola plaintext | ||
+ | const salt = await bcryptjs.genSalt(5); // se genereaza salt-ul | ||
+ | const hash = await bcryptjs.hash(plainTextPassword, salt); // pe baza saltului si a parolei plainText se genereaza hashul parolei | ||
+ | return hash | ||
+ | |||
+ | }; | ||
+ | |||
+ | const compare = async (plainTextPassword, hashedPassword) => { // functie care compara o parola text cu o parola hashuita | ||
+ | const isOk = await bcryptjs.compare(plainTextPassword, hashedPassword); | ||
+ | return isOk; | ||
+ | }; | ||
+ | |||
+ | module.exports = { | ||
+ | hash, | ||
+ | compare | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | === Interactiunea cu token JWT === | ||
+ | |||
+ | Interactiuena cu tokenul JWT poate fi impartita in 2 categorii: | ||
+ | |||
+ | - configurarea tokenului | ||
+ | - filtrele atasate cererilor care se ocupa de manipularea tokenului | ||
+ | |||
+ | == Configurarea Tokenului (security/jwt/token.js) == | ||
+ | |||
+ | <code javascript> | ||
+ | const jwt = require('jsonwebtoken'); //pachetul care se ocupa de tokenuri JWT | ||
+ | |||
+ | const { | ||
+ | ServerError | ||
+ | } = require('../../errors'); | ||
+ | |||
+ | const { | ||
+ | getSecret | ||
+ | } = require('docker-secret'); | ||
+ | |||
+ | const generalOptions = { // optiunile ce tin de identitatea organizatiei care creaza un token | ||
+ | issuer: 'Workshop Moby', | ||
+ | subject: 'Authorization Token Workshop Moby', | ||
+ | audience: 'Workshop Moby Users' | ||
+ | }; | ||
+ | |||
+ | const jwtKey = process.env.NODE_ENV === 'development' ? process.env.JWT_SECRET_KEY : getSecret(process.env.JWT_SECRET_KEY_FILE); // cheia este preluata din .env | ||
+ | |||
+ | const generateToken = async (payload) => { // functia care se ocupa de generarea tokenului | ||
+ | try { | ||
+ | const encodingOptions = { ...generalOptions, expiresIn: process.env.JWT_EXPIRE_TIME || '1h' } // timpul de expirare este preluat din .env | ||
+ | const token = await jwt.sign(payload.toPlainObject(), jwtKey, encodingOptions); // tokenul este obtinut apeland functia "sign" | ||
+ | return token; | ||
+ | } catch (err) { | ||
+ | console.error(err); | ||
+ | throw new ServerError("Error when encoding the token!", 500); | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | const verifyAndDecodeData = async (token, ignoreExpiration = false) => { // functia care se ocupa de extragerea datelor din token. O vom folosi in douua circumstante, intr-una fiind nevoie sa se tina cont de expirarea tokenului si in alta nu | ||
+ | try { | ||
+ | const decodingOptions = {...generalOptions, ignoreExpiration} | ||
+ | const decoded = await jwt.verify(token, jwtKey, decodingOptions); // optiunile scoase sunt salvate in obiectul decoded | ||
+ | return decoded; | ||
+ | } catch (err) { // handling customizat de erori. Dorim sa intoarcem mesajul si codul clare. | ||
+ | if (err.message === 'jwt expired') { | ||
+ | throw new ServerError('Token is expired. Please refresh!', 401); | ||
+ | } else if (err.message === 'jwt malformed') { | ||
+ | throw new ServerError('Token is malformed!', 401); | ||
+ | } | ||
+ | throw new ServerError("Token is compromised!", 401); | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | |||
+ | module.exports = { | ||
+ | generateToken, | ||
+ | verifyAndDecodeData | ||
+ | }; | ||
+ | |||
+ | </code> | ||
+ | |||
+ | == Filtrele JWT (security/jwt/filters.js) == | ||
+ | |||
+ | <code javascript> | ||
+ | const { | ||
+ | verifyAndDecodeData | ||
+ | } = require('./token.js'); | ||
+ | |||
+ | const { | ||
+ | ServerError | ||
+ | } = require('../../errors'); | ||
+ | |||
+ | |||
+ | const verifyAuthHeader = (req) => { //verificarea existentei headerului "Authorization" | ||
+ | if (!req.headers.authorization) { | ||
+ | throw new ServerError('Authorization Header missing!', 401); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | const authorizeAndExtractToken = async (req, res, next) => { // middleware ce va fi atasat rutelor. Contine, in parametri, obiectele req, res si next | ||
+ | try { | ||
+ | verifyAuthHeader(req); | ||
+ | const token = req.headers.authorization.split(" ")[1]; // extrage tokenul din header. Tokenul este separat prin spatiu de cuvantul Bearer | ||
+ | |||
+ | const decoded = await verifyAndDecodeData(token); // preia obiectul decodat | ||
+ | req.state = { // salveaza informatiile din token in req.state | ||
+ | decoded | ||
+ | }; | ||
+ | |||
+ | next(); // prin apelul functiei next(), urmatorul middleware este notificat ca poate sa se execute | ||
+ | } catch (err) { | ||
+ | next(err); | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | const authorizeAndExtractTokenNoExpire = async (req, res, next) => { // acelasi lucru ca mai sus, doar ca se trimite in plus parametrul de ignoreExpiration = true la functia "verifyAndDecodeData" | ||
+ | try { | ||
+ | verifyAuthHeader(req); | ||
+ | const token = req.headers.authorization.split(" ")[1]; | ||
+ | |||
+ | const decoded = await verifyAndDecodeData(token, true); | ||
+ | req.state = { | ||
+ | decoded | ||
+ | }; | ||
+ | |||
+ | next(); | ||
+ | } catch (err) { | ||
+ | next(err); | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | module.exports = { | ||
+ | authorizeAndExtractToken, | ||
+ | authorizeAndExtractTokenNoExpire | ||
+ | } | ||
+ | |||
+ | |||
+ | </code> | ||
+ | |||
+ | == Payload-ul JWT (security/jwt/models.js) == | ||
+ | |||
+ | Clasa ce este folosita pentru impachetarea payload-ului JWT | ||
+ | |||
+ | <code javascript> | ||
+ | class JwtPayload { | ||
+ | constructor(id) { | ||
+ | this.userId = id; | ||
+ | } | ||
+ | |||
+ | toPlainObject (){ // functia "sign" nu stie sa lucreze cu clase, ci cu obiecte json simple, asa ca trebuie returnat un obiect simplu | ||
+ | return { | ||
+ | userId: this.userId | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | module.exports = { | ||
+ | JwtPayload | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | === Clientul HTTP (http-client/index.js) === | ||
+ | |||
+ | Deoarece microserviciile sunt entitati/procese separate, ele comunica prin retea, nu in process. Pentru acest lucru, este nevoie de utilizarea unei forme de comunicare peste HTTP, sau event-based, prin cozi de mesaje. La acest workshop vom lucra exclusiv prin comunicare peste HTTP. | ||
+ | |||
+ | In NodeJS, comunicarea prin HTTP se poate face in multe moduri, insa cel mai uzual este prin folosirea pachetului **axios**. Mai mult, noi am scris un wrapper peste acest pachet care se ocupa si de error handling. | ||
+ | |||
+ | <code javascript> | ||
+ | const axios = require('axios').default; | ||
+ | |||
+ | const { | ||
+ | ServerError | ||
+ | } = require('../errors'); | ||
+ | |||
+ | const sendRequest = async (options) => { // wrapper peste apelul de axios. Parametrul este un obiect care contine cel putin unu camp, url. Documentatia o gasiti pe pagina pachetului axios | ||
+ | try { | ||
+ | const { data } = await axios(options); // daca se realizeaza cu succes, este intors un obiect ce contine campul data unde se afla informatia efectiva | ||
+ | return data; | ||
+ | } catch (err) { | ||
+ | if (err.isAxiosError) { // eroarea aruncata de axios are proprietatea specifica "isAxiosError" | ||
+ | throw new ServerError(err.response.data.message, err.response.status); | ||
+ | } | ||
+ | throw err; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | module.exports = { | ||
+ | sendRequest | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | === Utilizarea filtrelor JWT === | ||
+ | |||
+ | Filtrele JWT sunt atasate rutelor. Un exemplu de utilizare il puteti gasi in token/controllers.js. Un alt exemplu este in users/controllers.js | ||
+ | |||
+ | <code javascript> | ||
+ | // users/controllers.js | ||
+ | |||
+ | Router.delete('/', authorizeAndExtractToken, async (req, res) => { // observati cum filtrul JWT este atasat cererii | ||
+ | const { | ||
+ | userId | ||
+ | } = req.state.decoded; | ||
+ | |||
+ | await remove(userId); | ||
+ | |||
+ | res.status(200).end(); | ||
+ | }); | ||
+ | |||
+ | |||
+ | </code> | ||
+ | |||
+ | === Interactiunea dintre microserviciul Library si microserviciul Auth === | ||
+ | |||
+ | In microserviciul Library am creat un nou filtru de autorizare. | ||
+ | |||
+ | Rolul acestui filtru este de a se conecta la microservicul de autorizare. Se trimite o cerere HTTP catre microserviciul de autorizare, prin care forwardeaza headerul ce contine tokenul JWT. | ||
+ | |||
+ | Microserviciul de autorizare despacheteaza tokenul si returneaza **id-ul utilizatorului care a facut cererea** si **permisiunea acestuia** in sistem. | ||
+ | |||
+ | Ruta din microserviciul de autorizare responsabila pentru autorizarea cererilor provenite de la alte microservicii | ||
+ | <code javascript> | ||
+ | // auth-microservice/token/controllers.js | ||
+ | |||
+ | Router.get('/authorizeAndReturnDetails', authorizeAndExtractToken, async (req, res) => { | ||
+ | const { | ||
+ | userId | ||
+ | } = req.state.decoded; | ||
+ | |||
+ | const role = await getRole(userId); | ||
+ | |||
+ | res.json({ | ||
+ | userId, | ||
+ | role | ||
+ | }); | ||
+ | }); | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Filtrul din microserviciul library responsabil cu trimiterea cererilor de autorizare catre microserviciul de autorizare. Filtrul este o functie curry (functie ce returneaza alta functie). | ||
+ | <code javascript> | ||
+ | const authorizeUser = (...roles) => { //poate accepta mai mult de un singur rol (de exemplu 'ADMIN', 'READER') | ||
+ | |||
+ | return async (req, res, next) => { //returneaza un middleware clasic ce contine req, res, next | ||
+ | if (!req.headers.authorization) { | ||
+ | throw new ServerError("Authorization Header missing!", 403); | ||
+ | } | ||
+ | |||
+ | const options = { //creaza obiectul options ce va fi trimis clientului de http | ||
+ | url: `http://${process.env.AUTH_SERVICE}/api/token/authorizeAndReturnDetails`, //IP-ul si PORT-ul serviciului de auth sunt preluate din .env. Observati ruta. | ||
+ | headers: { | ||
+ | 'Authorization': req.headers.authorization | ||
+ | } | ||
+ | } | ||
+ | |||
+ | const { | ||
+ | userId, | ||
+ | role | ||
+ | } = await sendRequest(options); | ||
+ | |||
+ | if (!roles.includes(role)) { // se verifica daca rolul primit face parte din rolurile date ca parametru | ||
+ | throw new ServerError(`${role} is not allowed for this method!`, 403); | ||
+ | } | ||
+ | |||
+ | req.state = { | ||
+ | userId | ||
+ | } | ||
+ | |||
+ | next(); // se apeleaza next pentru a se trece la urmatorul middleware | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Exemplu de utilizare in cadrul microserviciului Library: | ||
+ | <code javascript> | ||
+ | const { | ||
+ | ADMIN_ROLE, | ||
+ | READER_ROLE | ||
+ | } = process.env; //preluarea valorilor celor 2 roluri din .env | ||
+ | |||
+ | Router.get('/:id', authorizeUser(ADMIN_ROLE, READER_ROLE), async (req, res) => { //introducerea filtrului pentru ambele roluri. Observati cum, de fapt, este pus un apel de functie. Motivul este datorat comportamentului de functie curry (functie in functie) pe care l-am folosit pentru a returna middleware-ul ce contine (req, res, next) | ||
+ | const { | ||
+ | id | ||
+ | } = req.params; | ||
+ | |||
+ | const author = await getById(id); | ||
+ | |||
+ | res.json(author); | ||
+ | |||
+ | }); | ||
+ | </code> | ||
+ | |||
+ | === Baza de date === | ||
+ | |||
+ | Baza de date functioneaza la fel ca la microserviciul Library. Aveti un folder **database-deploy** in care veti efectua comanda | ||
+ | <code> | ||
+ | docker-compose up | ||
+ | </code> | ||
+ | |||
+ | === Exercitii === | ||
+ | |||
+ | Singurul exercitiu este sincronizarea dintre stergerea unui utilizator cu stergerea tuturor cartilor pe care acesta le-a introdus in sistem. Pentru acest lucru, trebuie sa urmariti TODO-urile puse in scheletul de cod. |