أمان قواعد البيانات وربطها بالتطبيقات
من يرى ماذا؟ وكيف نحمي القاعدة من الحذف والتسريب والحقن؟ ⏱ نحو 25 دقيقة.
- تعدّد أشهر تهديدات قواعد البيانات وحماية كل منها.
- تقرأ أوامر المستخدمين والصلاحيات في
MySQL: CREATE USERوGRANTوREVOKE. - تنشئ عرضًا
Viewيخفي الأعمدة الحساسة. - تشرح حقن
SQLوعلاجه، وطرق ربط القاعدة بالتطبيقات.
١لماذا نحمي قاعدة البيانات؟
قاعدة البيانات أثمن ما في النظام: أرقام جوالات العملاء، ودرجات الطلاب، والمبالغ المالية. وأشهر ما يهددها:
| التهديد | مثال | الحماية |
|---|---|---|
| الوصول غير المصرح | موظف يطّلع على رواتب زملائه. | حسابات وصلاحيات لكل مستخدم. |
| حقن SQL SQL Injection | مهاجم يكتب أمر SQL في خانة البحث بالموقع. | الاستعلامات ذات المعاملات. |
| فقدان البيانات | تعطل القرص، أو حذف بالخطأ. | نسخ احتياطي منتظم. |
| التسريب | سرقة ملف القاعدة أو النسخة الاحتياطية. | تشفير البيانات وكلمات المرور. |
٢المستخدمون والصلاحيات
في MySQL لكل شخص أو تطبيق حساب (مصادقة Authentication)، ولكل حساب صلاحيات محددة (تفويض Authorization). وتُدار بأوامر عائلة DCL:
-- إنشاء حساب للكاشيرCREATE USER 'cashier'@'localhost' IDENTIFIED BY 'Strong#Pass2025'; -- يقرأ المنتجات فقطGRANT SELECT ON store.products TO 'cashier'@'localhost'; -- ويضيف الطلباتGRANT INSERT ON store.orders TO 'cashier'@'localhost'; -- عرض صلاحياته، ثم سحب واحدةSHOW GRANTS FOR 'cashier'@'localhost';REVOKE INSERT ON store.orders FROM 'cashier'@'localhost';
هذه الأوامر خاصة بخادم MySQL، ولا تعمل في SQLite لأنه ملف واحد بلا مستخدمين. نفّذها في المعمل.
٣العرض View: نافذة على جزء من البيانات
أحيانًا نريد أن يرى قسمٌ بعض الأعمدة فقط. العرض View استعلام محفوظ باسم، يُستخدم كأنه جدول. هنا قسم التسويق يرى أسماء العملاء ومدنهم، دون أرقام الجوالات:
CREATE VIEW customer_cities ASSELECT name, cityFROM customers; SELECT * FROM customer_cities;
| name | city |
|---|---|
| سعود العتيبي | المدينة المنورة |
| نورة القحطاني | الرياض |
| خالد الحربي | المدينة المنورة |
| ريم الشهري | جدة |
| فهد الدوسري | الرياض |
| سارة المطيري | الدمام |
| عبدالله الزهراني | جدة |
| هند السبيعي | المدينة المنورة |
| ماجد الغامدي | تبوك |
| لمى العنزي | NULL |
في MySQL نمنح التسويق صلاحية على العرض وحده: GRANT SELECT ON store.customer_cities TO 'marketing'، فلا يصل إلى جدول العملاء الأصلي.
٤حقن SQL: أخطر ثغرة
تخيّل موقعًا يبحث عن عميل باسمه، ويبني الاستعلام بلصق ما كتبه الزائر. زائر عادي يكتب «سعود العتيبي». لكن مهاجمًا يكتب في الخانة:
x' OR '1'='1
فيصبح الاستعلام الذي يصل القاعدة:
SELECT name, phone FROM customers WHERE name = 'x' OR '1'='1';| name | phone |
|---|---|
| سعود العتيبي | 0551000001 |
| نورة القحطاني | 0551000002 |
| خالد الحربي | 0551000003 |
| ريم الشهري | 0551000004 |
| فهد الدوسري | 0551000005 |
| سارة المطيري | 0551000006 |
| عبدالله الزهراني | NULL |
| هند السبيعي | 0551000008 |
| ماجد الغامدي | 0551000009 |
| لمى العنزي | 0551000010 |
الشرط '1'='1' صحيح دائمًا، فعرضت القاعدة أرقام جوالات كل العملاء! ويمكن بالطريقة نفسها حذف جداول كاملة.
والعلاج: لا تلصق مدخلات المستخدم داخل نص الاستعلام. استخدم الاستعلام ذا المعاملات Parameterized Query: الاستعلام ثابت، والقيمة تُرسل منفصلة فلا تُفهم أبدًا على أنها أوامر:
# ✗ خطير: نلصق ما كتبه المستخدم داخل الاستعلامname = input("اسم العميل: ")sql = "SELECT * FROM customers WHERE name = '" + name + "'"cursor.execute(sql)
# ✓ آمن: الاستعلام ثابت، والقيمة تُمرَّر منفصلةname = input("اسم العميل: ")sql = "SELECT * FROM customers WHERE name = %s"cursor.execute(sql, (name,))
٥النسخ الاحتياطي
النسخة الاحتياطية Backup ملف يحفظ القاعدة كلها لتستعيدها عند الكارثة. في MySQL تُنشأ بأداة mysqldump من سطر الأوامر:
-- (تُكتب في سطر الأوامر لا في محرر SQL)mysqldump -u root -p store > store_backup.sql -- نسخmysql -u root -p store < store_backup.sql -- استعادة
- جدولة النسخ يوميًّا، وحفظ النسخ في مكان آخر غير الخادم نفسه.
- تجربة الاستعادة دوريًّا: نسخة لم تُجرَّب استعادتها قد لا تعمل وقت الحاجة.
٦تكامل القاعدة مع التطبيقات
القاعدة لا تعمل وحدها: الموقع وتطبيق الجوال ونظام المحاسبة يتصلون بها. يتصل التطبيق بالخادم بعنوانه، واسم مستخدم خاص به، ثم يرسل الاستعلامات ويستقبل النتائج. هذا تطبيق بايثون يتصل بـ MySQL:
import mysql.connector db = mysql.connector.connect( host="localhost", user="shop_app", # مستخدم خاص بالتطبيق، لا root password="********", database="store")cursor = db.cursor()cursor.execute("SELECT name, price FROM products WHERE stock > %s", (0,))for name, price in cursor.fetchall(): print(name, price)
| طريقة التكامل | متى تُستخدم؟ |
|---|---|
اتصال مباشر (مكتبة مثل mysql-connector أو PDO في PHP) | التطبيق الخلفي للموقع يقرأ ويكتب مباشرة. |
| واجهة برمجية API | تطبيق الجوال لا يتصل بالقاعدة؛ يطلب من خادم وسيط يتصل هو بها. أكثر أمانًا. |
| تصدير واستيراد CSV أو JSON | نقل البيانات إلى نظام آخر، أو إلى Excel للتحليل. |
ستبني اتصالًا كهذا بنفسك في مقرر «تقنيات تطوير الواجهات الخلفية».
توقّع وتحقّق
الأمر الذي يمنح صلاحية في MySQL:
DCL.أهم علاج لحقن SQL:
مبدأ أقل الصلاحيات يعني:
Least Privilege.العرض View هو:
الخلاصة
- لكل مستخدم حساب وصلاحيات، ومبدأ أقل الصلاحيات هو الأساس.
GRANTيمنح وREVOKEيسحب، ولا تشغّل التطبيق بحسابroot.Viewاستعلام محفوظ يعرض جزءًا من البيانات.- حقن
SQLيحدث بلصق مدخلات المستخدم؛ وعلاجه الاستعلام ذو المعاملات. - نسخ احتياطي منتظم في مكان آخر، وتجربة الاستعادة.
تحدٍّ برمجي: عرض آمن لقسم الشحن
متوسطقسم الشحن يحتاج اسم العميل ومدينته ورقم الطلب وحالته، ولا يجوز أن يرى أرقام الجوالات. أنشئ عرضًا لهم.
- أنشئ عرضًا اسمه
shipping_viewمن جدول الطلباتordersوجدول العملاء. - أعمدته: رقم الطلب، واسم العميل، ومدينته، وحالة الطلب، بهذا الترتيب.
- لا تُدخل رقم الجوال.
👀 اعرض النتيجة المطلوبة
| id | name | city | status |
|---|---|---|---|
| 1 | سعود العتيبي | المدينة المنورة | مُسلَّم |
| 2 | نورة القحطاني | الرياض | مُسلَّم |
| 3 | سعود العتيبي | المدينة المنورة | مُسلَّم |
| 4 | خالد الحربي | المدينة المنورة | مُسلَّم |
| 5 | ريم الشهري | جدة | ملغى |
| 6 | فهد الدوسري | الرياض | مُسلَّم |
| 7 | نورة القحطاني | الرياض | قيد الشحن |
| 8 | سارة المطيري | الدمام | قيد الشحن |
| 9 | هند السبيعي | المدينة المنورة | جديد |
| 10 | سعود العتيبي | المدينة المنورة | جديد |
| 11 | ماجد الغامدي | تبوك | جديد |
| 12 | خالد الحربي | المدينة المنورة | جديد |
- الكود يعمل بلا أخطاء
- العرض يعرض الأعمدة الأربعة المطلوبة لكل طلب
- لا يظهر رقم الجوال
💡 تلميح 1
💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
CREATE VIEW shipping_view ASSELECT orders.id, customers.name, customers.city, orders.statusFROM ordersJOIN customers ON customers.id = orders.customer_id; SELECT * FROM shipping_view;