Hur man autentiserar och auktoriserar användare med JWT i NodeJS

By rik

Grunderna i Autentisering och Auktorisering

Säkerheten i datorsystem bygger på två viktiga begrepp: autentisering och auktorisering. Autentisering handlar om att bevisa att du är den du utger dig för att vara. Detta görs vanligen genom att ange användaruppgifter, som ett användarnamn och lösenord, vilket bekräftar din identitet som en registrerad användare. Efter autentiseringen kan du sedan få tillgång till ytterligare funktioner och resurser baserat på din behörighet.

Detta princip gäller även när du loggar in på onlinetjänster med ett konto från exempelvis Facebook eller Google.

I den här guiden ska vi skapa ett Node.js API som använder JWT (JSON Web Tokens) för autentisering. Vi kommer att använda följande verktyg:

  • Express.js
  • MongoDB databas
  • Mongoose
  • Dotenv
  • Bcrypt.js
  • Jsonwebtoken

Autentisering kontra Auktorisering

Vad är Autentisering?

Autentisering är processen att verifiera en användares identitet genom att kontrollera deras uppgifter, såsom e-post, lösenord eller tokens. Dessa inmatade uppgifter jämförs sedan med sparade data, antingen i det lokala systemet eller i en databas. Om uppgifterna matchar, anses autentiseringen lyckad och användaren får tillgång till resurserna.

Vad är Auktorisering?

Auktorisering sker *efter* autentisering och kräver att autentiseringen redan har ägt rum. Auktorisering är processen att bevilja behörighet till autentiserade användare att få tillgång till specifika resurser på ett system eller webbplats. I den här guiden kommer vi att ge inloggade användare möjlighet att komma åt sin egen data. Användare som inte är inloggade ska inte ha tillgång till dessa data.

Tänk på sociala medieplattformar som Facebook eller Twitter som exempel. Du kan inte se innehåll utan att ha ett konto. Ett annat exempel är prenumerationsbaserat innehåll; även om du har loggat in, kommer du inte att kunna se innehållet om du inte har en giltig prenumeration.

Förutsättningar

Innan vi fortsätter förutsätts du ha grundläggande kunskaper i Javascript, MongoDB och god förståelse för Node.js.

Se till att Node.js och npm är installerade på din dator. Kontrollera detta genom att öppna kommandotolken och skriva `node -v` och `npm -v`. Detta ska visa de installerade versionerna.

Dina versioner kan skilja sig från bilden ovan. NPM installeras automatiskt med Node.js. Om du inte har dem, ladda ner dem från Node.js webbplats.

Du behöver också en IDE (Integrated Development Environment) för att skriva kod. Den här guiden använder VS Code. Om du föredrar en annan IDE kan du använda den. Om du inte har någon IDE kan du ladda ner Visual Studio Code. Ladda ner den version som passar ditt operativsystem.

Projektet

Börja med att skapa en mapp, vi kan kalla den `nodeapi`, någonstans på din dator. Öppna sedan mappen i VS Code. Öppna VS Code-terminalen och kör följande kommando för att initiera Node Package Manager:

npm init -y

Se till att du befinner dig i `nodeapi`-katalogen.

Detta kommando skapar en `package.json`-fil som kommer att innehålla alla beroenden som vi behöver för projektet.

Nu installerar vi de nödvändiga paketen. I terminalen skriver du in följande:

npm install express dotenv jsonwebtoken mongoose bcryptjs

Du bör nu se en liknande struktur som i bilden nedan:

Skapa Server och Koppla till Databas

Skapa nu en fil som heter `index.js` och en mapp som heter `config`. Inuti `config`, skapa två filer: `conn.js` för att ansluta till databasen och `config.env` för miljövariabler. Lägg till nedanstående kod i respektive filer.

index.js

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

// Konfigurera dotenv-filer
dotenv.config({path:'./config/config.env'});

// Skapa en app med express
const app = express();

// Använd express.json för att hantera JSON-data i förfrågningar
app.use(express.json());

// Starta servern
app.listen(process.env.PORT,()=>{
    console.log(`Servern lyssnar på port ${process.env.PORT}`);
})

Om du använder dotenv, se till att den konfigureras i din `index.js`-fil innan andra filer som använder miljövariabler inkluderas.

conn.js

const mongoose = require('mongoose');

mongoose.connect(process.env.URI, 
    { useNewUrlParser: true,
     useUnifiedTopology: true })
    .then((data) => {
        console.log(`Databasen är ansluten till ${data.connection.host}`)
})

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000

Jag använder en MongoDB Atlas URI, men du kan även använda localhost.

Skapa Modeller och Rutter

En modell beskriver strukturen på din data i MongoDB-databasen, och kommer att sparas som JSON-dokument. Vi använder Mongoose schema för att definiera en modell.

Rutter definierar hur din applikation svarar på klientförfrågningar. Vi kommer att använda express router-funktionen för att hantera detta.

Rutt-metoder tar oftast två argument: den första är själva routen, och den andra är en callback-funktion som definierar vad som ska hända när klienten gör en förfrågan. En tredje parameter, en middleware-funktion, kan också användas vid behov, till exempel under autentiseringen. När vi skapar ett autentiserat API kommer vi också att använda middleware för att autentisera och auktorisera användare.

Skapa två mappar: `routes` och `models`. Inuti `routes`, skapa filen `userRoute.js`, och inuti `models`, skapa `userModel.js`. Lägg till följande kod i respektive filer.

userModel.js

const mongoose = require('mongoose');

// Skapa schema med mongoose
const userSchema = new mongoose.Schema({
    name: {
        type:String,
        required:true,
        minLength:[4,'Namnet måste vara minst 4 tecken']
    },
    email:{
        type:String,
        required:true,
        unique:true,
    },
    password:{
        type:String,
        required:true,
        minLength:[8,'Lösenordet måste vara minst 8 tecken']
    },
    token:{
        type:String
    }
})

// Skapa model
const userModel = mongoose.model('user',userSchema);
module.exports = userModel;

userRoute.js

const express = require('express');
// Skapa en express router
const route = express.Router();
// Importera userModel
const userModel = require('../models/userModel');

// Skapa registrering route
route.post('/register',(req,res)=>{

})
// Skapa inloggnings route
route.post('/login',(req,res)=>{

})

// Skapa route för att hämta användardata
route.get('/user',(req,res)=>{

})

Implementera Ruttfunktionalitet och Skapa JWT Tokens

Vad är JWT?

JSON Web Tokens (JWT) är ett bibliotek som används för att skapa och verifiera tokens. Det är en öppen standard som används för att säkert dela information mellan två parter, en klient och en server. Vi kommer att använda två funktioner från JWT: sign för att skapa en ny token och verify för att verifiera en befintlig token.

Vad är bcrypt.js?

Bcrypt.js är en hashfunktion som används för att säkra lösenord. Den använder en hash-algoritm för att omvandla lösenordet till ett hashvärde. Vi kommer att använda två funktioner: hash för att generera ett hashvärde, och compare för att jämföra ett angivet lösenord med ett hashat lösenord.

Implementera Ruttfunktionalitet

Callback-funktionen i routing tar tre argument: request, response och next. Det sista argumentet är valfritt och används endast om du behöver det. Dessa argument ska alltid vara i just den ordningen. Ändra nu `userRoute.js`, `config.env` och `index.js` med följande kod:

userRoute.js

// Importera nödvändiga filer och bibliotek
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// Skapa en express router
const route = express.Router();
// Importera userModel
const userModel = require('../models/userModel');

// Skapa registrering route
route.post("/register", async (req, res) => {

    try {
        const { name, email, password } = req.body;
        // Kontrollera om data saknas
        if (!name || !email || !password) {
            return res.json({ message: 'Vänligen fyll i alla fält' })
        }

        // Kontrollera om användaren redan finns
        const userExist = await userModel.findOne({ email: req.body.email });
        if (userExist) {
            return res.json({ message: 'En användare med den e-postadressen finns redan' })
        }
        // Hasha lösenordet
        const salt = await bcrypt.genSalt(10);
        const hashPassword = await bcrypt.hash(req.body.password, salt);
        req.body.password = hashPassword;
        const user = new userModel(req.body);
        await user.save();
        const token = await jwt.sign({ id: user._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({ 'token': token }).json({ success: true, message: 'Användaren är nu registrerad', data: user })
    } catch (error) {
        return res.json({ error: error });
    }

})
// Skapa inloggnings route
route.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        // Kontrollera om data saknas
        if (!email || !password) {
            return res.json({ message: 'Vänligen fyll i alla fält' })
        }
        // Kontrollera om användaren finns
        const userExist = await userModel.findOne({email:req.body.email});
        if(!userExist){
            return res.json({message:'Felaktiga uppgifter'})
        }
        // Kontrollera lösenord matchar
        const isPasswordMatched = await bcrypt.compare(password,userExist.password);
        if(!isPasswordMatched){
            return res.json({message:'Felaktiga lösenord'})
        }
        const token = await jwt.sign({ id: userExist._id }, process.env.SECRET_KEY, {
            expiresIn: process.env.JWT_EXPIRE,
        });
        return res.cookie({"token":token}).json({success:true,message:'Inloggningen lyckades'})
    } catch (error) {
        return res.json({ error: error });
    }

})

// Skapa route för att hämta användardata
route.get('/user', async (req, res) => {
    try {
        const user  = await userModel.find();
        if(!user){
            return res.json({message:'Ingen användare hittades'})
        }
        return res.json({user:user})
    } catch (error) {
        return res.json({ error: error });  
    }
})

module.exports = route;

När du använder async funktioner, använd try-catch blocket, annars kan du få ett ohanterat promise reject fel.

config.env

URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority'
PORT = 5000
SECRET_KEY = KGGK>HKHVHJVKBKJKJBKBKHKBMKHB
JWT_EXPIRE = 2d

index.js

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

// Konfigurera dotenv-filer
dotenv.config({path:'./config/config.env'});
require('./config/conn');
// Skapa en app med express
const app = express();
const route = require('./routes/userRoute');

// Använd express.json för att hantera JSON-data i förfrågningar
app.use(express.json());
// Använd rutter

app.use('/api', route);

// Starta servern
app.listen(process.env.PORT,()=>{
    console.log(`Servern lyssnar på port ${process.env.PORT}`);
})

Skapa Middleware för att Autentisera Användare

Vad är Middleware?

Middleware är en funktion som har tillgång till förfrågan, svars-objekt och den nästa funktionen i förfrågan-svar-cykeln. Nästa funktionen anropas när den aktuella funktionens exekvering är klar. Som nämnt tidigare, använd `next()` när du behöver anropa en annan callback- eller middleware-funktion.

Skapa nu en mapp med namnet `middleware` och i den filen `auth.js` med följande kod:

auth.js

const userModel = require('../models/userModel');
const jwt = require('jsonwebtoken');
const isAuthenticated = async (req,res,next)=>{
    try {
        const {token} = req.cookies;
        if(!token){
            return next('Vänligen logga in för att se data');
        }
        const verify = await jwt.verify(token,process.env.SECRET_KEY);
        req.user = await userModel.findById(verify.id);
        next();
    } catch (error) {
       return next(error);
    }
}

module.exports = isAuthenticated;

Installera nu cookie-parser-biblioteket för att konfigurera cookieParser i din app. cookieParser gör det enkelt att hantera cookies. Utan cookieParser konfigurerat, kan du inte nå cookies från request-objektet. I terminalen, skriv följande för att installera cookie-parser:

npm i cookie-parser

Nu när cookie-parser är installerat, konfigurera din app genom att ändra `index.js` och lägg till middleware till routen `/user`.

index.js

const cookieParser = require('cookie-parser');
const express = require('express');
const dotenv = require('dotenv');

// Konfigurera dotenv-filer
dotenv.config({path:'./config/config.env'});
require('./config/conn');
// Skapa en app med express
const app = express();
const route = require('./routes/userRoute');

// Använd express.json för att hantera JSON-data i förfrågningar
app.use(express.json());
// Konfigurera cookie-parser
app.use(cookieParser());

// Använd rutter
app.use('/api', route);

// Starta servern
app.listen(process.env.PORT,()=>{
    console.log(`Servern lyssnar på port ${process.env.PORT}`);
})

userRoute.js

// Importera nödvändiga filer och bibliotek
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const isAuthenticated = require('../middleware/auth');

// Skapa en express router
const route = express.Router();
// Importera userModel
const userModel = require('../models/userModel');

// Skapa route för att hämta användardata
route.get('/user', isAuthenticated, async (req, res) => {
    try {
        const user = await userModel.find();
        if (!user) {
            return res.json({ message: 'Ingen användare hittades' })
        }
        return res.json({ user: user })
    } catch (error) {
        return res.json({ error: error });
    }
})

module.exports = route;

Routen `/user` är nu endast tillgänglig för inloggade användare.

Testa API:erna i POSTMAN

Innan du testar API:erna måste du redigera `package.json`-filen och lägga till följande:

"scripts": {
    "test": "echo "Error: no test specified" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

Du kan starta servern genom att skriva `npm start`, men den körs bara en gång. För att hålla servern aktiv under utveckling behöver du `nodemon`. Du installerar den genom att skriva i terminalen:

npm install -g nodemon

Flaggan `-g` installerar nodemon globalt på ditt system, så du behöver inte installera den på nytt för varje projekt.

För att starta servern skriver du `npm run dev` i terminalen. Du ska då se ett meddelande som bekräftar att servern startat.

Nu är din kod färdig och servern körs, så du kan öppna POSTMAN och verifiera funktionaliteten.

Vad är POSTMAN?

POSTMAN är ett verktyg som används för att designa, bygga, utveckla och testa API:er.

Om du inte har POSTMAN, ladda ner det från POSTMANs hemsida.

Öppna POSTMAN och skapa en ny samling, döp den till `nodeAPItest`, och skapa tre förfrågningar inuti: `register`, `login` och `user`. Du ska ha en liknande struktur som bilden nedan:

När du skickar JSON-data till `localhost:5000/api/register`, ska du få ett liknande svar:

Eftersom vi skapar och sparar tokens i cookies när vi registrerar oss kan du hämta användardata genom att skicka en request till routen `localhost:5000/api/user`. Du kan själv testa de övriga requests i POSTMAN.

Om du vill se hela koden finns den tillgänglig på mitt github-konto.

Slutsats

I den här handledningen har vi lärt oss att implementera autentisering i ett Node.js API med hjälp av JWT-tokens. Vi har också auktoriserat användare för att ge tillgång till deras egna data.

Lycka till med kodningen!