Skip to content

18. [Cours]: إدارة الوصول بين المجالات

الكلمات المفتاحية: CORS (Cross-Origin Resource Sharing).

هذا الفصل بعيد بعض الشيء عن TD. وقد تم الاحتفاظ به لأنه يقدم مقدمة عن برمجة الويب وبرمجة جافا سكريبت. يجب أن نتذكر هنا أن أحد أهداف هذا الفصل هو عرض المفاهيم المستخدمة بشكل متكرر في تطوير تطبيقات الويب التي تعتمد على أطر عمل Java. نستكمل هنا الخادم الويب المستخدم في دراسة قاعدة بيانات المنتجات والفئات لتمكينه من قبول الطلبات عبر المجالات.

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

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

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

سنقوم بإنشاء البنية التالية:

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

18.1. Support

  

ستجد مشاريع هذا الفصل في المجلد [support / chap-18].

18.2. مشروع العميل

نقوم بإنشاء مشروع Eclipse التالي:

  

18.3. تكوين Maven

المشروع هو مشروع Maven مع الملف [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>istia.st.webjson</groupId>
    <artifactId>intro-server-webjson-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-server-webjson-01</name>
    <description>démo spring mvc</description>

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

    <dependencies>
        <dependency>
            <groupId>istia.st.springdata</groupId>
            <artifactId>intro-spring-data-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <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>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • الأسطر 11-15: إنه مشروع Spring Boot؛
  • الأسطر 23-26: نستخدم التبعية [spring-boot-starter-web] التي تجلب معها خادم Tomcat و Spring MVC؛

18.4. تكوين Spring

  

الفئة [WebConfig] التي تهيئ مشروع Spring هي التالية:


package spring.cors.client.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // -------------------------------- تكوين الطبقة [web]
    @Autowired
    private ApplicationContext context;

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
        return servlet;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new ServletRegistrationBean(dispatcherServlet, "/*");
    }

    @Bean
    public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
        return new TomcatEmbeddedServletContainerFactory("", 8081);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/*.html").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/*.js").addResourceLocations("classpath:/static/js/");
    }
}
  • السطر 15: تقوم الفئة بتكوين مشروع Spring MVC؛
  • السطر 16: الفئة توسع الفئة [WebMvcConfigurerAdapter] لإعادة تعريف بعض أساليبها؛
  • الأسطر 18-36: لقد سبق أن تعرفنا على هذه الفاصوليا، على سبيل المثال في الفقرة 13.5.3.1. تجدر الإشارة، في السطر 35، إلى أن خدمة الويب ستعمل على المنفذ 8081؛
  • الأسطر 38-42: تسمح الطريقة [addResourceHandlers] بتعريف الموارد الثابتة، أي الموارد التي لا تعالجها [DispatcherServlet] في السطر 23؛
  • السطر 40: أي طلب لمورد له امتداد .html سيحصل على الملف المطلوب في الطلب والموجود في مجلد [static] في مسار Classpath للمشروع؛
  • السطر 41: أي طلب لمورد بامتداد .js سيحصل على الملف Javascript المطلوب في الطلب والموجود في المجلد [static/js] في مسار Classpath للمشروع؛
  

18.5. أساسيات jQuery و Javascript

ستكون صفحة HTML الخاصة بالعميل كما يلي:

 

وستتضمن معها كود جافا سكريبت (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="/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] وسنقوم بتغيير السطر 6 من الملف HTML وفقًا للإصدار المثبت.

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

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

JS
résultat
$("#element1")
: يعرض مجموعة جميع العناصر ذات المعرف [element1
لذلك عادةً ما تكون مجموعة مكونة من 0 أو 1 عنصر
لأنه لا يمكن وجود معرفين متطابقين في صفحة واحدة 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 للمتصفح لم يتغير خلال كل هذه العمليات. لم يكن هناك أي تبادل مع خادم الويب. كل شيء يحدث داخل المتصفح. الآن، لنلقِ نظرة على كود المصدر للصفحة:

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

18.6. كود جافا سكريبت للتطبيق

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

  
 

كود HTML لهذه الصفحة هو التالي:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="/client.js"></script>
</head>
<body>
    <h2>Client du service web / jSON</h2>
    <form id="formulaire">
        <!--  المعرف -->
        Identifiant :
        <!--  -->
        <input type="text" id="identifiant" name="identifiant" value="" />
        <!--  كلمة المرور -->
        <br /> <br /> Mot de passe :
        <!--  -->
        <input type="text" id="password" name="password" value="" />
        <!--  الطريقة HTTP -->
        <br /> <br /> 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 (commençant par /): <input type="text"
            id="url" size="30"><br />
        <!-- القيمة المرسلة -->
        <br /> Chaîne jSON à poster : <input type="text" id="posted"
            size="50" />
        <!-- زر التأكيد -->
        <br /> <br /> <input type="button" value="Valider"
            onclick="javascript:requestServer()"></input>
    </form>
    <hr />
    <h2>Réponse du serveur</h2>
    <div id="response"></div>
</body>
</html>
  • السطر 6: يتم استيراد مكتبة jQuery؛
  • السطر 7: يتم استيراد رمز سنقوم بكتابته؛
  • الأسطر 15 و19 و26 و29 و31: نلاحظ معرفات [id] لمكونات الصفحة. يشير جافا سكريبت إلى هذه المكونات عبر هذه المعرفات؛

الرمز [client.js] هو التالي:


// البيانات العامة
var url;
var posted;
var response;
var method;
var baseUrl = 'http://localhost:8080'؛
var identifiant;
var password;
var authorizationHeader;

function requestServer() {
    // استرداد المعلومات
    var urlValue = url.val();
    var postedValue = posted.val();
    var identifiantValue = identifiant.val();
    var passwordValue = password.val();
    var method = document.forms[0].elements['method'].value;
    authorizationCode = btoa(identifiantValue + ':' + passwordValue);
    // نقوم بمسح الرد السابق
    response.text("");
    // إجراء استدعاء Ajax يدويًا
    if (method === "get") {
        doGet(urlValue);
    } else {
        doPost(urlValue, postedValue);
    }
}

function doGet(url) {
    // إجراء استدعاء Ajax يدويًا
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // نتيجة نصية
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // خطأ في النظام
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

function doPost(url, posted) {
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'POST',
        contentType : 'application/json; charset=UTF-8',
        data : posted,
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // نتيجة نصية
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // خطأ في النظام
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}

// أثناء تحميل المستند
$(document).ready(function() {
    // يتم استرداد مراجع مكونات الصفحة
    identifiant = $("#identifiant");
    password = $("#password");
    url = $("#url");
    posted = $("#posted");
    response = $("#response");
});
  • الأسطر 80-87: من الكود jS الذي يتم تنفيذه عند انتهاء تحميل المستند في المتصفح؛
  • الأسطر 81-86: يتم استرداد مراجع العناصر المختلفة في المستند HTML، عبر معرفها [id]؛
  • الأسطر 2-9: متغيرات عامة معروفة في جميع الوظائف المحددة في الملف jS؛
  • السطر 13: يتم استرداد URL الذي أدخله المستخدم؛
  • السطر 14: يتم استرداد القيمة التي يريد نشرها (فارغة في حالة عملية GET
  • السطر 15: يتم استرداد المعرف الذي أدخله المستخدم؛
  • السطر 16: يتم استرداد كلمة المرور الخاصة به؛
  • السطر 17: يتم استرداد الطريقة [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']؛
  • السطر 18: نقوم بإنشاء ترميز Base74 لسلسلة identifiant:password. ستُستخدم هذه السلسلة المُرمزة في الرأس HTTP [Authorization] الذي سنرسله إلى الخادم لمصادقة الطلب؛
  • الأسطر 22-26: وفقًا للطريقة HTTP المطلوب استخدامها، يتم تنفيذ الطريقة [doGet] أو [doPost
  • تقوم الطريقة jQuery [$.ajax] بإجراء استدعاء HTTP؛
  • الأسطر 32-34: يتم الاتصال بخادم يتطلب رأس HTTP [Authorization: Basic code
  • السطر 35: سيقوم المستخدم بإدخال URL من النوع [/cors-getAllCategories,/cors-addProduits, ...]. لذا يجب استكمال هذه URL بـ URL من الخادم في السطر 6؛
  • السطر 36: طريقة HTTP المطلوب استخدامها؛
  • السطر 37: يعيد الخادم jSON. يتم تحديد النوع [text] كنوع للنتيجة لعرضها كما تم استلامها؛
  • السطر 42: عرض الرد النصي من الخادم؛
  • السطران 48-49: عرض رسالة الخطأ المحتملة؛
  • السطر 53: تتلقى الطريقة [doPost] معلمة ثانية وهي القيمة المراد إرسالها؛
  • السطر 61: للإشارة إلى أن القيمة المرسلة ستكون في شكل سلسلة jSON؛

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

تطبيق العميل هو تطبيق Spring Boot يتم تشغيله بواسطة الفئة القابلة للتنفيذ [Boot] التالية:

  

package spring.cors.client.boot;

import org.springframework.boot.SpringApplication;

import spring.cors.client.config.WebConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(WebConfig.class, args);
    }
}
  • السطر 10: تستخدم الطريقة [SpringApplication.run] ملف التكوين [WebConfig]. سيتم نشر الصفحة [client.html] على خادم Tomcat الموجود في مسار Classpath للمشروع؛

18.8. L'URL [/getAllCategories]

نقوم بتشغيل:

  • خادم الويب / json على المنفذ 8080؛
  • عميل هذا الخادم على المنفذ 8081؛

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

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

لا نتلقى أي استجابة من الخادم. عند النظر إلى وحدة التحكم في تطوير 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.

18.9. خدمة الويب الجديدة / json

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

18.9.1. تكوين Maven

تكوين 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>istia.st.cors</groupId>
    <artifactId>spring-cors-server-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>

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

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

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

    <dependencies>
        <dependency>
            <groupId>istia.st.spring.security</groupId>
            <artifactId>intro-spring-security-server-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

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

</project>
  • الأسطر 23-27: نستفيد من كل ما تم إنجازه حتى الآن بالاعتماد على أرشيف خادم الويب / json الآمن؛

18.9.2. تكوين Spring

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

  

package spring.cors.server.config;

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 spring.security.config.SecurityConfig;

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

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

18.9.3. الفئة [AbstractCorsController]

الفئة [AbstractCorsController] التي ستكون الفئة الأم لجميع وحدات التحكم في هذا التطبيق:

 

ورمزها هو التالي:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

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

public abstract class AbstractCorsController {

    @Autowired
    private boolean isCorsEnabled;

    // إرسال الخيارات إلى العميل
    public void setHeaders(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");
    }
}
  • السطر 7: الفئة [CorsController] هي فئة مجردة لأنها مصممة لتكون قابلة للتوسيع وليست قابلة للتجسيد؛
  • الأسطر 13-24: تضع الطريقة [setHeaders] في الاستجابة [HttpServletResponse response] (السطر 13) الموجهة إلى العميل، الرؤوس HTTP المطلوبة من قبل الطلبات عبر المجالات؛
  • السطر 33: تقبل الطريقة [/setHeaders] كمعلمات:
    • السلسلة [origin] الموجودة في الرأس HTTP [Origin] لطلبات ما بين المجالات:
Origin:http://localhost:8081

هنا، سيكون للمعلمة [origin] في السطر 13 القيمة [http://localhost:8081]. في حالة عدم احتواء الطلب على الرأس HTTP [Origin]، سيتم الترتيب للحصول على [origin==null

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

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

  • الأسطر 15-175: إذا تم تكوين التطبيق لقبول الطلبات عبر المجالات وإذا أرسل المرسل الرأس HTTP [Origin] وإذا كان هذا المصدر يبدأ بـ [http://localhost]، فسيتم قبول الطلب عبر النطاقات، وإلا فسيتم رفضه؛
  • السطر 19: إذا كان العميل في المجال [http://localhost:port]، يتم إرسال الرأس HTTP:
Access-Control-Allow-Origin:  http://localhost:port

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

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

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

18.9.4. وحدة التحكم [MyControllerWithHttpOptions]

لكي لا نضطر إلى تعديل خادم الويب غير الآمن / jSON [intro-server-webjson-01] الذي تمت دراسته في الفقرة 13.5.3، سنقوم بإنشاء وحدة تحكم جديدة حيث يقوم الخادم غير الآمن بمعالجة URL [/url]، ستعالج وحدة التحكم الجديدة URL و [/cors-url]، وستقبل URL الطلبات عبر المجالات.

الفئة [MyControllerWithHttpOptions] هي وحدة التحكم التي ستعالج الطلبات HTTP من النوع [OPTIONS]:

 

package spring.cors.server.service;

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

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.fasterxml.jackson.core.JsonProcessingException;

@Controller
public class MyControllerWithHttpOptions extends AbstractCorsController {

    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.OPTIONS)
    public void getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse){
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
    }
...
  • السطر 14: الفئة هي وحدة تحكم Spring MVC؛
  • السطر 15: الفئة [MyControllerWithHttpOptions] تمتد الفئة [AbstractCorsController] التي وصفناها للتو؛
  • السطران 17-18: تعالج الطريقة [getAllCategories] (السطر 18) تعالج URL ["/cors-getAllCategories"] عندما يتم طلبها باستخدام الطريقة HTTP [OPTIONS
  • السطر 18: تقبل الطريقة [getAllCategories] معلمتين:
    • [@RequestHeader(value = "Origin", required = false) String origin] لاسترداد قيمة الرأس HTTP [Origin:http://localhost:8081] عند وجوده. في هذا المثال، سيتلقى المعلمة [String origin] القيمة [http://localhost:8081]. هذا العنوان ليس إلزاميًا [required = false]. عندما لا يكون موجودًا، سيكون للمعلمة [String origin] القيمة null؛
    • [HttpServletResponse httpServletResponse]: الرد الذي سيتم إرساله إلى العميل؛
  • السطر 21: يتم إرسال الرؤوس HTTP التي تسمح بالطلبات عبر النطاقات. يتم تعريف الطريقة [setHeaders] في الفئة الأصلية [AbstractCorsController

ويتم ذلك بالنسبة لجميع URL المعروضة بواسطة خادم الويب / jSON غير الآمن [intro-server-webjson-01] الذي تمت دراسته في الفقرة 13.5.3. عندما يعرض هذا الخدمة URL و [/url]، تعرض الفئة [MyControllerWithHttpOptions] المذكورة أعلاه URL و [/cors-url].

18.9.5. وحدة التحكم [MyControllerWithCors]

 

الفئة [MyControllerWithCors] هي وحدة التحكم التي ستعالج الطلبات HTTP من النوع [GET] و [POST]:


package spring.cors.server.service;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
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.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;

import spring.webjson.service.MyController;

@Controller
public class MyControllerWithCors extends AbstractCorsController {

    // تبعيات Spring
    @Autowired
    private MyController myController;

...
    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // الاستجابة
        return myController.getAllCategories();
    }
...
  • السطر 17: الفئة [MyControllerWithCors] هي وحدة تحكم Spring MVC
  • السطر 18: وهي تمتد من الفئة [AbstractCorsController
  • السطران 21-22: حقن وحدة التحكم [MyController] من خادم الويب / jSON غير الآمن [intro-server-webjson-01] الذي تمت دراسته في الفقرة 13.5.3؛
  • السطور 25-27: تعالج الطريقة [getAllCategories] URL [/cors-getAllCategories] (السطر 28) عند طلبها باستخدام الطريقة HTTP [GET
  • السطر 26: سيتم إرسال نتيجة الطريقة [getAllCategories] إلى العميل. هذه النتيجة هي تدفق jSON (السمة [produces] في السطر 27 ونوع [String] لنتيجة السطر 25)؛
  • السطر 27: تتلقى الطريقة نفس المعلمات التي تتلقاها الطريقة [getAllCategories] من وحدة التحكم [MyControllerWithHttpOptions] التي درسناها للتو؛
  • السطر 30: يُطلب من الطريقة [myController.getAllCategories()] إرسال الرد؛

في النهاية، تقوم الطريقة [myController.getAllCategories()] الخاصة بالخادم غير الآمن بإرسال الرد. لقد قمنا ببساطة بإثراء ردها بالرؤوس اللازمة للطلبات عبر المجالات.

ويتم ذلك بالنسبة لجميع URL التي يعرضها خادم الويب / jSON غير الآمن [intro-server-webjson-01] الذي تمت دراسته في الفقرة 13.5.3. عندما يعرض هذا الخدمة URL [/url]، تعرض الفئة [MyControllerWithCors] المذكورة أعلاه URL [/cors-url].

سيتم تنفيذ طلب عبر النطاقات على النحو التالي:

  • يطلب الرمز JS الخاص بالعميلURL و [/cors-url] مع طلب HTTP و GET أو POST؛
  • المتصفح الذي ينفذ هذا الرمز يعترض هذا الطلب ويطلب أولاًURL [/cors-url] مع طلب HTTP OPTIONS للتحقق من أن خدمة الويب المستهدفة تقبل الطلبات عبر المجالات؛
  • تقوم إحدى طرق وحدة التحكم [MyControllerWithHttpOptions] بإرسال الرؤوس بين المجالات التي يتوقعها المتصفح؛
  • ثم يطلب المتصفح URL الأولي [/cors-url] مع طلب HTTP GET أو POST؛
  • ثم ترد عليه إحدى طرق وحدة التحكم [MyControllerWithCors

18.9.6. الاختبارات

فئة التمهيد للمشروع [intro-spring-cors-server-jpa] هي كما يلي:

  

package spring.cors.server.boot;

import org.springframework.boot.SpringApplication;

import spring.cors.server.config.AppConfig;

public class Boot {

    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}
  • السطر 10: يتم تنفيذ الطريقة الثابتة [SpringApplication.run] باستخدام تكوين Spring [AppConfig]. وبسبب هذا التكوين، يتم تشغيل خادم Tomcat المضمن في أرشيفات المشروع ويتم نشر تطبيق الويب [intro-spring-cors-server-jpa] عليه. كما يتم نشر تطبيق الويب الخاص بالخادم غير الآمن [intro-server-webjson-01]، الذي يشكل جزءًا من أرشيفات المشروع، عليه أيضًا. ونظرًا لأن المشروع [intro-spring-security-server-01] يشكل أيضًا جزءًا من الأرشيفات، يتم عرض نوعين من URL في النهاية:
    • خدمة الويب الآمنة: /url؛
    • تلك الخاصة بخدمة الويب التي تقبل الطلبات عبر النطاقات: /cors-url؛

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


@Controller
public class MyControllerWithHttpOptions extends AbstractCorsController {

    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.OPTIONS)
    public void getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse){
        System.out.println(un_texte) ;
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
    }

بعد إجراء بعض الأبحاث، نكتشف أن Spring MVC يتولى بنفسه معالجة الأوامر HTTP و [OPTIONS] بشكل افتراضي. لذلك، فإن Spring هو الذي يستجيب دائمًا، وليس أبدًا الطريقة [getAllCategories] في السطر 5 أعلاه. يمكن تغيير هذا السلوك الافتراضي لـ 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;

import spring.security.config.SecurityConfig;

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

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

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @PostConstruct
    public void init() {
        // التطبيق يعالج الطلبات بنفسه HTTP [OPTIONS]
        dispatcherServlet.setDispatchOptionsRequest(true);
    }
}
  • السطران 25-26: حقن bean [dispatcherServlet] الذي يدير طلبات العملاء. تم تعريف هذا bean في تكوين خادم الويب / jSON غير الآمن [intro-server-webjson-01] الذي تمت دراسته في الفقرة 13.5.3؛
  • السطران 28-29: سيتم تنفيذ الطريقة [init] (السطر 29) بمجرد إنشاء مثيل للفئة [AppConfig] وإجراء عمليات حقن Spring. لذلك، عند تنفيذها، يكون الحقل الموجود في السطر 26 قد تم تهيئته؛
  • السطر 31: نقوم بتكوين bean [dispatcherServlet] بحيث يترك التطبيق الويب يعالج بنفسه الأوامر HTTP [OPTIONS

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

  • في [1]، نلاحظ وجود طلبين HTTP موجهين إلى URL [http://localhost:8080/cors-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].

لذلك، نقوم بتعديل وحدة التحكم [MyControllerWithCors] بحيث ترسل هي الأخرى الرؤوس اللازمة للطلبات بين المجالات:


    @RequestMapping(value = "/cors-getAllCategories", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllCategories(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
        // الرد
        return myController.getAllCategories();
}
  • السطر 6: يتم تضمين الرؤوس اللازمة لطلبات ما بين المجالات في الرد؛

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

لقد حصلنا بالفعل على قائمة الفئات.

18.10. الأخرى URL [GET]

في وحدات التحكم [MyControllerWithCors, MyControllerWithHttpOptions]، يتبع رمز الإجراءات التي تعالج URL المطلوبة مع [GET] نموذج الإجراءات التي عالجت سابقًا URL [/cors-getAllCategories]. يمكن للقارئ التحقق من الكود في الأمثلة المرفقة مع هذا المستند. فيما يلي مثال لـ URL و [/cors-getAllProduits]:

في [MyControllerWithHttpOptions]


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.OPTIONS)
    public void getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) {
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
}

في [MyControllerWithCors]


    @RequestMapping(value = "/cors-getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse httpServletResponse) throws JsonProcessingException {
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
        // الرد
        return myController.getAllProduits();
}

والنتيجة التي تم الحصول عليها هي كما يلي:

18.11. URL [POST]

لننظر إلى الحالة التالية:

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

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

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

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


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

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

public abstract class AbstractCorsController {

    @Autowired
    private boolean isCorsEnabled;

    // إرسال الخيارات إلى العميل
    public void setHeaders(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");
    }
}
  • السطر 21: تمت إضافة الرأس HTTP [Content-Type] (لا يهم استخدام الأحرف الكبيرة أو الصغيرة)؛
  • السطر 23: تمت إضافة الطريقة HTTP [POST

وبذلك يتم التعامل مع الطرق [POST] بنفس الطريقة التي يتم بها التعامل مع الطلبات [GET]. فيما يلي مثال على URL [/cors-addArticles]:

في [MyControllerWithCors]


    @RequestMapping(value = "/cors-addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String addCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
                    throws JsonProcessingException {
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
        // الرد
        return myController.addCategories(request);
}

في [MyControllerWithHttpOptions]


    @RequestMapping(value = "/cors-addCategories", method = RequestMethod.OPTIONS)
    public void addCategories(HttpServletRequest request,
            @RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse httpServletResponse)
                    throws JsonProcessingException {
        // رؤوس CORS
        setHeaders(origin, httpServletResponse);
}

والنتيجة التي تم الحصول عليها هي كما يلي:

 

تمت إضافة الفئة [categorie2] إلى قاعدة البيانات. وقد خصص لها SGBD المفتاح الأساسي 1729.

18.12. وحدة التحكم [AuthenticateCorsController]

  

وحدة التحكم [AuthenticateCorsController] موجودة لتوفيرURL [/cors-authenticate] الذي يسمح باستدعاء URL [/authenticate] الموجودة بالفعل، باستخدام استعلام بين المجالات. رمزها هو التالي:


package spring.cors.server.service;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;

import spring.security.service.AuthenticateController;

@Controller
public class AuthenticateCorsController extends AbstractCorsController {
    @Autowired
    private AuthenticateController authenticateController;

    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.GET)
    @ResponseBody
    public String authenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) throws JsonProcessingException {
        // رؤوس CORS
        setHeaders(origin, response);
        // طريقة الأصل
        return authenticateController.authenticate();
    }

    @RequestMapping(value = "/cors-authenticate", method = RequestMethod.OPTIONS)
    public void corsAuthenticate(@RequestHeader(value = "Origin", required = false) String origin,
            HttpServletResponse response) {
        // رؤوس CORS
        setHeaders(origin, response);
    }

}

فيما يلي مثالان:

  • يتم عرض الإجابات بواسطة الرمز jS التالي:

function doGet(url) {
    // يتم إجراء استدعاء Ajax يدويًا
    $.ajax({
        headers : {
            'Authorization':'Basic '+authorizationCode
        },
        url : baseUrl + url,
        type : 'GET',
        dataType : 'text',
        beforeSend : function() {
        },
        success : function(data) {
            // نتيجة نصية
            response.text(data);
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // خطأ في النظام
            response.text(JSON.stringify(jqXHR.statusCode()));
        }
    })
}
  • يتم عرض الإجابة [1] بواسطة السطر 14 من الدالة [success
  • يتم عرض الإجابة [2] بواسطة السطر 20 من الدالة [error]. تقوم الدالة [JSON.stringify] بإنشاء السلسلة jSON من الكائن [jqXHR.statusCode()] الذي يمثل الكائن الذي يغلف الخطأ الذي حدث. لا يقدم هذا الكائن سوى القليل من المعلومات. من الممكن استخدام طرق أخرى للكائن [jqXHR] للحصول، على سبيل المثال، على الرؤوس HTTP التي يعيدها الخادم؛

18.13. Conclusion

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


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

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

    @Autowired
    private DispatcherServlet dispatcherServlet;

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