Skip to content

21. إدارة الوصول بين المجالات

21.1. البنية

سنناقش الآن مشكلة الطلبات عبر النطاقات. في المستند [Tutoriel AngularJS / Spring 4]، يتم تطوير تطبيق عميل/خادم حيث يكون العميل هو تطبيق AngularJS:

  • تأتي الصفحات HTML / CSS / JS الخاصة بتطبيق Angular من الخادم [1]؛
  • في [2]، تقوم الخدمة [dao] بإرسال طلب إلى خادم آخر، وهو الخادم [2]. حسنًا، هذا الأمر محظور من قِبل المتصفح الذي يقوم بتشغيل تطبيق Angular لأنه يمثل ثغرة أمنية. لا يمكن للتطبيق أن يستعلم إلا عن الخادم الذي أتى منه، أي الخادم [1]؛

في الواقع، من غير الدقيق القول إن المتصفح يمنع تطبيق Angular من الاستعلام عن الخادم [2]. فالمتصفح يستعلم في الواقع عن الخادم [2] لمعرفة ما إذا كان يسمح لعميل لا ينتمي إليه بالاستعلام عنه. تُسمى هذه التقنية «CORS» (Cross-Origin Resource Sharing). ويوافق الخادم [2] على ذلك عن طريق إرسال رؤوس HTTP محددة.

لإظهار المشكلات التي قد نواجهها، سنقوم بإنشاء تطبيق عميل/خادم حيث:

  • سيكون الخادم هو خادم الويب الآمن الخاص بنا / jSON؛
  • سيكون العميل عبارة عن صفحة بسيطة HTML مزودة برمز جافا سكريبت يقوم بإرسال طلبات إلى خادم الويب / jSON؛

سنقوم بتنفيذ البنية التالية:

  • في [1]، يقوم تطبيق ويب بتقديم صفحات HTML / jS؛
  • في [2]، يقوم المتصفح بتنفيذ جافا سكريبت المضمن في الصفحات HTML لاستعلام الخدمة الويب الآمنة [3]؛

21.2. المشروع [spring-cors-server-jdbc-generic]

21.2.1. إعداد بيئة العمل

  
  • قم بتحميل المشاريع المذكورة أعلاه. ستجد مشاريع [spring-cors-*] في المجلد [<exemples>\spring-database-generic\spring-cors
  • اضغط على Alt-F5 وأعد إنشاء جميع مشاريع Maven؛

ثم قم بتشغيل تكوين التشغيل المسمى [spring-cors-server-jdbc-generic] (يجب تشغيل SGBD و MySQL) الذي يقوم بتشغيل خدمة ويب على المنفذ 8081:

 

املأ قاعدة البيانات [dbproduitscategories] باستخدام تكوين التشغيل المسمى [spring-jdbc-generic-04-fillDataBase]:

 

قم بتنفيذ تكوين التشغيل المسمى [spring-cors-client-generic] الذي يقوم بتشغيل تطبيق ويب ثانٍ (على Tomcat آخر) على المنفذ 8082:

 

باستخدام متصفح، اطلب URL [http://localhost:8082/client.html]:

  • في [1]، يُطلب الحصول على النسخة المختصرة لجميع الفئات؛
  • في [2]، يتم استلام الرد jSON من الخادم؛

21.2.2. مشروع العميل [spring-cors-client-generic]

  

يتيح لنا الملف [application.properties] تحديد منفذ تطبيق الويب الخاص بالعميل. ومحتواه كما يلي:


server.port=8082

وبالتالي:

  • العميل هو تطبيق ويب متاح على URL [http://localhost:8082
  • الخادم هو تطبيق ويب متاح على URL [http://localhost:8081

ونظرًا لأن العميل لا يتم الوصول إليه من نفس المنفذ الذي يستخدمه الخادم، فإن مشكلة الطلبات عبر النطاقات تظهر. في الواقع، [http://localhost:8081] و [http://localhost:8082] هما نطاقان مختلفان.

21.2.3. تكوين Maven

المشروع هو مشروع Maven يحتوي على الملف [pom.xml] التالي:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-cors-client-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-cors-client-generic</name>
    <description>Client cors for webjson server</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- البحث عن الأصل من المستودع -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <!-- المكونات الإضافية -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • الأسطر 14-19: هذا مشروع Spring Boot؛
  • الأسطر 27-30: يتم استخدام التبعية [spring-boot-starter-web] التي تتضمن خادم Tomcat و Spring MVC؛

21.2.4. أساسيات jQuery وجافا سكريبت

يعرض تطبيق الويب الصفحة التالية الوحيدة:

 

وتتضمن هذه الصفحة كود جافا سكريبت (jS) يتم تنفيذه في المتصفح. سنقدم بعض أساسيات جافا سكريبت التي ستساعدنا على فهم الكود. سيقوم العميل بإجراء استدعاءات HTTP باستخدام المكتبة jQuery [https://jquery.com/] التي توفر العديد من الوظائف التي تسهل تطوير جافا سكريبت. نقوم بإنشاء ملف ثابت HTML [jQuery.html] ونضعه في المجلد [static]:

 

سيحتوي هذا الملف على المحتوى التالي:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/js/jquery-2.1.3.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>
  • السطر 6: استيراد ملف jQuery؛
  • الأسطر 10-12: عنصر من الصفحة التي تحمل المعرف [element1]. سنقوم بالتعديل على هذا العنصر.

علينا تنزيل الملف [jquery-2.1.3.min.js]. سنجد أحدث إصدار من jQuery في URL و[http://jquery.com/download/]:

Image

سنضع الملف الذي تم تنزيله في المجلد [static / js]:

  

بعد ذلك، نطلب العرض الثابت [jQuery.html] باستخدام متصفح Chrome [1-2]:

باستخدام Google Chrome، أدخل [Ctrl-Maj-I] لإظهار أدوات التطوير [3]. تتيح علامة التبويب [Console] [4] تنفيذ كود جافا سكريبت. نقدم في ما يلي أوامر جافا سكريبت التي يجب كتابتها مع شرح لها.

JS
résultat
$("#element1")
: يعرض مجموعة جميع العناصر التي تحمل المعرف
[element1]، وبالتالي عادةً ما تكون مجموعة
تتكون من عنصر واحد أو لا شيء، لأنه لا يمكن وجود
معرفين متطابقين في صفحة واحدة HTML.
$("#element1").text("blabla")
: يطبق النص [blabla] على جميع عناصر
المجموعة. ويؤدي ذلك إلى تغيير
المحتوى المعروض في الصفحة
$("#element1").hide()
يخفي عناصر المجموعة. لم يعد النص
[blabla] لم يعد معروضًا.
$("#element1")
: يعرض المجموعة مرة أخرى. وهذا
رؤية أن العنصر ذو المعرف [element1] يحتوي
يحتوي على السمة CSS style='display: none;' التي
إخفاء العنصر.
$("#element1").show()
: يعرض عناصر المجموعة. يظهر النص
[blabla] يظهر مرة أخرى. السمة
CSS style='display: block;' هو الذي يضمن هذا
العرض.
$("#element1").attr('style','color: red')
: يحدد سمة لجميع عناصر
المجموعة. السمة هنا هي [style] وقيمتها
[color: red]. يتحول النص [blabla] إلى اللون الأحمر.
Tableau
Dictionnaire

تجدر الإشارة إلى أن معرّف المتصفح URL لم يتغير خلال جميع هذه العمليات. لم تحدث أي تفاعلات مع خادم الويب. كل شيء يحدث داخل المتصفح. والآن، دعونا نلقي نظرة على الكود المصدري للصفحة:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

هذا هو النص الأصلي. وهو لا يعكس بأي شكل من الأشكال المعالجات التي أجريناها على العنصر في الأسطر 10-12. من المهم تذكر ذلك عند إجراء تصحيح أخطاء جافا سكريبت. لذا، غالبًا ما يكون من غير المجدي عرض الكود المصدري للصفحة المعروضة.

21.2.5. الكود jS الخاص بالتطبيق

لنعد إلى الرمز HTML الخاص بصفحة تطبيق العميل التي ستستعلم عن خدمة الويب / jSON:

 

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  طريقة HTTP -->
        Méthode HTTP :
        <!--  -->
        <input type="radio" id="get" name="method" value="get" checked="checked" />GET
        <!--  -->
        <input type="radio" id="post" name="method" value="post" />POST
        <!--  URL -->
        <br /> <br />URL cible : <input type="text" id="url" size="30"><br />
        <!-- القيمة التي تم إدخالها -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
        <!-- زر التأكيد -->
        <br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • السطر 6: يتم استيراد المكتبة jQuery؛
  • السطر 7: يتم استيراد كود سنقوم بكتابته؛
  • الأسطر 11 و15 و17 و21: نلاحظ معرّفات [id] لمكونات الصفحة. يشير جافا سكريبت إلى هذه المكونات عبر هذه المعرّفات؛

الكود [client.js] هو كما يلي:


// البيانات العامة
var url;
var posted;
var response;
var method;

function requestServer() {
    // يتم استرداد معلومات النموذج
    var urlValue = url.val();
    var postedValue = posted.val();
    method = document.forms[0].elements['method'].value;
    // يتم إجراء استدعاء Ajax يدويًا
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // يتم إجراء استدعاء Ajax يدويًّا
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081' + url،
        type : 'GET',
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // نتيجة نصية
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // خطأ في النظام
            response.text(jqXHR.responseText);
        }
    })
}

function doPost(url, posted) {
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        headers : {
            'Authorization' : 'Basic YWRtaW46YWRtaW4='
        },
        url : 'http://localhost:8081    ' + url،
        type : 'POST',
        contentType : 'application/json',
        data : posted,
        dataType : 'tex/plain',
        beforeSend : function() {
        },
        success : function(data) {
            // نتيجة نصية
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // خطأ في النظام
            response.text(jqXHR.responseText);
        }
    })
}

// أثناء تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع مكونات الصفحة
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • الأسطر 71-75: من الكود jS الذي يتم تنفيذه عند انتهاء تحميل المستند في المتصفح؛
  • الأسطر 73-75: يتم استرداد مراجع ثلاث مناطق من المستند HTML؛
  • الأسطر 2-5: متغيرات عامة معروفة في جميع الدوال المُعرَّفة في الملف jS؛
  • السطر 9: يتم استرداد قيمة URL التي أدخلها المستخدم؛
  • السطر 10: يتم استرداد القيمة التي يرغب في إرسالها؛
  • السطر 11: يتم استرداد طريقة [get] أو [post] التي سيتم استخدامها لطلب URL من السطر 9:
    • يشير «document» إلى المستند الذي تم تحميله بواسطة المتصفح، وهو ما يُعرف باسم DOM (نموذج كائن المستند)،
    • يشير document.forms[0] إلى النموذج الأول في المستند، حيث يمكن أن يحتوي المستند على عدة نماذج. هنا، يوجد نموذج واحد فقط،
    • document.forms[0].elements['method'] يشير إلى عنصر النموذج الذي يحمل السمة [name='method']. وهناك عنصران:

<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
  • (تابع)
    • document.forms[0].elements['method'].value هي القيمة التي سيتم إرسالها للمكون الذي يحمل السمة [name='method']. من المعروف أن القيمة التي يتم إرسالها هي قيمة السمة [value] الخاصة بزر الاختيار المحدد. وهنا، ستكون إحدى السلاسل ['get', 'post']؛
  • الأسطر 13-18: وفقًا للطريقة HTTP المطلوب استخدامها، يتم تنفيذ الطريقة [doGet] أو [doPost
  • تقوم الطريقة jQuery [$.ajax] بإجراء استدعاء HTTP؛
  • الأسطر 23-25: يتم الاتصال بخادم يتطلب رأسًا HTTP [Authorization: Basic code]. نقوم بإنشاء هذا الرأس للمستخدم [admin / admin] الذي هو الوحيد القادر على الاستعلام من الخادم؛
  • السطر 26: سيقوم المستخدم بإدخال URL من النوع [/getAllLongCategories, /saveCategories, ...]. لذا يجب استكمال هذه القيم URL؛
  • السطر 27: طريقة HTTP المطلوب استخدامها؛
  • السطر 28: يقوم الخادم بإرجاع jSON. يتم تحديد النوع [text/plain] كنوع للنتيجة من أجل عرضها كما تم استلامها؛
  • السطر 33: عرض الرد النصي من الخادم؛
  • السطر 39: عرض رسالة الخطأ المحتملة بتنسيق نصي؛
  • السطر 44: تتلقى الطريقة [doPost] معلمة ثانية وهي القيمة المراد إرسالها؛
  • السطر 52: للإشارة إلى أن القيمة المرسلة ستكون على شكل سلسلة jSON؛

21.2.6. تشغيل العميل

تطبيق العميل هو تطبيق وحدة التحكم يتم تشغيله بواسطة الفئة القابلة للتنفيذ [Client] التالية:

  

package spring.cors.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

@EnableAutoConfiguration
public class Client {

    public static void main(String[] args) {
        SpringApplication.run(Client.class, args);
    }
}
  • السطر 6: التعليق التوضيحي [@EnableAutoConfiguration] هو تعليق توضيحي للمشروع [Spring Boot] (السطر 4). سيقوم Spring Boot بفحص الملفات الموجودة في مسار الفئات (Classpath) للمشروع. وستكون هذه في هذه الحالة جميع تبعيات Maven التي تجلبها التبعية الوحيدة لملف [pom.xml]:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
</dependencies>

تجلب هذه التبعية عددًا كبيرًا جدًّا من الملفات المضغوطة، ولا سيما Spring MVC وخادم Tomcat. وبسبب وجود هذه التبعيات، سيقوم Spring Boot بتكوين مشروع Spring MVC الذي يعمل على Tomcat باستخدام القيم الافتراضية. ويتم عندئذ تكوين خادم Tomcat للعمل على المنفذ 8080. وإذا أردنا تجاوز القيم الافتراضية التي اختارها Spring Boot، فيمكننا استخدام الملف [application.properties] الموجود في جذر مسار Classpath (كل ما يوجد في [src / main / resources] موجود في جذر مسار Classpath):

  

نحدد أن يعمل خادم Tomcat على المنفذ 8082 بالطريقة التالية:


server.port=8082

يمكن العثور على قائمة المعلمات القابلة للاستخدام في [application.properties] إلى URL (يونيو 2015) [http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

العودة إلى كود [Client.java]:

  • السطر 10: ستقوم الطريقة [SpringApplication.run] بنشر الصفحة [client.html] على خادم Tomcat الموجود في مسار Classpath للمشروع؛

21.2.7. URL [/getAllShortCategories]

نقوم بتشغيل:

  • خادم الويب/json الآمن على المنفذ 8081 (تكوين [spring-security-server-jdbc-generic])؛
  • عميل هذا الخادم على المنفذ 8082 (التكوين [spring-cors-client-generic])؛

ثم نطلب URL [http://localhost:8082/client.html] [1]:

  • في [2]، نقوم بعمل GET على URL و [http://localhost:8081/getAllShortCategories

لا نتلقى أي استجابة من الخادم. عند الاطلاع على وحدة التحكم في تطوير Chrome (Ctrl-Shift-I)، نلاحظ وجود خطأ:

  • في [1]، ونحن في علامة التبويب [Network
  • في [2]، نلاحظ أن الطلب HTTP الذي تم إرساله ليس [GET] بل [OPTIONS]. في حالة الطلب عبر النطاقات، يتحقق المتصفح من الخادم للتأكد من استيفاء عدد معين من الشروط عن طريق إرسال طلب HTTP [OPTIONS] إليه. في هذه الحالة، الطلبات هي تلك المشار إليها بالرموز [5-6]؛
  • في [5]، يسأل المتصفح عما إذا كان يمكن الوصول إلى الهدف URL باستخدام GET. يطلب الطلب [Access-Control-Request-Method] استجابةً برأس HTTP [Access-Control-Allow-Methods] يشير إلى أن الطريقة المطلوبة مقبولة؛
  • في [6]، يرسل المتصفح رأس HTTP [Origin: http://localhost:8081]. يطلب هذا الرأس استجابة في رأس HTTP [Access-Control-Allow-Origin] تشير إلى قبول المصدر المحدد؛
  • في [7]، يسأل المتصفح عما إذا كانت الرؤوس HTTP و[accept] و[authorization] مقبولة. الطلب [Access-Control-Request-Headers] ينتظر ردًا يتضمن رأسًا HTTP و[Access-Control-Allow-Headers] يشير إلى قبول الرؤوس المطلوبة؛
  • يظهر خطأ في [3]. عند النقر على الأيقونة، يظهر الخطأ [4]؛
  • في [4]، تشير الرسالة إلى أن الخادم لم يرسل الرأس HTTP [Access-Control-Allow-Origin] الذي يحدد ما إذا كان مصدر الطلب مقبولًا أم لا؛
  • في [8]، يمكن ملاحظة أن الخادم لم يرسل هذا الرأس بالفعل. ونتيجة لذلك، رفض المتصفح تنفيذ الطلب HTTP GET الذي تم طلبه في البداية؛

علينا تعديل خادم الويب / jSON.

21.2.8. خدمة ويب جديدة / json

نقوم بإنشاء مشروع Maven جديد [spring-cors-server-jdbc-generic]:

 

تكوين Maven لخدمة الويب الجديدة هو كما يلي:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-cors-server-jdbc-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-cors-server-jdbc-generic</name>
    <description>démo spring cors</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <!-- المكونات الإضافية -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jdbc-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • الأسطر 30-32: نستفيد من كل ما تم إنجازه حتى الآن بالاعتماد على أرشيف خادم الويب / json الآمن؛

وفي النهاية، تكون التبعيات كما يلي:

  

فئة التكوين [AppConfig] هي كما يلي:

  

package spring.cors.server.config;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.DispatcherServlet;

@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ spring.security.config.AppConfig.class })
public class AppConfig {

    // طلبات عبر النطاقات
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}
  • السطر 12: الفئة هي فئة تكوين Spring؛
  • السطر 9: توجد مكونات Spring أخرى في الحزمة [spring.cors.server.service
  • السطر 14: يتم استيراد الفاصوليا من مشروع [spring-security-server-jdbc-generic
  • الأسطر 18-21: نقوم بإنشاء مكون Spring باسم [isCorsEnabled] يحدد ما إذا كنا نقبل العملاء من خارج نطاق الخادم أم لا؛

21.2.9. وحدات التحكم

تحتوي خدمة الويب الجديدة على أربعة وحدات تحكم:

  
  • تتولى وحدة التحكم [CorsCategorieController] إدارة وحدة التحكم URL الخاصة بمعالجة الفئات. وهي لا تدير سوى رؤوس CORS الخاصة بعملاء الويب. بخلاف ذلك، فإنها تفوض المهمة إلى وحدة التحكم [CategorieController] التابعة لوحدة [spring-webjson-server-jdbc-generic
  • ويفعل كل من [CorsProduitController] و[CorsAuthenticateController] الشيء نفسه عن طريق تفويض المهمة إلى وحدات التحكم [ProduitController] التابعة لـ [spring-webjson-server-jdbc-generic] و[AuthenticateController] التابعة لـ[spring-security-server-jdbc-generic
  • يُستخدم [CorsController] لتجميع العناصر المشتركة بين وحدات التحكم الثلاث السابقة؛

21.2.9.1. وحدة التحكم [CorsController]

الفئة [CorsController] هي كما يلي:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CorsController {

    @Autowired
    private boolean isCorsEnabled;

    // إرسال الخيارات إلى العميل
    public void sendOptions(String origin, HttpServletResponse response) {
        // هل CORS مسموح به؟
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // يتم تعيين الرأس CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // يُسمح ببعض الرؤوس
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
        // يُسمح بـ GET
        response.addHeader("Access-Control-Allow-Methods", "GET");
    }
}
  • السطر 8: الفئة [CorsController] هي وحدة تحكم Spring؛
  • السطران 11-12: حقن الكائن [isCorsEnabled] الذي يحدد ما إذا كان يجب معالجة الرؤوس CORS أم لا؛
  • الأسطر 15-26: تتولى الطريقة [sendOptions] الرد على العملاء الذين يرسلون رؤوس CORS؛
  • الأسطر 17-19: إذا كان التطبيق مُهيأً لقبول الطلبات عبر النطاقات، وإذا كان المرسل قد أرسل الرأس HTTP [Origin]، وإذا كان هذا المصدر يبدأ بـ [http://localhost]، فسيتم قبول الطلب عبر النطاقات، وإلا فسيتم رفضه؛
  • السطر 21: إذا كان العميل في المجال [http://localhost:port]، يتم إرسال الرأس HTTP:
Access-Control-Allow-Origin:  http://localhost:port

مما يعني أن الخادم يقبل مصدر العميل؛

  • الأسطر 22-25: أشرنا إلى رأسين خاصين هما HTTP في الطلب HTTP [OPTIONS]:
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization

عند رأس الرسالة HTTP [Access-Control-Request-X]، يرد الخادم برأس رسالة HTTP [Access-Control-Allow-X] يوضح فيه ما هو مسموح به. تكتفي الأسطر 22-25 بإعادة طلب العميل للإشارة إلى أنه تم قبوله؛

21.2.9.2. وحدة التحكم [CorsCategorieController]


package spring.cors.server.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import spring.jdbc.entities.Categorie;
import spring.webjson.server.entities.CoreCategorie;
import spring.webjson.server.service.CategorieController;
import spring.webjson.server.service.Response;

@RestController
public class CorsCategorieController extends CorsController {

    @Autowired
    private CategorieController categorieController;

    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.OPTIONS)
    public void corsGetAllShortCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
    }

    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // طريقة الأصل
        return categorieController.getAllShortCategories();
    }

...
}
  • السطر 19: التعليق التوضيحي [@RestController] يجعل الفئة في آن واحد مكونًا في Spring ووحدة تحكم MVC التي ترسل بنفسها استجاباتها إلى العميل؛
  • السطر 20: الفئة [CorsCategorieController] تمتد من الفئة [CorsController] التي رأيناها للتو؛
  • السطران 22-23: حقن وحدة التحكم [CategorieController categorieController] من التبعية [spring-webjson-server-jdbc-generic
  • الأسطر 25-29: تعالج URL [/cors-getAllShortCategories] عند طلبها باستخدام الأمر HTTP [OPTIONS]. وبموجب الاتفاق، قررنا أن عملاء الويب الذين يرغبون في استدعاء URL [/U] من خدمة الويب الآمنة، سيتعين عليهم في الواقع استدعاء URL [/cors-U]. وبذلك، ستتضمن خدمة الويب التي تم نشرها نوعين من URL:
    • [/U]: للعملاء غير المستخدمين للويب؛
    • [/cors-U]: للعملاء الذين يستخدمون الويب؛
  • السطر 25: تقبل الطريقة [/cors-getAllShortCategories] المعلمات التالية:
    • الكائن [@RequestHeader(value = "Origin", required = false)] الذي سيسترد رأس الطلب HTTP [Origin]. وقد تم إرسال رأس الطلب هذا من قبل مرسل الطلب:
Origin:http://localhost:8082

يُشار إلى أن الرأس HTTP [Origin] اختياري [required = false]. في هذه الحالة، إذا كان الرأس غائبًا، فستكون قيمة المعلمة [String origin] هي null. مع [required = true]، وهي القيمة الافتراضية، يتم إطلاق استثناء إذا كان الرأس غائبًا. وقد أردنا تجنب هذه الحالة؛

  • (تابع)
    • الكائن [HttpServletResponse response] الذي سيتم إرجاعه إلى العميل الذي قدم الطلب؛

يتم إدخال هذين المعلمتين بواسطة Spring؛

  • السطر 28: يتم تفويض معالجة الطلب إلى الطريقة [sendOptions] التابعة للفئة الأم [CorsController
  • الأسطر 31-36: تعالج الطريقة [getAllShortCategories] الكائن URL [/cors-getAllShortCategories] عند طلبها باستخدام GET؛
  • السطر 35: يتم تفويض المهمة إلى الأسلوب [CategorieController.getAllShortCategories] التابع لـ [spring-webjson-server-jdbc-generic

نحن الآن جاهزون لإجراء اختبارات جديدة. نقوم بتشغيل الإصدار الجديد من خدمة الويب ونكتشف أن المشكلة لا تزال قائمة. لم يتغير شيء. إذا أضفنا عرضًا على وحدة التحكم في السطر 28 أعلاه، فلن يتم عرضه أبدًا، مما يدل على أن الطريقة [corsGetAllShortCategories] في السطر 25 لم يتم استدعاؤها أبدًا.

بعد إجراء بعض الأبحاث، اكتشفنا أن Spring MVC يعالج بنفسه الأوامر HTTP و [OPTIONS] باستخدام معالجة افتراضية. وبالتالي، فإن Spring هو الذي يستجيب دائمًا، ولا يتم استدعاء الطريقة [corsGetAllShortCategories] الموجودة في السطر 25 أبدًا. يمكن تغيير هذا السلوك الافتراضي لـ Spring MVC. نقوم بتعديل الفئة [AppConfig] الموجودة:

  

package spring.cors.server.config;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.DispatcherServlet;

@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ spring.security.config.AppConfig.class })
public class AppConfig {

    // طلبات عبر النطاقات
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
    
    @Autowired
    private DispatcherServlet dispatcherServlet;
    
    @PostConstruct
    public void init(){
        // التطبيق يعالج الطلبات بنفسه HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}

  • السطران 23-24: نقوم بإدخال المكون [DispatcherServlet dispatcherServlet] الذي تم تعريفه في التبعية [spring-webjson-server-jdbc-generic
  • الأسطر 26-30: تعمل العلامة التوضيحية [@PostConstruct] على ضمان تنفيذ الطريقة [init] بعد إنشاء مثيل للفئة [AppConfig] وبعد عمليات الإدخال التي يقوم بها Spring؛
  • السطر 29: يُطلب من السيرفلت أن يحيل الأوامر HTTP و [OPTIONS] إلى التطبيق؛

نُعيد إجراء الاختبارات باستخدام هذا التكوين الجديد. ونحصل على النتيجة التالية:

  • في [1]، نلاحظ وجود طلبين HTTP موجهين إلى URL و[http://localhost:8080/getAllCategories
  • في [2]، الطلب [OPTIONS
  • في [3]، الرؤوس الثلاثة HTTP التي قمنا بتكوينها للتو في استجابة الخادم؛

لننظر الآن إلى الطلب الثاني:

  • إلى [1]، وهو الطلب الذي تم فحصه؛
  • إلى [2]، وهو الطلب GET. بفضل الطلب الأول [OPTIONS]، تلقى المتصفح المعلومات التي طلبها. وهو الآن ينفذ الطلب [GET] الذي تم طلبه في البداية؛
  • في [3]، رد الخادم؛
  • في [4]، يرسل الخادم jSON؛
  • في [5]، حدث خطأ؛
  • في [6]، رسالة الخطأ؛

من الصعب شرح ما حدث هنا. رد الخادم [3] هو رد طبيعي [HTTP/1.1 200 OK]. لذلك من المفترض أن نحصل على المستند المطلوب. من المحتمل أن يكون الخادم قد أرسل المستند بالفعل، لكن المتصفح هو الذي يمنع استخدامه لأنه يريد أن تتضمن الاستجابة لطلب GET أيضًا الرأس HTTP [Access-Control-Allow-Origin:http://localhost:8081].

نقوم بتعديل الطريقة التي تعالج GET من URL و[/cors-getAllShortCategories]:


    @RequestMapping(value = "/cors-getAllShortCategories", method = RequestMethod.GET)
    public Response<List<Categorie>> getAllShortCategories(
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // رؤوس CORS
        sendOptions(origin, response);
        // طريقة المنشأ
        return categorieController.getAllShortCategories();
}
  • السطر 5: كما هو الحال بالنسبة لطلب HTTP و [OPTIONS]، سيقوم الخادم بإرسال الرؤوس HTTP CORS لطلب HTTP [GET

بعد هذا التعديل، تكون النتائج كما يلي:

 

لقد حصلنا بالفعل على النسخة المختصرة لجميع الفئات.

21.2.9.3. القيم URL و [GET]

في وحدات التحكم [CorsCategorieController, CorsProduitController, CorsAuthenticateController]، يتبع كود الإجراءات التي تعالج URL المطلوبة مع [GET] نمط الإجراءات التي عالجت سابقًا URL و [/cors-getAllShortArticles]. يمكن للقارئ التحقق من الكود في الأمثلة المرفقة مع هذا المستند. فيما يلي مثال على URL و[/cors-getAllLongProduits] الخاصين بوحدة التحكم [CorsProduitController]:


package spring.cors.server.service;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import spring.jdbc.entities.Produit;
import spring.webjson.server.entities.CoreProduit;
import spring.webjson.server.service.ProduitController;
import spring.webjson.server.service.Response;

@RestController
public class CorsProduitController extends CorsController {

    @Autowired
    private ProduitController produitController;

@RequestMapping(value = "/cors-getAllLongProduits", method = RequestMethod.GET)
    public Response<List<Produit>> getAllLongProduits(@RequestHeader(value = "Origin", required = false) String origin,HttpServletResponse response) {
        // رؤوس CORS
        sendOptions(origin, response);
        // طريقة المنشأ
        return produitController.getAllLongProduits();

    }

    @RequestMapping(value = "/cors-getAllLongProduits", method = RequestMethod.OPTIONS)
    public void corsGetAllLongProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
    }
...
}
 

21.2.9.4. URL [POST]

لننظر في الحالة التالية:

  • نقوم بإجراء تحويل من POST [1] إلى URL [2]؛
  • في [3]، القيمة المنشورة. وهي عبارة عن السلسلة jSON لفئة لا تحتوي على منتجات؛
  • في النهاية، نسعى إلى إنشاء فئة تسمى [categorie[2]]؛

لن نقوم بتعديل أي كود في الوقت الحالي. والنتيجة التي نحصل عليها هي كما يلي:

  • في [1]، كما هو الحال مع الاستعلامات [GET]، يقوم المتصفح بإجراء استعلام [OPTIONS
  • في حالة [2]، يطلب المتصفح إذنًا بالوصول لطلب [POST]. وكان الطلب السابق هو [GET
  • في [3]، يطلب إذنًا لإرسال الرؤوس HTTP و[accept, authorization, content-type]. في السابق، لم يكن لدينا سوى الرؤوس الأولى والثانية؛
  • في [4]، لا تمنح خدمة الويب جميع الأذونات المطلوبة، مما يتسبب في حدوث الخطأ [5]؛

نقوم بتعديل الطريقة [CorsController.sendOptions] على النحو التالي:


    public void sendOptions(String origin, HttpServletResponse response) {
        // هل يُسمح بـ Cors؟
        if (!isCorsEnabled || origin == null || !origin.startsWith("http://localhost")) {
            return;
        }
        // يتم تعيين الرأس CORS
        response.addHeader("Access-Control-Allow-Origin", origin);
        // يُسمح ببعض الرؤوس
        response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
        // يُسمح بـ GET و POST
        response.addHeader("Access-Control-Allow-Methods", "GET, POST");
    }
}
  • السطر 9: تمت إضافة الرأس HTTP [Content-Type] (لا يهم استخدام الأحرف الكبيرة أو الصغيرة)؛
  • السطر 11: تمت إضافة الطريقة HTTP [POST

وبذلك، تُعالج الطرق [POST] بنفس الطريقة التي تُعالج بها الاستعلامات [GET]. فيما يلي مثال على URL و[/cors-saveCategories] في وحدة التحكم [CorsCategorieController]:


    @RequestMapping(value = "/cors-saveCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Response<List<CoreCategorie>> saveCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
        // رؤوس CORS
        sendOptions(origin, response);
        // طريقة المنشأ
        return categorieController.saveCategories(request);
    }

    @RequestMapping(value = "/cors-saveCategories", method = RequestMethod.OPTIONS)
    public void corsSaveCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        sendOptions(origin, response);
}

بعد إجراء هذه التعديلات، تكون النتيجة كما يلي:

 

تمت إضافة الفئة [categorie[2]] بنجاح إلى قاعدة البيانات. وقد خصص لها SGBD المفتاح الأساسي 226. ويمكن التحقق من ذلك باستخدام الطريقة GET [/cors-getAllShortCategories]:

 

21.2.10. الخلاصة

يدعم تطبيقنا الآن الاستعلامات عبر النطاقات. ويمكن السماح بهذه الاستعلامات أو حظرها من خلال التكوين في الفئة [AppConfig]:


package spring.cors.server.config;

...

@Configuration
@ComponentScan(basePackages = { "spring.cors.server.service" })
@Import({ spring.security.config.AppConfig.class })
public class AppConfig {

    // طلبات عبر النطاقات
    @Bean
    public boolean isCorsEnabled() {
        return true;
    }
...
}

21.3. مشروع Eclipse [spring-cors-server-jpa-generic]

سيتم الآن تنفيذ خدمة الويب CORS من خلال المشروع [spring-cors-server-jpa-generic] الذي يعتمد على المشروع [spring-security-server-jpa-generic] الذي يدير الوصول إلى قاعدة البيانات باستخدام Spring Data JPA:

يتم الحصول على المشروع [spring-cors-server-jpa-generic] عن طريق نسخ المشروع الذي تمت دراسته سابقًا [spring-cors-server-jdbc-generic].

  

بعد ذلك، هناك تعديلان يجب إجراؤهما. الأول في الملف [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-cors-server-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>spring-cors-server-jpa-generic</name>
    <description>démo spring cors</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <!-- المكونات الإضافية -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>spring-security-server-jpa-generic</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>
  • الأسطر 30-32: التبعية على خدمة الويب الآمنة [spring-security-server-jpa-generic

وفي النهاية، تكون تبعيات المشروع كما يلي:

  

ملاحظة: اضغط على Alt-F5 ثم أعد إنشاء جميع المشاريع

التعديل الثاني هو تحديث عمليات الاستيراد في الفئات التي تشير إلى أخطاء [Alt-Maj-O].

هذا كل شيء. نقوم بتشغيل خدمة الويب CORS باستخدام إعدادات التشغيل [spring-cors-server-jpa-generic-hibernate-eclipselink]:

ثم نقوم بتشغيل العميل العام:

ومن خلال متصفح، نطلب URL و [1] مع GET. في [2]، نلاحظ أن النسخة المختصرة من الفئات المُرجعة تحتوي على الحقل [entityType] الذي لم يكن موجودًا في النسخة السابقة JDBC.

سنقوم بمعالجة بنيتين أخريين هما CORS:

  • البنية CORS / JPA EclipseLink / DB2؛
  • البنية CORS / JPA OpenJpa / Firebird؛

سنقوم بتنفيذ البنية التالية:

نقوم بتحميل المشاريع التالية:

  

ملاحظة: اضغط على Alt-F5 وأعد إنشاء جميع مشاريع Maven.

قم بتشغيل SGBD و DB2 وتأكد من وجود قاعدة البيانات [dbproduitscategories]. إذا لم تكن موجودة، فقم بإنشائها (الفقرة 12.1.2).

يتم إنشاء المستخدمين في قاعدة البيانات [dbproduitscategories] باستخدام إعدادات التشغيل [spring-security-create-users-hibernate-eclipselink]:

Image

ثم قم بتشغيل خدمة الويب CORS باستخدام إعدادات التشغيل المسماة [spring-cors-server-jpa-generic-hibernate-eclipselink] وعميلها المسمى [spring-cors-client-generic]:

قم بتعبئة قاعدة البيانات [dbproduitscategories] بالقيم باستخدام إعداد التشغيل [spring-jdbc-generic-04-fillDataBase]:

 

وأخيرًا، اطلب في متصفح الويب ما يلي: URL:

 

21.5. بنية CORS / JPA OpenJPA / Firebird

سنقوم الآن بتنفيذ البنية التالية:

نقوم بتحميل المشاريع التالية:

  

ملاحظة: اضغط على Alt-F5 وأعد إنشاء جميع مشاريع Maven.

قم بتشغيل SGBD Firebird وتأكد من وجود قاعدة البيانات [dbproduitscategories]. إذا لم تكن موجودة، فقم بإنشائها (الفقرة 14.1.2).

يتم إنشاء المستخدمين في قاعدة البيانات [dbproduitscategories] باستخدام إعدادات التشغيل [spring-security-create-users-openjpa]:

Image

ثم قم بتشغيل خدمة الويب CORS باستخدام إعدادات التشغيل المسماة [spring-cors-server-jpa-generic-openjpa]:

قم بتشغيل العميل CORS باستخدام التكوين [spring-cors-client-generic]:

 

املأ قاعدة البيانات [dbproduitscategories] بالقيم باستخدام إعداد التشغيل [spring-jdbc-generic-04-fillDataBase]:

 

وأخيرًا، اطلب في متصفح الويب ما يلي: URL: