المصادقة في Socket.io
تأمين اتصالات WebSocket أمر بالغ الأهمية لحماية البيانات الحساسة ومنع الوصول غير المصرح به. توفر Socket.io استراتيجيات مصادقة متعددة للتحقق من هوية المستخدم قبل إنشاء الاتصالات.
لماذا نصادق على اتصالات WebSocket؟
على عكس طلبات HTTP التي يمكن مصادقتها لكل طلب، اتصالات WebSocket طويلة الأمد. بمجرد إنشائها، تظل مفتوحة، مما يجعل المصادقة الأولية حاسمة:
- منع الوصول غير المصرح به إلى تدفقات البيانات في الوقت الفعلي
- ربط الاتصالات بمستخدمين محددين للأحداث المخصصة
- تطبيق قواعد التفويض للغرف ومساحات الأسماء
- تتبع نشاط المستخدم وإدارة الاتصالات لكل مستخدم
مصادقة البرمجيات الوسيطة (Middleware)
تُنفذ دوال البرمجيات الوسيطة في Socket.io قبل إنشاء الاتصال، مما يسمح لك بالتحقق من بيانات الاعتماد ورفض الاتصالات غير المصرح بها:
<!-- جانب الخادم (Node.js) -->
const io = require('socket.io')(server);
// برمجيات وسيطة للمصادقة
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('رمز المصادقة مطلوب'));
}
// التحقق من الرمز (مثال مع JWT)
try {
const decoded = verifyJWT(token);
socket.userId = decoded.userId;
socket.username = decoded.username;
next(); // السماح بالاتصال
} catch (err) {
next(new Error('رمز غير صالح'));
}
});
io.on('connection', (socket) => {
console.log(`المستخدم ${socket.username} متصل`);
});
ملاحظة: تعمل البرمجيات الوسيطة قبل حدث 'connection'، مما يمنحك الفرصة لرفض الاتصالات قبل إنشائها.
مصادقة رمز JWT
رموز الويب JSON (JWT) خيار شائع لمصادقة WebSocket لأنها عديمة الحالة ويمكن أن تحمل معلومات المستخدم:
// التحقق من JWT جانب الخادم
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET;
function verifyJWT(token) {
return jwt.verify(token, SECRET_KEY);
}
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('لم يتم توفير رمز'));
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return next(new Error('رمز غير صالح أو منتهي الصلاحية'));
}
socket.user = {
id: decoded.id,
username: decoded.username,
email: decoded.email,
role: decoded.role
};
next();
});
});
إرسال الرمز من جانب العميل
يجب على العميل إرسال بيانات اعتماد المصادقة أثناء المصافحة باستخدام خيار auth:
<!-- جانب العميل -->
<script>
// الحصول على الرمز من localStorage أو ملف تعريف الارتباط
const token = localStorage.getItem('authToken');
const socket = io('http://localhost:3000', {
auth: {
token: token
}
});
// معالجة أخطاء المصادقة
socket.on('connect_error', (error) => {
if (error.message === 'Invalid token') {
alert('فشلت المصادقة. يرجى تسجيل الدخول مرة أخرى.');
// إعادة التوجيه إلى صفحة تسجيل الدخول
window.location.href = '/login';
}
});
</script>
مصادقة قائمة على الجلسة
إذا كان تطبيقك يستخدم ملفات تعريف ارتباط الجلسة، يمكنك مصادقة اتصالات WebSocket باستخدام الجلسات الموجودة:
// جانب الخادم مع express-session
const session = require('express-session');
const sharedsession = require('express-socket.io-session');
// برمجيات وسيطة لجلسة Express
const sessionMiddleware = session({
secret: 'my-secret',
resave: false,
saveUninitialized: false,
cookie: { secure: false }
});
app.use(sessionMiddleware);
// مشاركة الجلسة مع Socket.io
io.use(sharedsession(sessionMiddleware, {
autoSave: true
}));
io.use((socket, next) => {
const session = socket.handshake.session;
if (!session || !session.userId) {
return next(new Error('غير مصادق عليه'));
}
socket.userId = session.userId;
socket.username = session.username;
next();
});
نصيحة: تعمل المصادقة القائمة على الجلسة بشكل جيد عندما تكون اتصالات WebSocket على نفس النطاق الخاص بتطبيق الويب الخاص بك، حيث يتم إرسال ملفات تعريف الارتباط تلقائيًا.
حماية مساحات الأسماء
طبق قواعد مصادقة مختلفة على مساحات أسماء مختلفة للتحكم الدقيق في الوصول:
// مساحة أسماء عامة (لا حاجة للمصادقة)
const publicIO = io.of('/public');
publicIO.on('connection', (socket) => {
console.log('اتصال عام');
});
// مساحة أسماء المسؤول (تتطلب دور المسؤول)
const adminIO = io.of('/admin');
adminIO.use((socket, next) => {
const token = socket.handshake.auth.token;
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err || decoded.role !== 'admin') {
return next(new Error('وصول المسؤول مطلوب'));
}
socket.user = decoded;
next();
});
});
adminIO.on('connection', (socket) => {
console.log(`المسؤول ${socket.user.username} متصل`);
});
قطع اتصال المستخدمين غير المصرح لهم
يمكنك قطع اتصال المستخدمين في أي وقت إذا تغيرت حالة التفويض الخاصة بهم:
io.on('connection', (socket) => {
// التحقق من أن المستخدم لا يزال مصرحًا له
socket.on('adminAction', async (data) => {
const user = await getUserFromDatabase(socket.userId);
if (user.role !== 'admin') {
socket.emit('error', { message: 'إجراء غير مصرح به' });
socket.disconnect(true); // فرض قطع الاتصال
return;
}
// معالجة إجراء المسؤول
});
});
// قطع اتصال المستخدم من جميع الأجهزة
function disconnectUser(userId) {
const sockets = await io.fetchSockets();
sockets.forEach(socket => {
if (socket.userId === userId) {
socket.emit('forceDisconnect', {
reason: 'الحساب معلق'
});
socket.disconnect(true);
}
});
}
استراتيجية تحديث الرمز
تعامل مع انتهاء صلاحية الرمز بشكل سلس من خلال تطبيق آلية تحديث:
<!-- تحديث الرمز جانب العميل -->
<script>
let socket;
function connectWithToken(token) {
socket = io('http://localhost:3000', {
auth: { token: token }
});
socket.on('connect_error', async (error) => {
if (error.message === 'Invalid or expired token') {
// محاولة تحديث الرمز
const newToken = await refreshAuthToken();
if (newToken) {
socket.auth.token = newToken;
socket.connect(); // إعادة محاولة الاتصال
} else {
// إعادة التوجيه لتسجيل الدخول
window.location.href = '/login';
}
}
});
}
async function refreshAuthToken() {
const response = await fetch('/api/refresh-token', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('authToken', data.token);
return data.token;
}
return null;
}
connectWithToken(localStorage.getItem('authToken'));
</script>
تحذير: لا تقم أبدًا بتخزين الرموز الحساسة في عناوين URL أو معاملات الاستعلام. استخدم دائمًا خيار auth أو ملفات تعريف ارتباط آمنة.
مصادقة الأجهزة المتعددة
تتبع وإدارة اتصالات متعددة من نفس المستخدم عبر أجهزة مختلفة:
// تتبع الاتصال جانب الخادم
const userConnections = new Map(); // userId => مجموعة معرفات المقابس
io.on('connection', (socket) => {
const userId = socket.user.id;
// إضافة الاتصال إلى مجموعة المستخدم
if (!userConnections.has(userId)) {
userConnections.set(userId, new Set());
}
userConnections.get(userId).add(socket.id);
console.log(`المستخدم ${userId} لديه ${userConnections.get(userId).size} اتصالات`);
socket.on('disconnect', () => {
userConnections.get(userId).delete(socket.id);
if (userConnections.get(userId).size === 0) {
userConnections.delete(userId);
console.log(`المستخدم ${userId} قطع الاتصال بالكامل`);
}
});
});
// إرسال رسالة لجميع أجهزة المستخدم
function sendToUser(userId, event, data) {
const connections = userConnections.get(userId);
if (connections) {
connections.forEach(socketId => {
io.to(socketId).emit(event, data);
});
}
}
تمرين: أنشئ خادم WebSocket مع مصادقة JWT:
- يتطلب رمز JWT صالحًا في المصافحة
- استخراج معلومات المستخدم من الرمز
- رفض الاتصالات بالرموز المنتهية الصلاحية
- تتبع عدد الأجهزة المتصلة لكل مستخدم
- السماح للمسؤولين بقطع اتصال مستخدمين محددين
اختبر مع رموز صالحة وغير صالحة للتحقق من أن المصادقة تعمل بشكل صحيح.