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.
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: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
Daha sonra 'Gönder' düğmesini tıklamanız yeterlidir; yanıt metnini gözlemlemelisiniz:
{ "message": "OK" }
İ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:
Şahsen ben kullanmayı tercih ediyorum
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.
Sahte veri knex seed:run
ile tohumlayalım ve tabloyu tekrar kontrol edelim.
Artık veritabanını yönetebilecek donanıma sahibiz ve gerektiğinde kullanıcıları eklememize, silmemize veya güncellememize olanak tanıyoruz.
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.
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 için CRUD yöntemlerini yeniden kullanacağım temel modeli kullanacağım. Daha önce model oluşturmayı başka bir konuda ele almış olsam dasrc/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.
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.
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.
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.
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:
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.
Kayıt uç noktasını kontrol edelim:
Açıkça görüldüğü gibi, oturumu geri almak için başlıkta kullanılacak bir jeton elde ettik.
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.
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.
Burada da yayınlandı