Differences

This shows you the differences between two versions of the page.

Link to this comparison view

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 DockerIn 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 ​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 = {...generalOptionsignoreExpiration} 
 +        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.
moby/backend/04.txt · Last modified: 2020/07/30 12:29 by alexandru.hogea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0