Node.js: المصادقة والأمان
آخر تحديث: 2026-08-26
1. القصة: واجهة برمجة تطبيقات غير محمية
أمضت أليس أسبوعين في نشر واجهة برمجة التطبيقات (API) الخاصة بإدارة المستخدمين، لتكتشف في اليوم التالي أن قاعدة البيانات قد حُذفت — حيث لم تكن واجهة برمجة التطبيقات الخاصة بها مزودة بنظام مصادقة، مما سمح لأي شخص باستدعائها مباشرةً DELETE /api/users. وبعد أن تعلمت الدرس، قامت بتنفيذ مصادقة JWT: حيث يتعين على المستخدمين أولاً تسجيل الدخول للحصول على رمز مميز (token)، ويجب أن تتضمن الطلبات اللاحقة هذا الرمز للوصول إلى المسارات المحمية. وفي الوقت نفسه، أضافت برمجيات وسيطة قائمة على الأدوار لعمليات الإدارة، مما يضمن عدم تمكن حتى المستخدمين الذين يمتلكون رمزًا من تنفيذ إجراءات خطيرة. وبعد أسبوع، استخدمت Helmet لإضافة إجراءات أمنية إلى رؤوس الاستجابة، وأصبحت واجهة برمجة التطبيقات (API) أخيرًا آمنة كالحصن.
2. المفاهيم الأساسية
(1) الفرق بين المصادقة والتفويض
- المصادقة: التحقق من «هويتك»، مثل التحقق من اسم المستخدم وكلمة المرور
- التفويض: يتحقق من «ما يمكنك القيام به»، مثل تحديد الأذونات القائمة على الأدوار
- غالبًا ما يُستخدم هذان الإجراءان معًا: أولاً، يتم التحقق من الهوية؛ ثم، يتم التصريح بالعملية.
(2) نظرة عامة على مبادئ JWT
- JWT (JSON Web Token) هو رمز مميز عديم الحالة يتألف من ثلاثة أجزاء مشفرة بترميز Base64
- بمجرد إصداره من قِبل الخادم، لا داعي لتخزينه؛ حيث يقوم العميل بحفظه وإدراجه في الطلب.
- مناسب للأنظمة الموزعة، مما يتيح تجنب مشاكل مشاركة الجلسات
(3) تجزئة كلمات المرور باستخدام bcrypt
- bcrypt هي خوارزمية تجزئة مصممة خصيصًا لكلمات المرور، وتحتوي على «سولت» مدمج
- يمكنك ضبط مدة الحساب باستخدام
cost factorلمقاومة هجمات القوة الغاشمة. - يُنتج النص العادي نفسه نتيجة تجزئة مختلفة في كل مرة، مما يجعله أكثر أمانًا بكثير من MD5/SHA
3. التفاصيل الفنية
(1) هيكل ومبادئ عمل JWT
يتكون JWT من ثلاثة أجزاء مرتبطة ببعضها بواسطة .:
Header.Payload.Signature
| القسم | المحتوى | الوصف |
|---|---|---|
| العنوان | { "alg": "HS256", "typ": "JWT" } |
الخوارزميات وهياكل البيانات |
| الحمولة | { "userId": 1, "role": "admin", "exp": ... } |
إقرار مخصص + إقرار قياسي |
| التوقيع | HMACSHA256(base64(header) + "." + base64(payload), secret) |
توقيع مقاوم للتلاعب |
مسار العمل:
- يقوم المستخدم بتسجيل الدخول؛ ويقوم الخادم بالتحقق من بيانات الاعتماد
- إذا نجحت عملية المصادقة، فقم بإصدار JWT وإرساله إلى العميل
- يقوم العميل بتخزين الرمز المميز في localStorage أو في ملف تعريف ارتباط
- تتضمن الطلبات اللاحقة العناصر التالية في رأس
Authorization: Bearer <token>: - تقوم البرمجيات الوسيطة من جانب الخادم بالتحقق من التوقيع وتاريخ انتهاء الصلاحية
▶ مثال: إصدار JWT والتحقق منه
const jwt = require('jsonwebtoken');
const SECRET = 'my_super_secret_key';
const token = jwt.sign(
{ userId: 42, role: 'admin' },
SECRET,
{ expiresIn: '2h' }
);
console.log('Token:', token);
const decoded = jwt.verify(token, SECRET);
console.log('Decoded:', decoded);
▶ مثال: فك تشفير JWT لعرض الحمولة (دون التحقق من التوقيع)
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
(2) معلمات bcrypt وكيفية استخدامها
| المعلمة | القيمة الموصى بها | الوصف |
|---|---|---|
| saltRounds | 10-12 | عدد الجولات؛ كلما زاد العدد، زادت الأمان ولكن زادت البطء |
| طول كلمة المرور | ≥8 أحرف | يتم التحقق من صحتها من خلال واجهة المستخدم |
| الخوارزميات | Blowfish | الخوارزميات الأساسية لـ bcrypt |
▶ مثال: تجزئة كلمة المرور والتحقق منها
const bcrypt = require('bcrypt');
async function hashPassword(plainPassword) {
const saltRounds = 10;
const hash = await bcrypt.hash(plainPassword, saltRounds);
console.log('Hash:', hash);
return hash;
}
async function verifyPassword(plainPassword, hash) {
const match = await bcrypt.compare(plainPassword, hash);
console.log('Match:', match);
return match;
}
(async () => {
const hash = await hashPassword('MyPassword123');
await verifyPassword('MyPassword123', hash);
await verifyPassword('WrongPassword', hash);
})();
(3) رؤوس أمان HTTP و Helmet
يعزز Helmet الأمان من خلال تعيين رؤوس استجابة HTTP:
| عنوان الأمان | الغرض | ممكّن افتراضيًا |
|---|---|---|
| سياسة أمان المحتوى | تمنع هجمات XSS وتقيد المصادر التي يمكن تحميل الموارد منها | لا (تتطلب تهيئة يدوية) |
| X-Frame-Options | منع اختطاف النقرات | نعم |
| X-Content-Type-Options | منع استكشاف نوع MIME | نعم |
| الأمان الصارم للنقل | فرض استخدام HTTPS | نعم |
| X-XSS-Protection | تصفية XSS في المتصفح | مهملة (لم يعد Helmet يقوم بتمكين هذه الميزة افتراضيًا) |
▶ مثال: دمج Helmet
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
}
}));
app.get('/', (req, res) => {
res.send('Secured with helmet!');
});
app.listen(3000);
(4) التهديدات الأمنية الشائعة والتدابير المضادة
| التهديد | الآلية | الإجراءات المضادة |
|---|---|---|
| حقن SQL | ربط مدخلات المستخدم لإنشاء جملة SQL ضارة | الاستعلامات المعلمة / ORM |
| XSS (التنفيذ البرمجي عبر المواقع) | إدخال نصوص برمجية ضارة في الصفحات | التهرب من الإخراج / CSP / Helmet |
| CSRF (تزوير الطلبات عبر المواقع) | إرسال الطلبات مع انتحال صفة مستخدم تمت مصادقته | رمز CSRF / ملف تعريف الارتباط SameSite |
| هجمات القوة الغاشمة | محاولات تسجيل الدخول المتكررة | تقييد معدل المحاولات / عدد جولات bcrypt المرتفع / حظر الحساب |
| هجمات «الرجل في الوسط» | اعتراض بيانات الاتصالات | HTTPS / HSTS |
▶ مثال: منع هجمات حقن SQL (استعلامات معلمة)
const { Pool } = require('pg');
const pool = new Pool();
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const result = await pool.query(
'SELECT * FROM users WHERE username = $1',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id, role: user.role }, SECRET, { expiresIn: '2h' });
res.json({ token });
});
▶ مثال: منع هجمات XSS (تشفير المخرجات)
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const userInput = '<script>alert("xss")</script>';
console.log(escapeHtml(userInput));
▶ مثال: الحماية من CSRF (برنامج الوسيط csurf)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
res.json({ message: 'Form submitted successfully' });
});
4. تمارين عملية
(1) واجهة برمجة التطبيقات (API) للتسجيل
▶ مثال: واجهة برمجة تطبيقات (API) لتسجيل المستخدمين
app.post('/api/register', async (req, res) => {
const { username, password, role } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
const existingUser = await pool.query(
'SELECT id FROM users WHERE username = $1',
[username]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({ error: 'Username already exists' });
}
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);
const result = await pool.query(
'INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3) RETURNING id, username, role',
[username, passwordHash, role || 'user']
);
const user = result.rows[0];
const token = jwt.sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '2h' }
);
res.status(201).json({ user: { id: user.id, username: user.username, role: user.role }, token });
});
(2) واجهة برمجة تطبيقات تسجيل الدخول
▶ مثال: واجهة برمجة تطبيقات تسجيل دخول المستخدم
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
const result = await pool.query(
'SELECT id, username, password_hash, role FROM users WHERE username = $1',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
SECRET,
{ expiresIn: '2h' }
);
res.json({
user: { id: user.id, username: user.username, role: user.role },
token
});
});
(3) البرمجيات الوسيطة لمصادقة JWT
▶ مثال: البرمجيات الوسيطة authenticate
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
(4) البرمجيات الوسيطة للتحكم في الوصول القائم على الأدوار
▶ مثال: برنامج وسيط لدور "authorize"
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
await pool.query('DELETE FROM users WHERE id = $1', [req.params.id]);
res.json({ message: 'User deleted' });
});
app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
res.json({ message: 'Welcome to admin dashboard' });
});
5. مثال شامل: نظام مصادقة كامل
project/
├── server.js
├── middleware/
│ ├── auth.js
│ └── role.js
├── routes/
│ ├── auth.js
│ └── users.js
└── パッケージ.json
middleware/auth.js:
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET || 'fallback_dev_secret';
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired. Please login again.' });
}
return res.status(403).json({ error: 'Invalid token.' });
}
}
module.exports = { authenticate, SECRET };
البرمجيات الوسيطة/role.js:
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required.' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden. Insufficient permissions.' });
}
next();
};
}
module.exports = { authorize };
routes/auth.js:
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { authenticate, SECRET } = require('../middleware/auth');
const router = express.Router();
const users = [];
router.post('/register', غير متزامن (req, res) => {
const { username, password, role } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required.' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters.' });
}
const exists = users.find(u => u.username === username);
if (exists) {
return res.status(409).json({ error: 'Username already exists.' });
}
const saltRounds = 10;
const passwordHash = انتظار bcrypt.hash(password, saltRounds);
const newUser = {
id: users.length + 1,
username,
passwordHash,
role: role || 'user'
};
users.push(newUser);
const token = jwt.sign(
{ userId: newUser.id, username: newUser.username, role: newUser.role },
SECRET,
{ expiresIn: '2h' }
);
res.status(201).json({
user: { id: newUser.id, username: newUser.username, role: newUser.role },
token
});
});
router.post('/login', غير متزامن (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required.' });
}
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials.' });
}
const match = انتظار bcrypt.compare(password, user.passwordHash);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials.' });
}
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
SECRET,
{ expiresIn: '2h' }
);
res.json({
user: { id: user.id, username: user.username, role: user.role },
token
});
});
router.get('/profile', authenticate, (req, res) => {
const user = users.find(u => u.id === req.user.userId);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
res.json({ id: user.id, username: user.username, role: user.role });
});
module.exports = router;
routes/users.js:
const express = require('express');
const { authenticate } = require('../middleware/auth');
const { authorize } = require('../middleware/role');
const router = express.Router();
const users = [];
router.get('/', authenticate, authorize('admin'), (req, res) => {
const safeList = users.map(u => ({ id: u.id, username: u.username, role: u.role }));
res.json(safeList);
});
router.delete('/:id', authenticate, authorize('admin'), (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found.' });
}
users.splice(index, 1);
res.json({ message: 'User deleted.' });
});
module.exports = router;
server.js:
const express = require('express');
const helmet = require('helmet');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const app = express();
app.use(helmet());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error.' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
عملية الاختبار:
# Register
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"SecurePass123","role":"admin"}'
# Log In
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"SecurePass123"}'
# Accessing Protected Routes
curl http://localhost:3000/api/auth/profile \
-H "Authorization: Bearer <your_token>"
# Administrator Actions
curl -X DELETE http://localhost:3000/api/users/2 \
-H "Authorization: Bearer <admin_token>"
# Registration Response
{
"user": { "id": 1, "username": "alice", "role": "admin" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Login Response
{
"user": { "id": 1, "username": "alice", "role": "admin" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Insufficient Permissions Response
{ "error": "Forbidden. Insufficient permissions." }
6. مخطط تدفق عملية المصادقة باستخدام JWT
sequenceDiagram
participant Client as Client
participant Server as Server-side
participant DB as Database
Client->>Server: POST /api/login {username, password}
Server->>DB: Query User Records
DB-->>Server: Return User Data
Server->>Server: bcrypt.compare() Confirm Password
alt The password is correct
Server->>Server: jwt.sign() Issued Token
Server-->>Client: Back { token }
Client->>Client: Storage Token
Client->>Server: GET /api/profile<br/>Authorization: Bearer <token>
Server->>Server: jwt.verify() Verification Token
alt Token Valid
Server-->>Client: 200 Return User Data
else Token Invalid or Expired
Server-->>Client: 401/403 Access Denied
end
else Incorrect password
Server-->>Client: 401 Invalid credentials
end
7. المقارنات والمراجع
(1) JWT مقابل الجلسة مقابل OAuth
| البعد | JWT | الجلسة | OAuth 2.0 |
|---|---|---|---|
| موقع التخزين | العميل | الخادم | الخادم + العميل |
| بلا جنسية | نعم | لا | لا |
| قابلية التوسع | يدعم الأنظمة الموزعة بشكل أصلي | يتطلب تخزينًا مشتركًا للجلسات | يتطلب خادم ترخيص |
| حالات الاستخدام | مصادقة واجهة برمجة التطبيقات (API)، الخدمات الصغيرة | تطبيقات الويب التقليدية | تسجيل الدخول عبر طرف ثالث |
| المخاطر الأمنية | تسرب الرموز التي لا يمكن إلغاؤها | اختطاف الجلسة | هجمات إعادة التشغيل |
| التعقيد | منخفض | منخفض | مرتفع |
(2) اختيار معلمات bcrypt
| جولات salt | المدة التقريبية | حالات الاستخدام |
|---|---|---|
| 8 | ~40 مللي ثانية | التطوير والاختبار |
| 10 | ~160 مللي ثانية | يُوصى به لبيئات الإنتاج |
| 12 | ~640 مللي ثانية | متطلبات أمان عالية |
| 14 | ~2.5 ثانية | سيناريوهات السلامة القصوى |
(3) التهديدات الأمنية الشائعة والتدابير المضادة
| التهديد | مثال على الهجوم | طريقة الدفاع | الأدوات/المكتبات |
|---|---|---|---|
| حقن SQL | ' OR 1=1 -- |
الاستعلامات المعلمة | pg/mysql2 |
| XSS | <script>document.cookie</script> |
Escape + CSP | helmet/xss |
| CSRF | تزوير إرسال النماذج | رمز CSRF + SameSite | csurf |
| هجوم القوة الغاشمة | هجوم القاموس على كلمات المرور | تحديد معدل المحاولات + bcrypt ذو عدد جولات مرتفع | express-rate-limit |
| هجمات «الرجل في الوسط» | استنشاق حركة مرور HTTP | HTTPS + HSTS | helmet/Let's Encrypt |
(4) الحقول الشائعة في حمولة JWT
| الحقل | الاسم الكامل | الوصف |
|---|---|---|
| iss | الجهة المُصدرة | معرّف الجهة المُصدرة |
| sub | الموضوع | الموضوع (عادةً ما يكون معرّف المستخدم) |
| aud | الجمهور | معرّف المستلم |
| exp | تاريخ انتهاء الصلاحية | وقت انتهاء الصلاحية (طابع زمني بنظام يونكس) |
| iat | مكان الإصدار | تاريخ الإصدار |
| jti | معرّف JWT | المعرّف الفريد (مضاد لإعادة التشغيل) |
| userId | مخصص | معرّف المستخدم التجاري |
| الدور | مخصص | دور المستخدم |
❓ أسئلة شائعة
httpOnly لمنع هجمات XSS من قراءته؛ أما localStorage فهو وسيلة مريحة لكنه عرضة للسرقة عبر هجمات XSS، لذا يجب استخدامه بالاقتران مع CSP.Content-Security-Policy-Report-Only أثناء مرحلة التطوير لمراقبة تقارير الانتهاكات وتشديد السياسة تدريجيًّا.📖 ملخص
- المقال: المفاهيم الأساسية لواجهات برمجة التطبيقات (API) غير المحمية وكيفية استخدامها
- المفاهيم الأساسية: التعريفات والتطبيقات
- المفاهيم الأساسية وطرق الاستخدام في الشروحات الفنية
- المفاهيم الأساسية واستخدامات التمارين العملية
- مثال شامل: المفاهيم الأساسية واستخدامات نظام مصادقة متكامل
- المفاهيم الأساسية واستخدام مخطط تدفق المصادقة باستخدام JWT
- المفاهيم الأساسية واستخدامات المقارنة والإشارة
📝 تمارين
- قم بتنفيذ واجهة التسجيل. يجب ألا يقل طول كلمة المرور عن 8 أحرف وأن تحتوي على رقم واحد على الأقل. ويجب تشفيرها باستخدام bcrypt وتخزينها في قاعدة البيانات.
- تنفيذ واجهة برمجة التطبيقات (API) الخاصة بتسجيل الدخول لإصدار توقيع JWT صالح لمدة ساعة واحدة بعد التحقق من كلمة المرور؛ ويجب أن تتضمن الحمولة
userIdوrole. - كتابة البرنامج الوسيط
authenticateلاستخراج JWT منAuthorization: Bearer <token>والتحقق من صحته - اكتب برنامج الوسيط
authorize('admin')لتقييد الوصول إلىDELETE /api/users/:idعلى المستخدمين الذين يحملون دور «admin» فقط. - دمج Helmet في المشروع، وتهيئة CSP بحيث لا يُسمح بتحميل سوى البرامج النصية وأنماط التصميم من نفس المصدر، واستخدام curl للتحقق من التغييرات في رؤوس الاستجابة