paint-brush
Knex ve Redis ile Node.js'de Kimlik Doğrulama ve Kullanıcı Akışında Ustalaşmaile@antonkalik
901 okumalar
901 okumalar

Knex ve Redis ile Node.js'de Kimlik Doğrulama ve Kullanıcı Akışında Ustalaşma

ile Anton Kalik26m2024/03/17
Read on Terminal Reader

Çok uzun; Okumak

Veritabanı yönetimi için Knex'i, verimli önbelleğe alma için Redis'i ve kusursuz yönlendirme için Express'i kullanma. Knex ve Redis'i kullanarak Node js sunucusu için sağlam bir kimlik doğrulama çözümü oluşturun ve anlayın. Node.js için bir veritabanı oluşturmak için Knex'i, ardından verileri önbelleğe almak için Redis'i ve verileri yönlendirmek için Express'i kullanın.
featured image - Knex ve Redis ile Node.js'de Kimlik Doğrulama ve Kullanıcı Akışında Ustalaşma
Anton Kalik HackerNoon profile picture
0-item

Veritabanı yönetimi için Knex'i, verimli önbelleğe alma için Redis'i ve sorunsuz yönlendirme için Express'i kullanarak Node js sunucusu için güçlü bir kimlik doğrulama çözümü oluşturun.

Node.js uygulamalarım için hızlı, sezgisel ve kolaylaştırılmış bir kimlik doğrulama çözümü arayışımda, işlevsellikten ödün vermeden hızlı uygulama gerektiren senaryolarla karşılaştım.


Kullanıcı kaydı ve oturum açmadan, unutulan şifreleri yönetmeye, kullanıcı verilerini güncellemeye ve hatta hesap silmeye kadar, bu önemli kullanıcı etkileşimlerinde sorunsuz bir şekilde gezinen kapsamlı bir çözüm aradım.


Bu nedenle, makalem tam olarak şunu sunmayı amaçlamaktadır: kimlik doğrulama ve önbelleğe alma uygulamak için net metodolojileri entegre eden, sağlam ve verimli bir kullanıcı akışı sağlayan uyumlu bir yaklaşım.


Burada temel kurulum prosedürlerini ve model oluşturmayı atlayarak doğrudan kimlik doğrulamanın ve kullanıcı akışının inceliklerine odaklanacağız. Makale boyunca yapılandırma dosyalarını edinmek için gerekli tüm bağlantıları dahil edeceğiz ve kurulum için gereken kaynaklara sorunsuz erişim sağlayacağız.

Aletler

Bu uygulama için Knex, Express ve Redis'in yanı sıra Node.js 20.11.1 sürümünden de yararlanacağız. Ek olarak, sorunsuz yönetim için Docker kullanılarak kapsayıcıya alınacak ve düzenlenecek olan PostgreSQL'i veritabanımız olarak kullanacağız.


Uygulamamızın adı user-flow-boilerplate olacaktır. Bu klasörü oluşturalım ve içinde temel package.json oluşturmak için npm init -y komutunu çalıştıralım.


 { "name": "user-flow-boilerplate", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

Başlangıç package.json


Bir sonraki adım gerekli bağımlılıkları eklemektir:


bağımlılıklar : npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator


geliştirici bağımlılıkları :

 npm i -D @babel/core @babel/eslint-parser @babel/plugin-transform-class-properties @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript @faker-js/faker @types/bcrypt @types/body-parser @types/cors @types/express @types/jest @types/jsonwebtoken @types/node @types/node-cron @types/validator @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-jest cross-env eslint eslint-config-prettier eslint-plugin-prettier jest nodemon npm-run-all prettier ts-jest ts-loader ts-node tsconfig-paths tslint typescript webpack webpack-cli webpack-node-externals


ve uygulamamızı oluşturup çalıştıracak komut dosyalarını ekleyin:

 "scripts": { "start": "NODE_ENV=production node dist/bundle.js", "build": "NODE_ENV=production webpack --config webpack.config.js", "dev": "cross-env NODE_ENV=development && npm-run-all -p dev:*", "dev:build": "webpack --config webpack.config.js --watch", "dev:start": "nodemon --watch dist --exec node dist/bundle.js", "test": "NODE_ENV=test jest --config ./jest.config.js", "lint": "eslint ./src -c .eslintrc.json" },


Uygulamamızın sorunsuz bir şekilde başlatılmasını sağlamak için, bir src klasörü oluşturmamız ve ilk giriş noktası dosyamız olan index.ts bu klasöre yerleştirmemiz önemlidir.

 require('dotenv').config(); import process from 'process'; import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 9999; app.use(bodyParser.json()); app.use(cors()); app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); (async () => { try { app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })();

Giriş noktası dosyası


Geliştirme için typscript , lint , jest , bable , prettier , nodemon ayarlarına sahip olmamız gerekiyor. Tüm bu dosyaları şu makalede anlattım: Express'te Postgres ve Knex ile Node.js Sunucusu Oluşturma .


Tüm ayarları yapılandırdıktan ve giriş noktasını oluşturduktan sonra, npm run dev çalıştırılması sunucuyu başlatmalı ve aşağıdakine benzer bir çıktı görmeyi beklemelisiniz:

 ./src/index.ts 1.7 KiB [built] [code generated] external "dotenv" 42 bytes [built] [code generated] external "process" 42 bytes [built] [code generated] external "express" 42 bytes [built] [code generated] external "body-parser" 42 bytes [built] [code generated] external "cors" 42 bytes [built] [code generated] webpack 5.90.3 compiled successfully in 751 ms [nodemon] restarting due to changes... [nodemon] starting `node dist/bundle.js` Server is running on port 9999


Daha sonra şuraya gidin: Postacı uç noktalarımızı test etmeye adanmış bir koleksiyon oluşturacağımız yer. Yeni koleksiyonda yeni bir GET isteği ekleyin, cmd + E tuşlarına basın (Mac'te ancak tuşlar işletim sisteminize bağlıdır) ve bunu health olarak adlandırın.


URL için enter ekleyin: {{BASE_URI}}/health . BASE_URI için koleksiyonda kullanacağınız yeni bir değişken ekleyin: http://localhost:9999/api/v1

Postacı Temel URL'yi Ayarladı

Daha sonra 'Gönder' düğmesini tıklamanız yeterlidir; yanıt metnini gözlemlemelisiniz:

 { "message": "OK" }


Veri tabanı

İlerlemeden önce veritabanımızın çalışır durumda olması çok önemlidir. Bunu docker-compose ile başlatarak başaracağız. Veritabanına erişmek ve yönetmek için aşağıdaki gibi çeşitli geliştirme platformlarını kullanabilirsiniz: pgAdmin .


Şahsen ben kullanmayı tercih ediyorum Yakut Madeni Etkin yönetim için PostgreSQL veritabanlarına kusursuz bağlantı sağlayan bir sürücüyle donatılmıştır.


Gerekli anahtarları, şifreleri ve test adlarını içeren .env dosyasına ihtiyacımız var:

 PORT=9999 WEB_HOST="localhost" # DB DB_HOST="localhost" DB_PORT=5432 DB_NAME="user_flow_boilerplate" DB_USER="username_123" DB_PASSWORD="SomeParole999" # User DEFAULT_PASSWORD="SomeParole999" JWT_SECRET="6f1d7e9b9ba56476ae2f4bdebf667d88eeee6e6c98c68f392ed39f7cf6e51c5a" # Test User TEST_EMAIL="[email protected]" TEST_USERNAME="test_username" TEST_PASSWORD="SomeParole999" # Redis REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD="SomeParole999"

Veritabanına bağlantı için .env, Redis ve tohumlara ilişkin test değerleri


Korkmayın, daha özgün bir şekilde göstermek için JWT_SECRET rastgele oluşturdum. Öyleyse projenin kökünde bir docker-compose.yml dosyası oluşturalım:

 version: '3.6' volumes: data: services: database: build: context: . dockerfile: postgres.dockerfile image: postgres:latest container_name: postgres environment: TZ: Europe/Madrid POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} networks: - default volumes: - data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped redis: image: redis:latest container_name: redis command: redis-server --requirepass ${REDIS_PASSWORD} networks: - default ports: - "6379:6379" restart: unless-stopped

hizmetleri içeren docker-compose dosyası


Hızlı bağlantı için Docker'da iki hizmeti başlatacağız. Veritabanına veya Redis'e hızlı erişimi kolaylaştırmak ve verileri verimli bir şekilde almamızı sağlamak için bu süreci kolaylaştırdım. Öyleyse, hadi bu hizmetleri docker-compose up çalıştıralım ve aşağıdaki çıktıyı docker ps sonra görebilmemiz gerekiyor:

 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e4bef95de1dd postgres:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp postgres 365e3a68351a redis:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:6379->6379/tcp redis


Şimdi uygulama için türlerimizi sakladığımız src/@types/index.ts dosyasını oluşturmamız gerekiyor:

 export enum Role { Admin = 'admin', Blogger = 'blogger', } export type UserSession = { id: number; }; export type DatabaseDate = { created_at: Date; updated_at: Date; }; export type DefaultUserData = { role: Role; }; export interface User extends DatabaseDate { id: number; email: string; username: string; password: string; role: Role; }

Hizmet türleri


Şu anda bağlantı, geçişler ve tohumlar için projenin kökünde ve veritabanı klasöründe knexfile.ts bulunması gerekir.


Kullanıcıların bu env değişkenlerini kullandığımız veritabanına nasıl taşınacağı ve tohumlanacağı hakkında Postgres ve Knex ile Node.js Sunucusu Oluşturma makalesinde oldukça ayrıntılı bir açıklama bıraktım.


Aynı sayfada olduğumuzdan emin olmak için geçişleri özellikle kontrol etmek istiyorum. Hizmetlerimizi zaten başlattık ve veritabanı bağlantısını kontrol edebilmemiz gerekiyor.

 docker exec -it postgres psql -U username_123 user_flow_boilerplate


Bağlantı iyiyse psql konsolunda olacaksınız. Tamam, eğer bağlantıda sorun yoksa tablolarımızı oraya taşıyabiliriz. knex migrate:latest çalıştırın. Daha sonra veritabanı içerisindeki users tablonuza yeni eklenen sütunları gözlemlemelisiniz.

Geçişten sonra kullanıcılar tablosu

Sahte veri knex seed:run ile tohumlayalım ve tabloyu tekrar kontrol edelim.

Tohumdan sonra veritabanındaki sonuç

Artık veritabanını yönetebilecek donanıma sahibiz ve gerektiğinde kullanıcıları eklememize, silmemize veya güncellememize olanak tanıyoruz.

Yönlendirici

Son olarak ayarları ve hazırlığı unutup özellikle kullanıcı akışına odaklanabiliriz. Bunun için bir yönlendirici oluşturmamız gerekiyor. Bu yönlendirici tarafından şu işlemleri gerçekleştirmemiz gerekir: login , oturum logout , signup , delete_user , update_user .


Bunun için src/routes/index.ts dosyasına aşağıdaki kodu ekleyin:

 import { Router } from 'express'; import { authRouter } from 'src/routes/authRouter'; import { healthController } from 'src/controllers/healthController'; import { sessionController } from 'src/controllers/sessionController'; import { authMiddleware } from 'src/middlewares/authMiddleware'; import { userRouter } from 'src/routes/userRouter'; export const router = Router({ mergeParams: true }); router.get('/health', healthController); router.use('/auth', authRouter); router.get('/session', authMiddleware, sessionController); router.use('/user', authMiddleware, userRouter); router.use((_, res) => { return res.status(404).json({ message: 'Not Found' }); });

Rota dosyası


Gördüğünüz gibi başlangıçta daha önce kontrol ettiğimiz /health rotasını ekledik. O halde, o rotaları orada uygulamak için giriş noktasını güncelleyelim. İlk önce önceki get kaldırın.

 -> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' }));


ve dosyanın en üstüne şunu ekleyin:

 import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router);


ve şu kodla health kontrolü src/controllers/healthController.ts için ilk denetleyiciyi oluşturun:

 import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok');

Sağlık Kontrolörü


Şimdi yönlendiriciye geri dönelim ve rotalara daha fazla ne eklememiz gerektiğine bakalım. İki dosya daha eklememiz gerekiyor: authRouter.ts ve userRouter.ts

 import { Router } from 'express'; import { signUpController } from 'src/controllers/auth/signUpController'; import { loginController } from 'src/controllers/auth/loginController'; export const authRouter = Router(); authRouter.post('/signup', signUpController); authRouter.post('/login', loginController);

Kimlik Doğrulama Yönlendiricisi


 import { Router } from 'express'; import { updateUserController } from 'src/controllers/user/updateUserController'; import { deleteUserController } from 'src/controllers/user/deleteUserController'; import { logoutController } from 'src/controllers/user/logoutController'; import { updatePasswordController } from 'src/controllers/user/updatePasswordController'; export const userRouter = Router(); userRouter.patch('/', updateUserController); userRouter.delete('/', deleteUserController); userRouter.post('/logout', logoutController); userRouter.post('/update-password', updatePasswordController);

Kullanıcı Yönlendiricisi


Okunabilirlik ve yalıtılmış işlevselliği sürdürme sorumluluğu açısından bu mantığı böldüm. Bu rotaların tümü, mantığı yöneteceğimiz kontrolörlere ihtiyaç duyar.


Kontrolörler


Kimlik doğrulama ve sistem durumu rotaları, kimlik doğrulama ara yazılımına ihtiyaç duymaz, dolayısıyla bu rotalar korunmaz, ancak eşleşme yoksa durum 404'ü alırız.

 router.get('/health', healthController); router.use('/auth', authRouter);

Artık tüm rotaları belirlediğimize göre kullanıcı modelini ayarlamamız gerekiyor.

Kullanıcı Modeli

Kullanıcı modeli için CRUD yöntemlerini yeniden kullanacağım temel modeli kullanacağım. Daha önce model oluşturmayı başka bir konuda ele almış olsam da madde , Daha iyi görünürlük ve anlayış için temel modeli buraya ekleyeceğim. src/models/Model.ts oluşturun

 import { database } from 'root/database'; export abstract class Model { protected static tableName?: string; protected static get table() { if (!this.tableName) { throw new Error('The table name must be defined for the model.'); } return database(this.tableName); } public static async insert<Payload>(data: Payload): Promise<{ id: number; }> { const [result] = await this.table.insert(data).returning('id'); return result; } public static async updateOneById<Payload>( id: number, data: Payload ): Promise<{ id: number; }> { const [result] = await this.table.where({ id }).update(data).returning('id'); return result; } public static async delete(id: number): Promise<number> { return this.table.where({ id }).del(); } public static async findOneById<Result>(id: number): Promise<Result> { return this.table.where('id', id).first(); } public static async findOneBy<Payload, Result>(data: Payload): Promise<Result> { return this.table.where(data as string).first(); } }

Temel model


Temel modelle aynı klasörde UserModel.ts oluşturabilmemiz gerekiyor:

 import { Model } from 'src/models/Model'; import { Role, User, DefaultUserData } from 'src/@types'; export class UserModel extends Model { static tableName = 'users'; public static async create<Payload>(data: Payload) { return super.insert<Payload & DefaultUserData>({ ...data, role: data.role || Role.Blogger, }); } public static findByEmail(email: string): Promise<User | null> { return this.findOneBy< { email: string; }, User >({ email }); } public static findByUsername(username: string): Promise<User | null> { return this.findOneBy< { username: string; }, User >({ username }); } }

Kullanıcı Modeli


Kullanıcı modelinde, yükten sağlanmadığı takdirde role yalnızca varsayılan olarak ayarlıyorum. Artık modellerimiz hazır olduğuna göre bunları denetleyicilerimiz ve ara katman yazılımlarımızda kullanmaya devam edebiliriz.

Kimlik Doğrulama Ara Yazılımı

Node.js uygulamasındaki kimlik doğrulama ara yazılımı, gelen isteklerin kimliğini doğrulamaktan ve bu isteklerin geçerli ve yetkili kullanıcılardan geldiğinden emin olmaktan sorumludur.


Genellikle gelen istekleri yakalar, kimlik doğrulama belirteçlerini veya kimlik bilgilerini çıkarır ve bu durumda bunların geçerliliğini JWT (JSON Web Belirteçleri) gibi önceden tanımlanmış bir kimlik doğrulama mekanizmasına göre doğrular.


Kimlik doğrulama işlemi başarılı olursa, ara yazılım, isteğin istek-yanıt döngüsündeki bir sonraki işleyiciye ilerlemesine izin verir. Ancak kimlik doğrulama başarısız olursa, uygun bir HTTP durum koduyla (örn. 401 Yetkisiz) yanıt verir ve isteğe bağlı olarak bir hata mesajı verir.


src/middlewares klasörünü oluşturun ve buraya aşağıdaki kodu içeren authMiddleware.ts dosyasını ekleyin:

 import { jwt } from 'src/utils/jwt'; import { Redis } from 'src/redis'; import type { Request, Response, NextFunction } from 'express'; import type { UserSession } from 'src/@types'; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; const JWT_SECRET = process.env.JWT_SECRET; if (!token) return res.sendStatus(401); if (!JWT_SECRET) { console.error('JWT_SECRET Not Found'); return res.sendStatus(500); } if (!token) return res.status(401).json({ error: 'Token not provided' }); try { const userSession = await jwt.verify<UserSession>(token); if (!userSession) { return res.sendStatus(401); } const storedToken = await Redis.getSession(userSession.id); if (!storedToken || storedToken !== token) { return res.sendStatus(401); } req.user = userSession; next(); } catch (error) { console.error('JWT_ERROR', error); return res.sendStatus(401); } }

Kimlik doğrulama ara yazılım dosyası


Kimlik doğrulama ara yazılımı, JWT belirtecini istek başlığından çıkarır, JWT kitaplığını kullanarak geçerliliğini doğrular ve belirtecin Redis'te depolanan belirteçle eşleşip eşleşmediğini kontrol eder.


Belirteç geçerliyse ve depolanan belirteçle eşleşiyorsa, ara yazılım, kimlik doğrulaması yapılmış kullanıcı oturumunu istek nesnesinde ( req.user ) ayarlar ve kontrolü bir sonraki ara yazılıma veya rota işleyicisine geçirmek için next() işlevini çağırır. Aksi takdirde, kimlik doğrulama başarısızlığını belirten 401 durum koduyla yanıt verir.

JSON Web Belirteçleri

Jwt'nin kullanımını gözden geçirelim. Aşağıdaki kodla src/utils/jwt.ts dosyasında oluşturun:

 require('dotenv').config(); import jsonwebtoken from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET as string; export const jwt = { verify: <Result>(token: string): Promise<Result> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => { if (error) { reject(error); } else { resolve(decoded as Result); } }); }); }, sign: (payload: string | object | Buffer): Promise<string> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { try { resolve(jsonwebtoken.sign(payload, JWT_SECRET)); } catch (error) { reject(error); } }); }, };

JWT yardımcı program dosyası


Bu yardımcı program, Node.js uygulaması içindeki JSON Web Belirteçlerinin işlenmesinde kritik bir rol oynar. jwt nesnesi, jsonwebtoken kitaplığından yararlanarak JWT'leri imzalamak ve doğrulamak için işlevleri dışa aktarır. Bu işlevler, uygulamada kimlik doğrulama mekanizmalarının uygulanması için gerekli olan JWT'lerin oluşturulmasını ve doğrulanmasını kolaylaştırır.


Yardımcı program, JWT'leri işlemeye yönelik işlevselliği kapsar ve Node.js uygulaması içinde güvenli kimlik doğrulama mekanizmaları sağlarken ortam değişkeni yönetimine yönelik en iyi uygulamalara bağlı kalır.

Redis

Veritabanı, önbellek ve mesaj aracısı olarak kullanılır. Önbelleğe alma, oturum yönetimi, gerçek zamanlı analizler, mesajlaşma kuyrukları, skor tabloları ve daha fazlasını içeren çeşitli kullanım durumlarında yaygın olarak kullanılır.


Jetonun Redis'ten kontrol edilmesi, JWT jetonu için ek bir güvenlik ve doğrulama katmanı görevi görür. Ayarlara dalalım. Bunun için src/redis/index.ts dosyasını aşağıdaki kodla oluşturun:

 require('dotenv').config({ path: '../../.env', }); import process from 'process'; import * as redis from 'redis'; const client = redis.createClient({ url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, }); client.on('error', error => console.error('Redis Client Error', error)); const connect = async () => { try { await client.connect(); console.log('Connected to Redis'); } catch (err) { console.error(`Could not connect to Redis: ${err}`); process.exit(1); } }; class Redis { public static setSession(userId: number, token: string) { if (!userId) throw new Error('userId is required'); if (!token) throw new Error('token is required'); try { return client.set(`session:${userId}`, token); } catch (error) { console.error(error); } } public static getSession(userId: number) { if (!userId) throw new Error('userId is required'); return client.get(`session:${userId}`); } public static deleteSession(userId: string) { if (!userId) throw new Error('userId is required'); try { return client.del(`session:${userId}`); } catch (error) { console.error(error); } } } export { client, connect, Redis };

Redis Oturum Mağazası


Redis ile kullanıcı oturumu belirteçlerini saklayıp yöneteceğiz. Kimlik doğrulama ara yazılımında, JWT belirtecinin orijinalliği doğrulandıktan sonra ara yazılım, belirtecin mevcut olup olmadığını ve karşılık gelen kullanıcı oturumu için Redis'te depolanan belirteçle eşleşip eşleşmediğini kontrol eder. Bu, yalnızca geçerli ve yetkili kullanıcıların korumalı rotalara erişebilmesini sağlamaya yardımcı olur.


Redis, kullanıcı oturumu belirteçlerini korumak için anahtar/değer deposu olarak kullanılır. Bir kullanıcı oturum açtığında veya kimlik doğrulaması yaptığında oturum belirteci Redis'te depolanır. Bu, sonraki kimlik doğrulama kontrolleri sırasında oturum belirteçlerinin verimli ve hızlı bir şekilde alınmasına olanak tanır.


Redis, verimli oturum yönetimi için kimlik doğrulama ara yazılımında kullanılırken Redis ile ilgili dosya, yapılandırmayı ve Redis sunucusuna bağlantıyı yönetir ve uygulamanın diğer bölümlerinde Redis ile etkileşim kurmaya yönelik işlevler sağlar.


Bu kurulum, Redis'te saklanan ve yönetilen kullanıcı oturumu belirteçleriyle güvenli ve güvenilir kimlik doğrulama mekanizmaları sağlar.


Son kısım ise giriş noktamızda Redis'e bağlanmamız gerektiği:

 // all imports import * as Redis from 'src/redis'; const app = express(); const PORT = process.env.PORT || 9999; // middlewares (async () => { try { await Redis.connect(); app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })();

Redis'e bağlanın


Kimlik doğrulama hazırlığını tamamladıktan sonra artık odağımızı denetleyicilere kaydırabiliriz.

Kontrolörler

Rotalardaki denetleyiciler, endişeleri ayırarak ve kodun sürdürülebilirliğini teşvik ederek uygulamanın mantığını düzenlemeye yardımcı olur. Durum kontrolü için denetleyiciyi zaten oluşturduk. Daha sonra, kullanıcıyla işlemleri yürütmek için denetleyiciler oluşturmaya devam edeceğiz.


Alacağımız ilk denetleyici, aşağıdaki kodla src/controllers içinde olması gereken sessionController.ts :

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const user = await UserModel.findOneById<User>(req.user.id); if (user) { return res.status(200).json(user); } else { return res.sendStatus(401); } } catch (error) { return res.sendStatus(500); } };

Oturum Denetleyicisi


Bu denetleyici, muhtemelen şu anda kimliği doğrulanmış kullanıcı hakkında bilgi almaktan sorumlu olan, oturumla ilgili bir uç noktayı işleme amacına hizmet eder. Bu denetleyiciye aşağıdaki nedenlerden dolayı ihtiyacımız var:


Kullanıcı Oturumu Bilgileri: Bu denetleyici, uygulamanın kullanıcı profili veya diğer ilgili veriler gibi kullanıcının oturumu hakkındaki bilgileri almasına olanak tanır. Bu bilgiler, kullanıcı deneyimini özelleştirmek veya kullanıcının profiline göre kişiselleştirilmiş içerik sağlamak için yararlı olabilir.


Kimlik Doğrulama ve Yetkilendirme: req.user mevcut olup olmadığını kontrol eden denetleyici, yalnızca kimliği doğrulanmış kullanıcıların uç noktaya erişebilmesini sağlar. Bu, kimlik doğrulama ve yetkilendirme kurallarının uygulanmasına yardımcı olarak hassas kullanıcı verilerinin yalnızca yetkili kullanıcılar tarafından erişilebilir olmasını sağlar.


Kullanıcı Profilinin Alınması: Denetleyici, oturum kimliklerine göre kullanıcının bilgilerini almak için veritabanını ( UserModel kullanarak) sorgular. Bu, uygulamanın kullanıcıya özel verileri dinamik olarak almasına olanak tanıyarak her kullanıcıya özel bir deneyim sunar. Bu bölüm kesinlikle Redis önbelleğiyle geliştirilebilir:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const cachedProfile = await Redis.getSession(req.user.id); if (cachedProfile) { return res.status(200).json(JSON.parse(cachedProfile)); } else { const user = await UserModel.findOneById<User>(req.user.id); if (user) { await Redis.setSession(req.user.id, JSON.stringify(user), CACHE_EXPIRATION); return res.status(200).json(user); } else { return res.sendStatus(401); } } } catch (error) { console.error('Error retrieving user profile:', error); return res.sendStatus(500); } };

Redis'in ayarlanmış oturumunu içeren Oturum Denetleyicisi dosyası


Önbellek son kullanma süresini saniye cinsinden belirtmek için bir CACHE_EXPIRATION sabiti tanımlarız. Bu örnekte 3600 saniyeye (1 saat) ayarlanmıştır. Önbelleğe alınan veriler düzenli aralıklarla yenilenerek eski verilerin kullanıcılara sunulması önlenir ve önbellekteki veri bütünlüğü korunur.


Uygulamamızda yeni kullanıcıların kayıt olma sürecini signUpController oluşturmaya geçmeden önce şemayı inceleyelim:

Kayıt İşlem Şeması

Bizim durumumuzda, veritabanında mevcut bir e-postayla kaydolmaya çalışırken, kullanıcının var olup olmadığını açıkça açıklamayarak kullanıcı gizliliğine öncelik veriyoruz. Bunun yerine, müşteriyi Invalid email or password belirten genel bir mesajla bilgilendiririz.


Bu yaklaşım, istemciyi mevcut kullanıcılar hakkındaki gereksiz bilgileri ifşa etmeden geçerli kimlik bilgileri göndermeye teşvik eder.


Şimdi src/controllers/auth/signUpController.ts oluşturalım ve aşağıdaki kodu ekleyelim:

 import bcrypt from 'bcrypt'; import { jwt } from 'src/utils/jwt'; import { Request, Response } from 'express'; import { validate } from 'src/helpers/validation/validate'; import { userSchema } from 'src/helpers/validation/schemas/userSchema'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; import { getRandomString } from 'src/utils/getRandomString'; type Payload = Omit<User, 'id' | 'created_at' | 'updated_at' | 'role'>; export async function signUpController(req: Request, res: Response) { const { email, password }: Payload = req.body; const validation = validate<Payload>(req.body, userSchema); if (!validation.isValid) { return res.status(400).send(`Invalid ${validation.invalidKey}`); } try { const user = await UserModel.findOneBy({ email }); if (user) { return res.status(400).json({ message: 'Invalid email or password' }); } const hashedPassword = (await bcrypt.hash(password, 10)) as string; const username = `${email.split('@')[0]}${getRandomString(5)}`; const createdUser = await UserModel.create<Payload>({ email, password: hashedPassword, username, }); const token = await jwt.sign({ id: createdUser.id, }); await Redis.setSession(createdUser.id, token); res.status(200).json({ token, }); } catch (error) { return res.sendStatus(500); } }

Denetleyiciye Kaydolun


Denetleyici, genellikle bir kayıt formundan kullanıcının e-postasını ve şifresini içeren bir talep alır. Gerekli formatı karşıladığından emin olmak için gelen verileri önceden tanımlanmış bir userSchema göre doğrular.


Doğrulama başarılı bir şekilde geçerse, mevcut bir kullanıcı ve geçerli alan bulunmadığını belirtirse, denetleyici bcrypt.hash kullanarak parolayı karma işlemine tabi tutar, bir username oluşturur ve UserModel.create kullanarak kullanıcıyı oluşturur.


Son olarak jwt kullanarak bir token oluşturur, session verilerini Redis ayarlar ve token kullanıcıya geri gönderir.


Şimdi bir oturum açma denetleyicisinin oluşturulmasına odaklanalım. src/controllers/auth/loginController.ts dosyasını oluşturun:


 require('dotenv').config({ path: '../../.env', }); import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { jwt } from 'src/utils/jwt'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export async function loginController(req: Request, res: Response) { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: 'Invalid email or password' }); } try { const user = await UserModel.findByEmail(email); if (user) { const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: 'Invalid email or password' }); } const token: string = await jwt.sign({ id: user.id, }); await Redis.setSession(user.id, token); res.status(200).json({ token }); } else { return res.status(400).json({ message: 'Invalid email or password' }); } } catch (error) { console.error(error); return res.sendStatus(500); } }

Oturum Açma Denetleyicisi


Temel olarak, sağlanan alanları doğrulayarak ve ardından bir kullanıcının varlığını kontrol ederek başlıyoruz. Hiçbir kullanıcı bulunamazsa, signupController davranışa benzer şekilde Invalid email or password mesajıyla birlikte 400 durum koduyla yanıt veririz.


Bir kullanıcı varsa, sağlanan parolayı bcrypt.compare kullanarak veritabanında saklanan karma parolayla karşılaştırmaya devam ederiz.


Şifreler eşleşmiyorsa tanıdık 'Geçersiz e-posta veya şifre' mesajıyla yanıt veririz. Son olarak, başarılı kimlik doğrulamanın ardından bir token oluşturuyoruz, Redis'te oturumu ayarlıyoruz ve tokenı istemciye geri gönderiyoruz.


Ara katman yazılımından elde edilen user_id'nin varlığına bağlı olan korumalı denetleyicilerimizi gözden geçirelim. Bu denetleyiciler içindeki işlemler için sürekli olarak bu user_id'ye güveniyoruz. İsteğin authorization başlığının eksik olduğu durumlarda 401 durum koduyla yanıt vermemiz gerekir.


 const authHeader = req.headers['authorization'];


Aşağıdaki kodla src/controllers/user/logoutController.ts dosyasını oluşturun:


 import type { Request, Response } from 'express'; import { Redis } from 'src/redis'; export async function logoutController(req: Request, res: Response) { try { await Redis.deleteSession(req.user.id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } }

Oturum Kapatma Denetleyicisi


Bu logoutController , bir kullanıcının sistemden çıkış yapmasından sorumludur. Bir istek alındığında, user.id ile ilişkili oturumu silmek için Redis istemcisiyle etkileşime girer. İşlem başarılı olursa, başarılı oturum kapatmayı belirtmek için 200 durum koduyla yanıt verir.


Ancak işlem sırasında bir hata meydana gelirse, dahili sunucu hatası sinyali vermek için 500 durum koduyla yanıt verir.


Şimdi kullanıcı verilerinin silinmesi konusunu ele alalım.


src/controllers/user/deleteUserController.ts oluşturun ve şu kodu ekleyin:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export const deleteUserController = async (req: Request, res: Response) => { const user_id = req.user.id; try { await Redis.deleteSession(user_id); await UserModel.delete(user_id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };

Kullanıcı denetleyicisini sil


Bir istek alındığında, genellikle kimlik doğrulama ara yazılımından elde edilen istek nesnesinden kullanıcı kimliğini çıkarır.


Daha sonra Redis istemcisini kullanarak bu user_id ile ilişkili oturumu Redis'ten silmeye devam eder. Daha sonra kullanıcının verilerini veritabanından kaldırmak için UserModel delete yöntemini çağırır.


Hem oturumun hem de kullanıcı verilerinin başarıyla silinmesi üzerine, başarılı silme işlemini belirtmek için 200 durum koduyla yanıt verir. Silme işlemi sırasında bir hata oluşması durumunda, dahili sunucu hatasını belirtmek için 500 durum koduyla yanıt verir.


Sistemdeki kullanıcı verilerini güncellemek için src/controllers/user/updateUserController.ts oluşturun ve dosyaya aşağıdaki kodu ekleyin:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { filterObject } from 'src/utils/filterObject'; type Payload = { first_name?: string; last_name?: string; username?: string; }; export const updateUserController = async (req: Request, res: Response) => { const { first_name, last_name, username } = req.body; const payload: Payload = filterObject({ first_name, last_name, username, }); try { const existingUserName = await UserModel.findByUsername(username); if (existingUserName) { return res.status(400).json({ error: 'Invalid username', }); } const updatedUser = await UserModel.updateOneById<typeof payload>(req.user.id, payload); res.status(200).json(updatedUser); } catch (error) { res.sendStatus(500); } };

Kullanıcı Denetleyicisini Güncelle


Bir istek alındığında, istek gövdesinden first_name , last_name ve username alanlarını çıkarır. Daha sonra, veriye yalnızca geçerli alanların dahil edildiğinden emin olmak için filterObject yardımcı program işlevini kullanarak bu alanları filtreler.


Daha sonra, sağlanan username veritabanında zaten mevcut olup olmadığını kontrol eder. Böyle bir durumda denetleyici, 400 durum koduyla ve geçersiz username belirten bir hata mesajıyla yanıt verir. username benzersizse denetleyici, UserModel updateOneById yöntemini kullanarak veritabanındaki kullanıcı verilerini güncellemeye devam eder.


Başarılı güncelleme sonrasında 200 durum kodu ve güncellenmiş kullanıcı verileriyle yanıt verir. Güncelleme işlemi sırasında herhangi bir hata olması durumunda denetleyici, dahili sunucu hatasını belirtmek için 500 durum koduyla yanıt verir.


Sonuncusu, kullanıcı verilerinin güncellenmesiyle hemen hemen aynı fikir olan, ancak yeni parolanın hashlenmesiyle parolayı güncellemek olacaktır. src/controllers/user/updatePasswordController.ts listemizden son denetleyiciyi oluşturun ve kodu ekleyin:


 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import bcrypt from 'bcrypt'; export const updatePasswordController = async (req: Request, res: Response) => { try { const { password } = req.body; if (!password) return res.sendStatus(400); const hashedPassword = (await bcrypt.hash(password, 10)) as string; const user = await UserModel.updateOneById(req.user.id, { password: hashedPassword }); return res.status(200).json({ id: user.id }); } catch (error) { return res.sendStatus(500); } };

Şifre denetleyicisini güncelle


Bir istek alındığında, istek gövdesinden yeni parolayı çıkarır. Daha sonra istek gövdesinde bir parola sağlanıp sağlanmadığını kontrol eder. Değilse, hatalı bir isteği belirten 400 durum koduyla yanıt verir. Daha sonra, bcrypt kütüphanesini kullanarak 10 tuz faktörüyle yeni şifreyi hashler.


Karma hale getirilmiş parola daha sonra UserModel updateOneById yöntemi kullanılarak user.id ile ilişkilendirilerek veritabanında güvenli bir şekilde saklanır. Başarılı parola güncellemesi üzerine denetleyici, 200 durum koduyla ve kullanıcının kimliğini içeren bir JSON nesnesiyle yanıt verir.


Şifre güncelleme işlemi sırasında herhangi bir hata oluşması durumunda kontrol cihazı diğer kontrol cihazlarında olduğu gibi dahili sunucu hatasını belirtmek için 500 durum koduyla yanıt verir.


Doğrulama yardımcısını ve yardımcı programları gözden geçirip kurduğunuzdan emin olun. GitHub deposu . Yapılandırıldıktan sonra uç noktaları test etmeye hazır olmalısınız.


Kayıt uç noktasını kontrol edelim:


Kimlik Doğrulama Kaydı


Açıkça görüldüğü gibi, oturumu geri almak için başlıkta kullanılacak bir jeton elde ettik.


Yanıt olarak oturumun sonucu


Başlıktaki yetkilendirme jetonunu sunucuya gönderdik ve yanıt olarak sunucu bize veritabanından alınan kullanıcı verilerini sağladı.


Güvenlik özelliklerini ve Redis önbelleğe almayı keşfetmekten ve denemekten çekinmeyin. Temel model uygulandığında, şifrelerini unutan kullanıcılar için hesap kurtarma gibi ek işlevlere girebilirsiniz. Ancak bu konu daha sonraki bir yazıya bırakılacaktır.

Çözüm

Yönlendirme ve kullanıcı kimlik doğrulama akışını ölçeklenebilir bir şekilde yönetmek zor olabilir. Rotaları korumak için ara katman yazılımı uygularken, hizmetin performansını ve güvenilirliğini artırmak için ek stratejiler de mevcuttur.


Hata yönetimi, daha kapsamlı kapsam gerektiren önemli bir husus olmaya devam ettiğinden, daha net hata mesajları sağlanarak daha da geliştirilmiş kullanıcı deneyimi sağlanır. Ancak, kullanıcıların kaydolmasına, hesaplarına erişmesine, oturum verilerini almasına, kullanıcı bilgilerini güncellemesine ve hesapları silmesine olanak tanıyarak birincil kimlik doğrulama akışını başarıyla uyguladık.


Umarım bu yolculuğu anlayışlı bulmuşsunuzdur ve kullanıcı kimlik doğrulaması konusunda değerli bilgiler edinmişsinizdir.

Kaynaklar

GitHub Deposu
Knex.js
İfade etmek
Düğüm Uygulaması Oluştur
Postacı


Burada da yayınlandı