Autentizace a autorizace pomocí JWT v Express.js

Úvod

V tomto článku budeme hovořit o tom, jak fungují webové tokeny JSON, jaké jsou jejich výhody, jejich struktura a jak je používat ke zpracování základní autentizace a autorizace v Express.

Nemusíte mít žádné předchozí zkušenosti s webovými tokeny JSON, protože o nich budeme mluvit od začátku.

Pro sekci implementace by bylo preferováno, pokud máte předchozí zkušenosti s klienty Express, Javascript ES6 a REST.

Co jsou webové tokeny JSON?

Webové tokeny JSON (JWT) byly zavedeny jako metoda bezpečné komunikace mezi dvěma stranami. Byl představen se specifikací RFC 7519 Internet Engineering Task Force (IETF).

I když můžeme JWT použít s jakýmkoli typem komunikační metody, dnes je JWT velmi populární pro zpracování autentizace a autorizace přes HTTP.

Nejprve budete potřebovat znát několik charakteristik HTTP.

HTTP je bezstavový protokol, což znamená, že požadavek HTTP neudržuje stav. Server neví o žádných předchozích požadavcích odeslaných stejným klientem.

Požadavky HTTP by měly být samostatné. Měly by obsahovat informace o předchozích požadavcích, které uživatel učinil v samotném požadavku.

Existuje několik způsobů, jak toho dosáhnout, ale nejoblíbenějším způsobem je nastavení ID relace , což je odkaz na informace o uživateli.

Server uloží toto ID relace do paměti nebo do databáze. Klient odešle každý požadavek s tímto ID relace. Server pak může načíst informace o klientovi pomocí této reference.

Zde je schéma toho, jak funguje ověřování založené na relacích:

Toto ID relace je obvykle odesláno uživateli jako soubor cookie. Již jsme to podrobně probrali v našem předchozím článku Zpracování ověřování v Express.js.

Na druhou stranu u JWT, když klient odešle na server požadavek na ověření, odešle klientovi zpět token JSON, který obsahuje všechny informace o uživateli s odpovědí.

Klient odešle tento token spolu se všemi následujícími požadavky. Server tedy nebude muset ukládat žádné informace o relaci. S tím přístupem je ale problém. Kdokoli může odeslat falešný požadavek s falešným tokenem JSON a předstírat, že je někdo, kým není.

Řekněme například, že po ověření server odešle zpět klientovi objekt JSON s uživatelským jménem a dobou expirace. Protože je tedy objekt JSON čitelný, kdokoli může tyto informace upravit a odeslat požadavek. Problém je, že neexistuje způsob, jak takový požadavek ověřit.

Zde přichází na řadu podepsání tokenu. Takže místo pouhého zaslání zpět prostého tokenu JSON, server odešle podepsaný token, který může ověřit, že se informace nezměnily.

Podrobněji se tomu budeme věnovat později v tomto článku.

Zde je schéma toho, jak JWT funguje:

Struktura JWT

Promluvme si o struktuře JWT prostřednictvím ukázkového tokenu:

Jak můžete vidět na obrázku, tento JWT má tři části, z nichž každá je oddělena tečkou.

Postranní panel:Kódování Base64 je jedním ze způsobů, jak zajistit, aby data nebyla poškozena, protože data nekomprimuje ani nešifruje, ale jednoduše je zakóduje způsobem, kterému většina systémů rozumí. Jakýkoli text kódovaný Base64 můžete číst jednoduchým dekódováním.

První částí JWT je záhlaví, což je řetězec zakódovaný v Base64. Pokud byste dekódovali záhlaví, vypadalo by to nějak podobně:

{
  "alg": "HS256",
  "typ": "JWT"
}

Sekce záhlaví obsahuje hashovací algoritmus, který byl použit ke generování znaménka a typu tokenu.

Druhá část je datová část, která obsahuje objekt JSON, který byl odeslán zpět uživateli. Vzhledem k tomu, že se jedná pouze o kódování Base64, může jej snadno dekódovat kdokoli.

Doporučuje se nezahrnovat do JWT žádná citlivá data, jako jsou hesla nebo osobní údaje.

Obvykle bude tělo JWT vypadat nějak takto, i když to není nutně vynuceno:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Většinu času sub vlastnost bude obsahovat ID uživatele, vlastnost iat , což je zkratka pro vydáno na , je časové razítko vydání tokenu.

Můžete také vidět některé běžné vlastnosti, jako je eat nebo exp , což je doba vypršení platnosti tokenu.

Poslední částí je podpis tokenu. To je generováno hašováním řetězce base64UrlEncode(header) + "." + base64UrlEncode(payload) + secret pomocí algoritmu, který je zmíněn v záhlaví.

secret je náhodný řetězec, který by měl znát pouze server. Žádný hash nelze převést zpět na původní text a i malá změna původního řetězce bude mít za následek jiný hash. Takže secret nelze provést zpětným inženýrstvím.

Když se tento podpis odešle zpět na server, může ověřit, že klient nezměnil žádné podrobnosti v objektu.

Podle standardů by měl klient odeslat tento token na server prostřednictvím HTTP požadavku v hlavičce nazvané Authorization ve tvaru Bearer [JWT_TOKEN] . Tedy hodnotu Authorization záhlaví bude vypadat nějak takto:

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

Pokud si chcete přečíst více o struktuře tokenu JWT, můžete se podívat na náš podrobný článek Pochopení webových tokenů JSON. Můžete také navštívit jwt.io a pohrát si s jejich debuggerem:

Výhoda použití JWT oproti tradičním metodám

Jak jsme již uvedli dříve, JWT může obsahovat všechny informace o samotném uživateli, na rozdíl od autentizace založené na relaci.

To je velmi užitečné pro škálování webových aplikací, jako je webová aplikace s mikroslužbami. Dnes vypadá architektura moderní webové aplikace podobně jako tato:

Všechny tyto služby mohou být stejnou službou, která bude přesměrována nástrojem pro vyrovnávání zatížení podle využití zdrojů (využití CPU nebo paměti) každého serveru, nebo podle některých různých služeb, jako je autentizace atd.

Pokud používáme tradiční metody autorizace, jako jsou soubory cookie, budeme muset sdílet databázi, jako je Redis, abychom mohli sdílet komplexní informace mezi servery nebo interními službami. Ale pokud sdílíme tajemství napříč mikroslužbami, můžeme použít JWT a pak nejsou potřeba žádné další externí zdroje k autorizaci uživatelů.

Používání JWT s Express

V tomto tutoriálu vytvoříme jednoduchou webovou aplikaci založenou na mikroslužbách pro správu knih v knihovně se dvěma službami. Jedna služba bude zodpovědná za ověřování uživatelů a druhá bude zodpovědná za správu knih.

Budou dva typy uživatelů – administrátoři a členové . Správci budou moci prohlížet a přidávat nové knihy, zatímco členové je budou moci pouze prohlížet. V ideálním případě by také mohli být schopni upravovat nebo mazat knihy. Ale aby byl tento článek co nejjednodušší, nebudeme zabíhat do takových podrobností.

Chcete-li začít, inicializujte ve svém terminálu prázdný projekt Node.js s výchozím nastavením:

$ npm init -y

Poté nainstalujme Express framework:

$ npm install --save express

Služba ověřování

Poté vytvořte soubor s názvem auth.js , což bude naše autentizační služba:

const express = require('express');
const app = express();

app.listen(3000, () => {
    console.log('Authentication service started on port 3000');
});

V ideálním případě bychom měli k ukládání uživatelských informací používat databázi. Ale aby to bylo jednoduché, vytvořte pole uživatelů, které budeme používat k jejich autentizaci.

Pro každého uživatele bude existovat role - admin nebo member připojené k jejich uživatelskému objektu. Pokud jste v produkčním prostředí, nezapomeňte heslo hashovat:

const users = [
    {
        username: 'john',
        password: 'password123admin',
        role: 'admin'
    }, {
        username: 'anna',
        password: 'password123member',
        role: 'member'
    }
];

Nyní můžeme vytvořit obslužný program pro přihlášení uživatele. Pojďme si nainstalovat modul jsonwebtoken, který se používá ke generování a ověřování tokenů JWT.

Nainstalujme také body-parser middleware k analýze těla JSON z požadavku HTTP:

$ npm i --save body-parser jsonwebtoken

Nyní se podívejme na tyto moduly a nakonfigurujte je v aplikaci Express:

const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

app.use(bodyParser.json());

Zdarma e-kniha:Git Essentials

Prohlédněte si našeho praktického průvodce učením Git s osvědčenými postupy, průmyslově uznávanými standardy a přiloženým cheat sheetem. Přestaňte používat příkazy Google Git a skutečně se naučte to!

Nyní můžeme vytvořit obslužnou rutinu požadavku pro zpracování požadavku na přihlášení uživatele:

const accessTokenSecret = 'youraccesstokensecret';

Toto je vaše tajemství k podpisu tokenu JWT. Toto tajemství byste nikdy neměli sdílet, jinak by ho mohl špatný herec použít k padělání tokenů JWT, aby získal neoprávněný přístup k vaší službě. Čím složitější je tento přístupový token, tím bezpečnější bude vaše aplikace. Zkuste tedy pro tento token použít složitý náhodný řetězec:

app.post('/login', (req, res) => {
    // Read username and password from request body
    const { username, password } = req.body;

    // Filter user from the users array by username and password
    const user = users.find(u => { return u.username === username && u.password === password });

    if (user) {
        // Generate an access token
        const accessToken = jwt.sign({ username: user.username,  role: user.role }, accessTokenSecret);

        res.json({
            accessToken
        });
    } else {
        res.send('Username or password incorrect');
    }
});

V tomto handleru jsme hledali uživatele, který odpovídá uživatelskému jménu a heslu v těle požadavku. Poté jsme vygenerovali přístupový token s objektem JSON s uživatelským jménem a rolí uživatele.

Naše autentizační služba je připravena. Spusťte jej spuštěním:

$ node auth.js

Po spuštění autentizační služby odešleme požadavek POST a uvidíme, zda funguje.

K tomu použiji klienta Insomnia zbytku. K tomu můžete použít libovolného klienta pro odpočinek nebo něco jako Postman.

Odešleme žádost o příspěvek na číslo http://localhost:3000/login koncový bod s následujícím JSON:

{
    "username": "john",
    "password": "password123admin"
}

Jako odpověď byste měli získat přístupový token:

{
  "accessToken": "eyJhbGciOiJIUz..."
}

Knihová služba

Po dokončení vytvoříme books.js soubor pro naši knižní službu.

Začneme souborem importováním požadovaných knihoven a nastavením aplikace Express:

const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');

const app = express();

app.use(bodyParser.json());

app.listen(4000, () => {
    console.log('Books service started on port 4000');
});

Po konfiguraci, abychom simulovali databázi, vytvořme pole knih:

const books = [
    {
        "author": "Chinua Achebe",
        "country": "Nigeria",
        "language": "English",
        "pages": 209,
        "title": "Things Fall Apart",
        "year": 1958
    },
    {
        "author": "Hans Christian Andersen",
        "country": "Denmark",
        "language": "Danish",
        "pages": 784,
        "title": "Fairy tales",
        "year": 1836
    },
    {
        "author": "Dante Alighieri",
        "country": "Italy",
        "language": "Italian",
        "pages": 928,
        "title": "The Divine Comedy",
        "year": 1315
    },
];

Nyní můžeme vytvořit velmi jednoduchý obslužný program pro načtení všech knih z databáze:

app.get('/books', (req, res) => {
    res.json(books);
});

Protože naše knihy by měly být viditelné pouze pro ověřené uživatele. Musíme vytvořit middleware pro ověřování.

Předtím vytvořte tajný přístupový token pro podepisování JWT, stejně jako předtím:

const accessTokenSecret = 'youraccesstokensecret';

Tento token by měl být stejný jako token používaný v ověřovací službě. Vzhledem k tomu, že tajemství je mezi nimi sdíleno, můžeme se ověřit pomocí autentizační služby a poté autorizovat uživatele v knižní službě.

V tomto okamžiku vytvoříme expresní middleware, který zpracovává proces ověřování:

const authenticateJWT = (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (authHeader) {
        const token = authHeader.split(' ')[1];

        jwt.verify(token, accessTokenSecret, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }

            req.user = user;
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

V tomto middlewaru čteme hodnotu autorizační hlavičky. Od authorization hlavička má hodnotu ve formátu Bearer [JWT_TOKEN] , rozdělili jsme hodnotu mezerou a oddělili token.

Poté jsme token ověřili pomocí JWT. Po ověření připojíme user vznést námitku do požadavku a pokračovat. V opačném případě odešleme klientovi chybu.

Tento middleware můžeme nakonfigurovat v našem obslužném programu požadavku GET takto:

app.get('/books', authenticateJWT, (req, res) => {
    res.json(books);
});

Spusťte server a otestujte, zda vše funguje správně:

$ node books.js

Nyní můžeme odeslat požadavek na http://localhost:4000/books koncový bod pro načtení všech knih z databáze.

Ujistěte se, že jste změnili záhlaví "Authorization" tak, aby obsahovalo hodnotu "Bearer [JWT_TOKEN]", jak je znázorněno na obrázku níže:

Nakonec můžeme vytvořit náš obslužný program pro vytvoření knihy. Protože pouze admin můžete přidat novou knihu, v tomto handleru musíme také zkontrolovat uživatelskou roli.

I v tomto můžeme použít autentizační middleware, který jsme použili výše:

app.post('/books', authenticateJWT, (req, res) => {
    const { role } = req.user;

    if (role !== 'admin') {
        return res.sendStatus(403);
    }


    const book = req.body;
    books.push(book);

    res.send('Book added successfully');
});

Protože autentizační middleware váže uživatele k požadavku, můžeme načíst role z req.user objekt a jednoduše zkontrolujte, zda je uživatel admin . Pokud ano, bude kniha přidána, jinak bude vyvolána chyba.

Zkusme to s naším klientem REST. Přihlaste se jako admin uživatele (pomocí stejné metody jako výše) a poté zkopírujte accessToken a odešlete jej pomocí Authorization záhlaví, jak jsme to udělali v předchozím příkladu.

Poté můžeme odeslat požadavek POST na http://localhost:4000/books koncový bod:

{
    "author": "Jane Austen",
    "country": "United Kingdom",
    "language": "English",
    "pages": 226,
    "title": "Pride and Prejudice",
    "year": 1813
}

Obnovení tokenu

V tuto chvíli naše aplikace zpracovává jak ověřování, tak autorizaci pro knižní službu, i když existuje hlavní chyba v designu – token JWT nikdy nevyprší.

Pokud je tento token ukraden, bude mít přístup k účtu navždy a skutečný uživatel nebude moci přístup zrušit.

Abychom tuto možnost odstranili, aktualizujme naši obsluhu žádosti o přihlášení, aby platnost tokenu vypršela po určité době. Můžeme to udělat předáním expiresIn vlastnictví jako možnost podepsat JWT.

Když vyprší platnost tokenu, měli bychom mít také strategii pro generování nového, v případě vypršení platnosti. Za tímto účelem vytvoříme samostatný token JWT, který se nazývá obnovovací token , který lze použít ke generování nového.

Nejprve vytvořte tajný klíč obnovovacího tokenu a prázdné pole pro uložení obnovovacích tokenů:

const refreshTokenSecret = 'yourrefreshtokensecrethere';
const refreshTokens = [];

Když se uživatel přihlásí, místo generování jednoho tokenu vygenerujte obnovovací i ověřovací tokeny:

app.post('/login', (req, res) => {
    // read username and password from request body
    const { username, password } = req.body;

    // filter user from the users array by username and password
    const user = users.find(u => { return u.username === username && u.password === password });

    if (user) {
        // generate an access token
        const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' });
        const refreshToken = jwt.sign({ username: user.username, role: user.role }, refreshTokenSecret);

        refreshTokens.push(refreshToken);

        res.json({
            accessToken,
            refreshToken
        });
    } else {
        res.send('Username or password incorrect');
    }
});

A nyní vytvoříme obslužnou rutinu požadavků, která vygeneruje nové tokeny na základě obnovovacích tokenů:

app.post('/token', (req, res) => {
    const { token } = req.body;

    if (!token) {
        return res.sendStatus(401);
    }

    if (!refreshTokens.includes(token)) {
        return res.sendStatus(403);
    }

    jwt.verify(token, refreshTokenSecret, (err, user) => {
        if (err) {
            return res.sendStatus(403);
        }

        const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' });

        res.json({
            accessToken
        });
    });
});

I s tím je ale problém. Pokud je obnovovací token uživateli odcizen, někdo jej může použít k vygenerování tolika nových tokenů, kolik chce.

Abychom tomu zabránili, implementujme jednoduchý logout funkce:

app.post('/logout', (req, res) => {
    const { token } = req.body;
    refreshTokens = refreshTokens.filter(token => t !== token);

    res.send("Logout successful");
});

Když uživatel požádá o odhlášení, odstraníme obnovovací token z našeho pole. Zajišťuje, že když je uživatel odhlášen, nikdo nebude moci použít obnovovací token k vygenerování nového ověřovacího tokenu.

Závěr

V tomto článku jsme vám představili JWT a jak implementovat JWT pomocí Express. Doufám, že nyní máte dobré znalosti o tom, jak JWT funguje a jak jej implementovat do vašeho projektu.

Zdrojový kód je jako vždy dostupný na GitHubu.


No