Podstawy bezpieczeństwa: Uwierzytelnianie i autoryzacja
Uwierzytelnianie i autoryzacja stanowią fundament bezpieczeństwa w świecie komputerów. Proces uwierzytelniania polega na weryfikacji tożsamości użytkownika, zazwyczaj poprzez podanie nazwy użytkownika i hasła. Po udanym uwierzytelnieniu, użytkownik może uzyskać dostęp do dodatkowych zasobów, co reguluje autoryzacja.
Identyczna zasada dotyczy logowania do serwisów internetowych za pomocą kont takich jak Facebook czy Google.
W tym artykule skoncentrujemy się na stworzeniu API w Node.js, gdzie uwierzytelnianie będzie realizowane za pomocą tokenów JWT (JSON Web Tokens). W trakcie tego procesu wykorzystamy:
- Express.js
- Bazę danych MongoDB
- Bibliotekę Mongoose
- Moduł Dotenv
- Bibliotekę Bcrypt.js
- Bibliotekę Jsonwebtoken
Różnica między uwierzytelnianiem a autoryzacją
Czym jest uwierzytelnianie?
Uwierzytelnianie to proces weryfikacji tożsamości użytkownika. Odbywa się to poprzez porównanie dostarczonych danych, takich jak adres e-mail, hasło lub tokeny, z informacjami przechowywanymi w lokalnym systemie komputerowym lub bazie danych. Po potwierdzeniu zgodności danych, użytkownik uzyskuje dostęp do zasobów.
Czym jest autoryzacja?
Autoryzacja następuje po udanym uwierzytelnieniu. To proces, który decyduje, do jakich zasobów użytkownik ma dostęp. W naszym przykładzie będziemy autoryzować zalogowanego użytkownika do przeglądania danych użytkowników. Jeśli użytkownik nie przejdzie procesu uwierzytelnienia, nie uzyska dostępu do tych informacji.
Doskonałym przykładem autoryzacji są platformy społecznościowe, takie jak Facebook i Twitter. Bez założonego konta dostęp do treści tych serwisów jest niemożliwy.
Innym przykładem jest model subskrypcyjny, gdzie mimo pomyślnego zalogowania do serwisu, dostęp do treści jest przyznawany dopiero po wykupieniu subskrypcji.
Wymagania wstępne
Zakładam, że posiadasz podstawową wiedzę z zakresu JavaScript, MongoDB i Node.js.
Upewnij się, że Node.js i npm są zainstalowane na Twoim komputerze. Aby to sprawdzić, wpisz `node -v` oraz `npm -v` w wierszu poleceń. Powinieneś zobaczyć wersje zainstalowanych narzędzi.
Twoje wersje mogą się różnić od moich. NPM jest automatycznie dołączany do Node.js. Jeśli nie masz jeszcze tych narzędzi, pobierz je ze strony internetowej NodeJS.
Do pisania kodu potrzebujesz IDE (zintegrowane środowisko programistyczne). W tym samouczku używam VS Code, ale możesz wybrać inne. Jeśli nie masz jeszcze IDE, pobierz je ze strony Visual Studio i zainstaluj odpowiednią wersję.
Konfiguracja projektu
Utwórz folder o nazwie `nodeapi` w wybranej lokalizacji i otwórz go w VS Code. Następnie otwórz terminal w VS Code i zainicjuj menedżera pakietów Node.js, wpisując:
npm init -y
Upewnij się, że jesteś w katalogu `nodeapi`.
Powyższe polecenie utworzy plik `package.json`, w którym znajdą się informacje o zależnościach projektu.
Teraz pobierz wszystkie wymienione wcześniej pakiety. Wpisz w terminalu:
npm install express dotenv jsonwebtoken mongoose bcryptjs
Po wykonaniu tych kroków struktura Twojego projektu powinna wyglądać jak poniżej.
Tworzenie serwera i łączenie z bazą danych
Utwórz plik `index.js` oraz folder `config`. Wewnątrz folderu `config` stwórz dwa pliki: `conn.js`, który będzie odpowiedzialny za połączenie z bazą danych oraz `config.env`, w którym zdefiniujemy zmienne środowiskowe. Wpisz poniższy kod do odpowiednich plików.
index.js
const express = require('express'); const dotenv = require('dotenv'); //Konfiguracja dotenv przed użyciem innych bibliotek i plików dotenv.config({path:'./config/config.env'}); //Tworzenie aplikacji z wykorzystaniem Express const app = express(); //Obsługa danych JSON w żądaniach app.use(express.json()); //Uruchomienie serwera app.listen(process.env.PORT,()=>{ console.log(`Serwer nasłuchuje na porcie ${process.env.PORT}`); })
Pamiętaj, aby skonfigurować `dotenv` w pliku `index.js` przed użyciem zmiennych środowiskowych w innych plikach.
conn.js
const mongoose = require('mongoose'); mongoose.connect(process.env.URI, { useNewUrlParser: true, useUnifiedTopology: true }) .then((data) => { console.log(`Połączono z bazą danych: ${data.connection.host}`) })
config.env
URI = 'mongodb+srv://ghulamrabbani883:[email protected]/?retryWrites=true&w=majority' PORT = 5000
Używam URI z MongoDB Atlas, możesz także skorzystać z localhost.
Tworzenie modeli i tras
Model reprezentuje strukturę danych przechowywanych w bazie danych MongoDB w formacie JSON. Do stworzenia modelu użyjemy schematu Mongoose.
Routing definiuje sposób, w jaki aplikacja reaguje na żądania klientów. Użyjemy funkcji routera w Express do tworzenia tras.
Metody routingu przyjmują dwa argumenty: trasę i funkcję zwrotną, która określa, co dana trasa zrobi, gdy klient wyśle żądanie. Opcjonalnie, może także przyjąć trzeci argument – funkcję oprogramowania pośredniczącego. Ponieważ budujemy API z uwierzytelnianiem, skorzystamy z funkcji oprogramowania pośredniczącego do autoryzacji użytkowników.
Stwórzmy teraz dwa foldery: `routes` i `models`. Wewnątrz folderu `routes` utwórz plik `userRoute.js`, a w folderze `models` plik `userModel.js`. Następnie dodaj poniższy kod do odpowiednich plików.
userModel.js
const mongoose = require('mongoose'); //Tworzenie schematu za pomocą Mongoose const userSchema = new mongoose.Schema({ name: { type:String, required:true, minLength:[4,'Imię musi mieć minimum 4 znaki'] }, email:{ type:String, required:true, unique:true, }, password:{ type:String, required:true, minLength:[8,'Hasło musi mieć minimum 8 znaków'] }, token:{ type:String } }) //Tworzenie modelu const userModel = mongoose.model('user',userSchema); module.exports = userModel;
userRoute.js
const express = require('express'); //Tworzenie routera Express const route = express.Router(); //Import modelu użytkownika const userModel = require('../models/userModel'); //Trasa rejestracji route.post('/register',(req,res)=>{ }) //Trasa logowania route.post('/login',(req,res)=>{ }) //Trasa pobierania danych użytkownika route.get('/user',(req,res)=>{ })
Implementacja funkcjonalności tras i tworzenie tokenów JWT
Co to jest token JWT?
Tokeny internetowe JSON (JWT) to biblioteka JavaScript, która umożliwia tworzenie i weryfikację tokenów. Jest to otwarty standard wykorzystywany do bezpiecznej wymiany informacji między dwiema stronami: klientem i serwerem. Wykorzystamy dwie funkcje JWT: pierwszą do generowania nowych tokenów, drugą do ich weryfikacji.
Co to jest bcryptjs?
Bcryptjs to funkcja hashująca stworzona przez Nielsa Provosa i Davida Mazièresa. Wykorzystuje algorytm haszujący do szyfrowania haseł. W projekcie wykorzystamy dwie najpopularniejsze funkcje: generowanie skrótów haseł oraz porównywanie haseł.
Implementacja funkcjonalności tras
Funkcja wywołania zwrotnego w routingu przyjmuje trzy argumenty: żądanie, odpowiedź i funkcję `next`. Argument `next` jest opcjonalny, dodaj go, jeśli potrzebujesz. Argumenty te powinny być w kolejności: żądanie, odpowiedź i funkcja `next`. Zmodyfikuj pliki `userRoute.js`, `config.env` i `index.js` za pomocą poniższego kodu.
userRoute.js
//Importowanie niezbędnych plików i bibliotek const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); //Tworzenie routera Express const route = express.Router(); //Import modelu użytkownika const userModel = require('../models/userModel'); //Trasa rejestracji route.post("/register", async (req, res) => { try { const { name, email, password } = req.body; //Sprawdzenie, czy wszystkie dane zostały wprowadzone if (!name || !email || !password) { return res.json({ message: 'Wprowadź wszystkie dane' }) } //Sprawdzenie, czy użytkownik już istnieje const userExist = await userModel.findOne({ email: req.body.email }); if (userExist) { return res.json({ message: 'Użytkownik o podanym adresie email już istnieje' }) } //Haszowanie hasła 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: 'Użytkownik zarejestrowany pomyślnie', data: user }) } catch (error) { return res.json({ error: error }); } }) //Trasa logowania route.post('/login', async (req, res) => { try { const { email, password } = req.body; //Sprawdzenie, czy wszystkie dane zostały wprowadzone if (!email || !password) { return res.json({ message: 'Wprowadź wszystkie dane' }) } //Sprawdzenie, czy użytkownik istnieje const userExist = await userModel.findOne({email:req.body.email}); if(!userExist){ return res.json({message:'Nieprawidłowe dane uwierzytelniające'}) } //Sprawdzenie zgodności hasła const isPasswordMatched = await bcrypt.compare(password,userExist.password); if(!isPasswordMatched){ return res.json({message:'Nieprawidłowe hasło'}); } 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:'Zalogowano pomyślnie'}) } catch (error) { return res.json({ error: error }); } }) //Trasa pobierania danych użytkownika route.get('/user', async (req, res) => { try { const user = await userModel.find(); if(!user){ return res.json({message:'Nie znaleziono użytkownika'}) } return res.json({user:user}) } catch (error) { return res.json({ error: error }); } }) module.exports = route;
Pamiętaj, by używać bloku try-catch z funkcjami asynchronicznymi, aby uniknąć nieobsłużonych odrzuceń obietnic.
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'); //Konfiguracja dotenv przed użyciem innych bibliotek i plików dotenv.config({path:'./config/config.env'}); require('./config/conn'); //Tworzenie aplikacji z wykorzystaniem Express const app = express(); const route = require('./routes/userRoute'); //Obsługa danych JSON w żądaniach app.use(express.json()); //Użycie tras app.use('/api', route); //Uruchomienie serwera app.listen(process.env.PORT,()=>{ console.log(`Serwer nasłuchuje na porcie ${process.env.PORT}`); })
Tworzenie oprogramowania pośredniczącego do uwierzytelniania użytkownika
Co to jest oprogramowanie pośredniczące?
Oprogramowanie pośredniczące to funkcja, która ma dostęp do obiektu żądania, odpowiedzi i funkcji `next` w cyklu żądanie-odpowiedź. Funkcja `next` jest wywoływana po zakończeniu wykonania funkcji pośredniczącej. Użyj `next()` w przypadku konieczności wykonania kolejnej funkcji wywołania zwrotnego lub innej funkcji pośredniczącej.
Utwórz teraz folder o nazwie `middleware`, a w nim plik `auth.js`. Wpisz do niego poniższy 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('Zaloguj się, aby uzyskać dostęp do danych'); } 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;
Zainstaluj bibliotekę `cookie-parser`, która umożliwi zarządzanie plikami cookie. `cookie-parser` umożliwia dostęp do tokena przechowywanego w pliku cookie. W przeciwnym wypadku dostęp do plików cookie z nagłówków żądania będzie niemożliwy. Wpisz w terminalu, aby pobrać `cookie-parser`:
npm i cookie-parser
Skonfiguruj aplikację, modyfikując plik `index.js` i dodaj oprogramowanie pośredniczące do trasy `”/user/”`.
plik index.js
const cookieParser = require('cookie-parser'); const express = require('express'); const dotenv = require('dotenv'); //Konfiguracja dotenv przed użyciem innych bibliotek i plików dotenv.config({path:'./config/config.env'}); require('./config/conn'); //Tworzenie aplikacji z wykorzystaniem Express const app = express(); const route = require('./routes/userRoute'); //Obsługa danych JSON w żądaniach app.use(express.json()); //Konfiguracja cookie-parser app.use(cookieParser()); //Użycie tras app.use('/api', route); //Uruchomienie serwera app.listen(process.env.PORT,()=>{ console.log(`Serwer nasłuchuje na porcie ${process.env.PORT}`); })
userRoute.js
//Importowanie niezbędnych plików i bibliotek const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const isAuthenticated = require('../middleware/auth'); //Tworzenie routera Express const route = express.Router(); //Import modelu użytkownika const userModel = require('../models/userModel'); //Trasa pobierania danych użytkownika route.get('/user', isAuthenticated, async (req, res) => { try { const user = await userModel.find(); if (!user) { return res.json({ message: 'Nie znaleziono użytkownika' }) } return res.json({ user: user }) } catch (error) { return res.json({ error: error }); } }) module.exports = route;
Trasa `”/user”` jest dostępna tylko dla zalogowanych użytkowników.
Testowanie API w POSTMAN
Przed sprawdzeniem API zmodyfikuj plik `package.json`. Dodaj następujące wiersze:
"scripts": { "test": "echo "Error: no test specified" && exit 1", "start": "node index.js", "dev": "nodemon index.js" },
Serwer uruchomisz za pomocą `npm start`, ale uruchomi się on tylko raz. Aby serwer automatycznie restartował się podczas zmian w plikach, użyjemy `nodemon`. Pobierz go, wpisując w terminalu:
npm install -g nodemon
Flaga `-g` pobierze `nodemon` globalnie, co pozwoli korzystać z niego w każdym projekcie.
Aby uruchomić serwer, wpisz `npm run dev` w terminalu. Powinieneś zobaczyć następujący wynik.
Gdy kod jest gotowy, a serwer działa poprawnie, przejdź do Postmana i przetestuj API.
Czym jest Postman?
Postman to narzędzie do projektowania, budowania, rozwijania i testowania API.
Jeśli nie masz zainstalowanego Postmana, pobierz go ze strony Postmana.
Otwórz Postmana i utwórz kolekcję `nodeAPItest`, a w niej trzy żądania: `register`, `login` i `user`. Struktura powinna wyglądać następująco.
Gdy wyślesz dane JSON do `”localhost:5000/api/register”`, otrzymasz następujący wynik.
Ponieważ token jest tworzony i zapisywany w plikach cookie podczas rejestracji, dane użytkownika można pobrać z trasy `”localhost:5000/api/user”`. Pozostałe żądania możesz sprawdzić samodzielnie.
Jeśli chcesz przejrzeć kompletny kod, znajdziesz go na moim koncie GitHub.
Podsumowanie
W tym samouczku dowiedzieliśmy się, jak zaimplementować uwierzytelnianie w API Node.js za pomocą tokenów JWT. Udzieliliśmy również autoryzacji użytkownikom, aby mogli mieć dostęp do swoich danych.
Życzę udanego kodowania!
newsblog.pl