التعامل مع الاستمارات

الاستمارة (form) هي عنصر أساسي يتيح للمستخدمين التفاعل مع تطبيقات الويب. لا يقدم إطار فلاسك - ضمنياً - أي شيء ليساعدنا في التعامل مع الاستمارات، ولكن يوجد في بحر إضافات فلاسك الغني، كالمُعتاد، إضافة Flask-WTF التي توفر لنا إمكانيّة استخدام حزمة WTForms الشهيرة في تطبيقاتنا. تجعل هذه الحزمة تعريف الاستمارات والتعامل مع بياناتها سهلاً وبسيطاً.

إضافة Flask-WTF

أول ما سنقوم به مع هذه الإضافة (بعد تثبيتها طبعاً) هو استخدامها لتعريف استمارة بسيطة في الحزمة myapp.forms.

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class EmailPasswordForm(Form):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
ملاحظة
وفَرَت إضافة Flask-WTF أغلفتها (wrappers) الخاصة، لحين قدوم الإصدار الثاني، للتعامل مع حقول إدخال (fields) ومصادقات (validators) حزمة WTForms. وبالتالي، قد تجد في الكثير من البرمجيات أنَّه يتم استدعاء TextField، و PasswordField وغيرها من مكتبة flask_wtforms بدلاً من wtforms.

بدءً من الإصدار 0.9 ينبغي علينا استيراد كل تلك الأشياء من حزمة wtforms مباشرةً.

الاستمارة التي عرفناها هي لتسجيل دخول المستخدمين. كان بإمكاننا تسميتها SigninForm، ولكن من خلال الابتعاد عن إطلاق تسميات محددة سيكون بإمكاننا استخدام نفس الصنف في أشياء أخرى، كاستخدامه في استمارة إنشاء الحساب مثلاً. سيودي بنا تعريف أصناف استمارات لأهداف محددة إلى إنشاء العديد من الأصناف المتشابهة الغير ضروريّة. من الأفضل تسميّة صنوف الاستمارات بناءً على محتواها، فهذا ما يميّزُها عن غيرها. بالرغم من ذلك، سنحتاج في بعض الأحيان إلى إنشاء أصناف محددة الغرض، وحينها لن يكون اختيار اسم يصف وظيفتها مشكلة.

ستقوم استمارة تسجيل الدخول بعدة أمور عند عملها. حيث ستقوم بحماية تطبيقنا من ثغرات تزوير الطلبات (CSRF)، وتنقيح مدخلات المستخدم بناءً على القالب الذي حددناه (فمثلاً البريد الإلكتروني يجب أن يكون له شكل محدد وكذلك رقم الهاتف).

التحقق من هجمات تزوير الطلبات والوقاية منها

يعد المصطلح CSRF اختصاراً لـ "Cross Site Request Forgery" (ثغرة تزوير الطلبات). تنطوي عمليّة الهجوم على قيام طرف ثالث بتزوير طلب (كإرسال بيانات استمارة) وإرساله لخادم التطبيق. يعتقد الخادم المُصاب بهذه الثغرة أنَّ ذلك الطلب - المزوَّر - قادم من استمارة مُستضافة عليه، فيتصرف وكأنَّه طلب عادي.

إليك هذا المثال الذي يوضِّح خطورة الهجمة: لنقل أنَّك مُسجَّل لدى مزود خدمة بريد إلكتروني، وهذا المزود يتيح لك حذف حسابك عبر استمارة بسيطة. تُرسِل تلك الاستمارة طلباً من النوع POST إلى الدالة account_delete التي تحذف الحساب الذي كان مُسجلاً عند إرسال بيانات الاستمارة. الآن دعنا نُنشئ استمارةً جديدة، ولتكن هذه الاستمارة موجودة على خادم آخر غير ذاك. ستُرسِل هذه الاستمارة، أيضاً، طلباً من النوع POST إلى الدالة account_delete الموجودة على استمارة التطبيق الأصلي. ماذا لو أقنعنا الآن شخصاً بالدخول إلى استمارتنا وضغط زر الإرسال (أو القيام بذلك تلقائياً عند الدخول إلى الصفحة باستخدام الجافاسكربت)؟ سيُحذَف حسابه المُسجَّل (إذا كانت لديه جلسة في المُتصفِح) عند مزود البريد الإلكتروني ذاك. كان يمكن تفادي ذلك لو كان موقع مزود تلك الخدمة أذكى ويستطيع التمييز بين الطلبات القادمة من الاستمارات الموجودة لديه أو الطلبات القادمة من مواقع أخرى.

كيف يمكننا إذاً إيقاف ذلك الوثوق الأعمى لتطبيقاتنا بجميع الطلبات من النوع POST؟ تجعل إضافة WTForms ذلك مُمكناً عبر توليد رمز مميز (token) عند تصيير (rendering) الاستمارات. يُرسَل ذلك الرمز إلى الخادم مع بيانات الطلب ويتم التحقق منه قبل قبول تلك البيانات. الفكرة الأساسيّة لهذا الرمز أنَّه يُضاف إلى متغيّر في جلسة المُستخدِم وتنتهي صلاحيته بعد مقدار مُعين من الوقت (بعد ثلاثين دقيقة افتراضياً). وبهذه الطريقة الوحيد من يستطيع إرسال الاستمارة هو مَنْ فَتَحَها (أو شخص ما على نفس الحاسب)، ولديه أيضاً مهلة ثلاثين دقيقة (أو أي وقت تحدده أنت) لإرسال البيانات بعد فتح الصفحة.

ملاحظة

علينا بدايةً إنشاء دالة صفحة التسجيل قبل استخدام إضافة Flask-WTF للوقاية من ثغرة تزوير الطلبات:

# ourapp/views.py

from flask import render_template, redirect, url_for

from . import app
from .forms import EmailPasswordForm

@app.route('/login', methods=["GET", "POST"])
def login():
    form = EmailPasswordForm()
    if form.validate_on_submit():

        # كود التحقق من كلمة مرور واسم المستخدم
        # [...]

        return redirect(url_for('index'))
    return render_template('login.html', form=form)

قمنا بدايةً باستيراد الحزمة forms واستهلالها في الدالة. ومن ثم قمنا بتشغيل الدالة form.validate_on_submit. تُرجِع هذه الدالة القيمة True إن كانت بيانات الاستمارة قد أُرسِلَت (بواسطة طريقة نقل البيانات POST أو PUT مثلاً) وتم التحقق منها من جميع المُصادِقات التي عرفناها في الوحدة forms.py.

ملاحظة

ستقوم الدالة عندما ترى أنَّ بيانات الاستمارة لم تُرسَل بعد (أي أنَّ الدالة اُستُدعيت بطلب GET) بتمرير الكائن form إلى القالب login.html وتصييره. يوجد أدناه الشكل الذي ستبدو عليه شيفرة القالب عند استخدام ميزة الحماية من ثغرة تزوير الطلبات.

{# ourapp/templates/login.html #}

{% extends "layout.html" %}
<html>
    <head>
        <title>Login Page</title>
    </head>
    <body>
        <form action="{{ url_for('login') }}" method="post">
            <input type="text" name="email" />
            <input type="password" name="password" />
            {{ form.csrf_token }}
        </form>
    </body>
</html>

تُنشئ الشيفرة {{ form.csrf_token }} حقلاً مخفياً يحتوي على ذلك الرمز الفريد لتتحقق الإضافة منه عند إرسال بيانات الاستمارة. هذا كل ما علينا القيام به وبدون إجراء أي عمليات تحققيّة أخرى.

حماية طلبات الأجاكس من ثغرة تزوير الطلبات

لا تقتصر أهميّة الرموز الفريدة التي تولّدها الإضافة على التحقق من الاستمارات وحسب. فمن الممكن أن تكون هناك أنواع أخرى من الطلبات في التطبيق يُمكِن تزويرها (مثل طلبات الأجاكس)، لذلك عليك استخدام هذه الخاصيّة في تلك الطلبات أيضاً.

ملاحظة
يحتوي توثيق إضافة Flask-WTF على قسم يتكلّم عن استخدام رموز الوقاية من ثغرة تزوير الطلبات في طلبات الأجاكس.

إنشاء مُصادِق مخصص

تتيح لنا الإضافة إمكانيّة تعريف مُصادِقات مخصصة بنا إلى جانب تلك التي توفرها افتراضياً (مثل المُصادِق ()Required و ()Email). سأشرح كيفيّة القيام بذلك عبر صنع المُصادِق ()Unique الذي يقوم بالتحقق من قاعدة البيانات والتأكّد من أنَّ القيمة التي أدخلها المُستخدِم في الحقل الفلاني غير موجودة. يُمكِن استخدام مُصادِق كهذا للتأكد من انَّ البريد الإلكتروني واسم المُستخدم المدخلان في الحقول فريدان. كنّا سنضطر إلى القيام بذلك في دالة عرض الصفحة لو أنّنا لا نستخدم إضافة WTForms، ولكن بما أنَّ الإضافة توفر لنا هذه الخواص المُيسِّرة فأصبح بإمكاننا عزل الوظيفتين وإنشاء مُصادِق مُنفصِل ومُخصَّص لذلك.

دعنا بدايةً نقوم بتعريف استمارة تسجيل بسيطة لغرضنا التوضيحي:

# ourapp/forms.py

from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class EmailPasswordForm(Form):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])

الآن سنقوم بتعريف المُصادِق الذي سيتأكّد أنَّ البريد الإلكتروني فريد وغير موجود في قاعدة البيانات. سأضع المُصادِق في وحدة جديدة في الدليل util بالاسم validators.

# ourapp/util/validators.py
from wtforms.validators import ValidationError

class Unique(object):
    def __init__(self, model, field, message=u'This element already exists.'):
        self.model = model
        self.field = field

    def __call__(self, form, field):
        check = self.model.query.filter(self.field == field.data).first()
        if check:
            raise ValidationError(self.message)

يفترض المُصادق أننا نستخدم إضافة SQLAlchemy في تعريفنا لنماذج قاعدة البيانات. تتوقع إضافة WTForms أن يُعيد المُصادِق نوعاً من الكائنات القابلة للاستدعاء (كالصنوف القابلة للاستدعاء (callable class) على سبيل المثال).

سنقوم بتحديد المُعطيات التي ينبغي تمريرها إلى المُصادِق في الدالة \_\_init\_\_. في حالتنا سنرغب بتمرير النموذج (والذي هو النموذج User) والحقل المُراد التحقق منه. سيُصدِر المُصادق الخطأ ValidationError عندما يُستدعى ويجد أنَّ هناك قيمة مطابقة لبيانات الاستمارة في قاعدة البيانات. قمنا أيضاً بإضافة رسالة افتراضيّة لتُضاف إلى رسالة الخطأ ValidationError بواسطة المُعامِل message.

الآن أصبح بإمكاننا تعديل الصنف EmailPasswordForm لنستخدم فيه المُصادِق Unique الذي أنشأناه.

ملاحظة
لا يتعيّن على المُصادِق الذي صنعناه أن يكون صنفاً قابلاً للاستدعاء. حيث يُمكِن أن يُرجِع كائِناً قابلاً للاستدعاء أو أن يُستدعى مباشرةً. يحتوي توثيق إضافة WTForms بعض الأمثلة حول ذلك.

تصيير الاستمارات

توفِر إضافة WTForms خاصيّة مُفيدة أخرى لتساعدنا على تصيير شيفرة الاستمارات. يُستخدَم الصنف Field الموجود في الإضافة لتصيير شيفرة لغة ترميز النص الفائق (HTML) المُمثِّلة لحقل ما، وبالتالي جلّ ما علينا القيام به هو استدعاء الحقول التي نرغب بها من داخل القالب لتصييرها ديناميكياً. الكيفيّة مماثلة لِمَ كنّا نقوم به عند تصيير حقل الرمز السري بواسطة الشيفرة csrf_token. انظر أدناه إلى المثال الذي يوضِّح كيفيّة استخدام الحقول المبنيّة في إضافة WTForms لإنشاء قالب بسيط لتسجيل الدخول.

{# ourapp/templates/login.html #}

{% extends "layout.html" %}
<html>
    <head>
        <title>Login Page</title>
    </head>
    <body>
        <form action="" method="post">
            {{ form.email }}
            {{ form.password }}
            {{ form.csrf_token }}
        </form>
    </body>
</html>

يمكننا تخصيص مظهر الحقول عبر تمرير خصائص الحقل كمُعطيات عند الاستدعاء (انظر أدناه).

<form action="" method="post">
    {{ form.email.label }}: {{ form.email(placeholder='yourname@email.com') }}
    <br>
    {{ form.password.label }}: {{ form.password }}
    <br>
    {{ form.csrf_token }}
</form>
ملاحظة
استخدم ''=_class إذا أردت استخدام الخاصيّة "class" في أحد الحقول، حيث أنَّ class كلمة محجوزة في بايثون.
ملاحظة
يحتوي توثيق إضافة WTForms على قائمة بالخواص المتوافرة التي يمكن تمريرها للحقول.
ملاحظة
قد تكون لاحظتَ أنَّه ليس علينا استخدام مُرشِّح جينجا safe| عند تصيير الحقول بواسطة إضافة WTForms، وهذا لأنَّ الإضافة تقوم بترشيح المُخرجات تلقائياً لتصييرها بشكل آمن.

ألقِ نظرة على توثيقات الإضافة للمزيد من التفاصيل.

الخلاصة

  • يُمكِن أن تكون الاستمارات مُضرَّة ومصدر قلق من الناحية الأمنيّة.
  • تجعل إضافة Flask-WTF من السهل تعريف وتأمين وتصيير الاستمارات.
  • استخدم ميزة الحماية من ثغرة تزوير الطلبات التي توفرها إضافة Flask-WTF لتأمين استمارات تطبيقك.
  • يمكنك استخدام إضافة Flask-WTF لتأمين طلبات الأجاكس أيضاً من هجمات تزوير الطلبات.
  • استخدم المُصادِقات لإبقاء عمليّة المُصادقة معزولة خارج دوال العرض.
  • استخدم ميّزة تصيير شيفرات حقول الإدخال التلقائيّة من إضافة WTForms لكيلا تضطر إلى تحديث جميع الحقول يدوياً في كل مرَّة تجري فيها تعديلات على تعريف الاستمارة.

results matching ""

    No results matching ""