دليل شامل لإعداد المصادقة والترخيص باستخدام JWT في تطبيقات Java Spring Boot

دقائق القراءة: 7

مقدمة إلى المصادقة والترخيص باستخدام JWT في Spring Boot

في عالم تطوير الويب الحديث، يعد تأمين واجهات برمجة التطبيقات (APIs) أمرًا بالغ الأهمية. توفر تقنية JSON Web Tokens (JWT) حلاً فعالاً وقويًا للمصادقة (Authentication) والترخيص (Authorization) في التطبيقات الموزعة. في هذا الدليل الشامل، سنستعرض كيفية دمج JWT في تطبيقات Java Spring Boot، بدءًا من الأساسيات النظرية وصولاً إلى التطبيق العملي خطوة بخطوة. سنركز على أفضل الممارسات الأمنية لضمان حماية بيانات المستخدم وتوفير تجربة آمنة وموثوقة.

أساسيات عمل JSON Web Tokens (JWT)

JWT، أو JSON Web Tokens (التعريف الرسمي RFC 7519)، هو معيار مفتوح يستخدم بشكل أساسي لتأمين واجهات برمجة التطبيقات من نوع REST APIs. على الرغم من كونه تقنية حديثة نسبيًا، إلا أنه اكتسب شعبية سريعة بفضل مرونته وقدرته على العمل بلا حالة (stateless).

كيف تعمل عملية المصادقة باستخدام JWT؟

  1. إرسال بيانات الاعتماد: يبدأ العميل (الواجهة الأمامية) بإرسال بيانات اعتماده (مثل اسم المستخدم وكلمة المرور في تطبيق الويب الخاص بنا) إلى الخادم.
  2. التحقق من البيانات وتوليد الرمز: يقوم الخادم (تطبيق Spring Boot في حالتنا) بالتحقق من صحة هذه البيانات. إذا كانت صالحة، يقوم الخادم بتوليد JWT فريد وإعادته إلى العميل.
  3. تضمين الرمز في الطلبات اللاحقة: بعد هذه الخطوة، يجب على العميل تضمين هذا الرمز في رأس طلبات HTTP اللاحقة، وتحديداً في رأس Authorization header بالصيغة: Bearer TOKEN.
  4. التحقق من الرمز والترخيص: يقوم الخادم بالتحقق من صلاحية هذا الرمز في كل طلب. بناءً على صحة الرمز، يقوم الخادم بترخيص الطلب أو رفضه. يمكن للرمز أيضًا تخزين أدوار المستخدم (user roles) واستخدامها لترخيص الطلبات بناءً على الصلاحيات الممنوحة.

تطبيق JWT في Spring Boot: دليل خطوة بخطوة

الآن، دعنا ننتقل إلى الجانب العملي ونرى كيف يمكننا تطبيق آلية تسجيل الدخول وحفظ المستخدمين باستخدام JWT في تطبيق Spring Boot حقيقي.

الاعتماديات الضرورية (Dependencies)

للبدء، نحتاج إلى إضافة بعض الاعتماديات إلى ملف pom.xml الخاص بمشروع Maven. الصورة التالية توضح قائمة الاعتماديات التي يستخدمها كود المثال. تجدر الإشارة إلى أن الاعتماديات الأساسية مثل Spring Boot و Hibernate لم يتم تضمينها في هذه الصورة، ويُفترض أنها موجودة بالفعل في مشروعك.

قائمة اعتماديات Maven الضرورية لتطبيق JWT في Spring Boot

حفظ المستخدمين بشكل آمن

سنبدأ بإنشاء وحدات تحكم (controllers) لحفظ المستخدمين بشكل آمن ومصادقتهم بناءً على اسم المستخدم وكلمة المرور. لدينا كيان نموذج يسمى User. إنه فئة كيان بسيطة تتطابق مع جدول USER في قاعدة البيانات. يمكنك استخدام أي خصائص تحتاجها اعتمادًا على تطبيقك.

نموذج كيان المستخدم (User Entity) في Spring Boot

نحتاج أيضًا إلى فئة UserRepository بسيطة لحفظ المستخدمين. يجب علينا تجاوز طريقة findByUsername لأننا سنستخدمها في عملية المصادقة:


public interface UserRepository extends JpaRepository < User , String > {
    User findByUsername (String username) ;
}

تشفير كلمات المرور باستخدام BCrypt

من الضروري عدم تخزين كلمات المرور كنص عادي في قاعدة البيانات، لأن العديد من المستخدمين يميلون إلى استخدام نفس كلمة المرور لمواقع متعددة. هناك العديد من خوارزميات التجزئة (hashing algorithms) المختلفة، ولكن BCrypt هي الأكثر شيوعًا والأكثر توصية كطريقة آمنة للتجزئة.

لتجزئة كلمة المرور، سنقوم بتعريف bean من نوع BCryptPasswordEncoder في فئة @SpringBootApplication الرئيسية وتضمينها على النحو التالي:


@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder () {
    return new BCryptPasswordEncoder();
}

سنستدعي طرق هذا bean عندما نحتاج إلى تجزئة كلمة مرور. نحتاج أيضًا إلى UserController لحفظ المستخدمين. نقوم بإنشاء وحدة التحكم، وتضمينها بالتعليق التوضيحي @RestController، وتحديد التعيين (mapping) المقابل.

في تطبيقنا، نقوم بحفظ المستخدم بناءً على كائن DTO (Data Transfer Object) يتم تمريره من الواجهة الأمامية. يمكنك أيضًا تمرير كائن User مباشرة في @RequestBody. بعد تمرير كائن DTO، نقوم بتشفير حقل كلمة المرور باستخدام BCrypt bean الذي أنشأناه سابقًا. يمكن القيام بذلك أيضًا في وحدة التحكم، ولكن من الأفضل وضع هذا المنطق في فئة الخدمة (service class).


@Transactional(rollbackFor = Exception.class)
public String saveDto (UserDto userDto) {
    userDto.setPassword(bCryptPasswordEncoder
        .encode(userDto.getPassword()));
    return save( new User(userDto)).getId();
}

مرشح المصادقة (Authentication Filter)

نحتاج إلى المصادقة للتأكد من أن المستخدم هو حقًا من يدعي أنه هو. سنستخدم زوج اسم المستخدم/كلمة المرور الكلاسيكي لتحقيق ذلك. إليك الخطوات لتطبيق المصادقة:

  1. إنشاء مرشح المصادقة الخاص بنا الذي يوسع UsernamePasswordAuthenticationFilter.
  2. إنشاء فئة تكوين الأمان التي توسع WebSecurityConfigurerAdapter وتطبيق المرشح.

مرشحات Spring Security هي العمود الفقري لآلية الأمان. هذه الفئة توسع UsernamePasswordAuthenticationFilter، وهي الفئة الافتراضية لمصادقة كلمة المرور في Spring Security. نوسعها لتعريف منطق المصادقة المخصص لدينا.

نقوم باستدعاء طريقة setFilterProcessesUrl في المنشئ (constructor). تحدد هذه الطريقة عنوان URL الافتراضي لتسجيل الدخول إلى المعامل المقدم. إذا قمت بإزالة هذا السطر، يقوم Spring Security بإنشاء نقطة النهاية /login افتراضيًا. يحدد هذا نقطة نهاية تسجيل الدخول لنا، ولهذا السبب لن نحدد نقطة نهاية تسجيل الدخول في وحدة التحكم الخاصة بنا بشكل صريح. بعد هذا السطر، ستكون نقطة نهاية تسجيل الدخول لدينا هي /api/services/controller/user/login. يمكنك استخدام هذه الوظيفة للحفاظ على الاتساق مع نقاط النهاية الخاصة بك.

نقوم بتجاوز طريقتي attemptAuthentication و successfulAuthentication من فئة UsernamePasswordAuthenticationFilter. تعمل دالة attemptAuthentication عندما يحاول المستخدم تسجيل الدخول إلى تطبيقنا. تقرأ بيانات الاعتماد، وتنشئ كائن POJO للمستخدم منها، ثم تتحقق من بيانات الاعتماد للمصادقة. نمرر اسم المستخدم وكلمة المرور وقائمة فارغة. تمثل القائمة الفارغة الصلاحيات (roles)، ونتركها كما هي لأننا لا نملك أي أدوار في تطبيقنا بعد.

إذا كانت المصادقة ناجحة، تعمل طريقة successfulAuthentication. يتم تمرير معاملات هذه الطريقة بواسطة Spring Security خلف الكواليس. تعيد طريقة attemptAuthentication كائن Authentication object يحتوي على الصلاحيات التي مررناها أثناء المحاولة. نريد إعادة رمز (token) للمستخدم بعد نجاح المصادقة، لذلك نقوم بإنشاء الرمز باستخدام اسم المستخدم، السر (secret)، وتاريخ انتهاء الصلاحية.

تعريف الثوابت الأمنية (SECRET و EXPIRATION_DATE)

نحتاج الآن إلى تعريف الثوابت SECRET و EXPIRATION_DATE. نقوم بإنشاء فئة لتكون حاوية لثوابتنا. يمكنك تعيين السر إلى ما تريد، ولكن أفضل الممارسات هي جعل مفتاح السر طويلًا مثل التجزئة الخاصة بك. نستخدم خوارزمية HS256 في هذا المثال، لذا فإن مفتاح السر الخاص بنا هو 256 بت / 32 حرفًا. تم تعيين وقت انتهاء الصلاحية إلى 15 دقيقة، لأنه أفضل ممارسة ضد هجمات القوة الغاشمة على مفتاح السر. يتم تحديد الوقت بالمللي ثانية.

لقد قمنا بإعداد مرشح المصادقة الخاص بنا، ولكنه ليس نشطًا بعد. نحتاج أيضًا إلى مرشح ترخيص (Authorization filter)، ثم سنقوم بتطبيقهما معًا من خلال فئة التكوين.

مرشح الترخيص (Authorization Filter)

تقوم طريقة doFilterInternal باعتراض الطلبات ثم تتحقق من رأس Authorization header. إذا لم يكن الرأس موجودًا أو لم يبدأ بـ BEARER، فإنه ينتقل إلى سلسلة المرشحات. إذا كان الرأس موجودًا، يتم استدعاء طريقة getAuthentication. تتحقق getAuthentication من JWT، وإذا كان الرمز صالحًا، فإنه يعيد رمز وصول (access token) سيستخدمه Spring داخليًا. يتم بعد ذلك حفظ هذا الرمز الجديد في SecurityContext. يمكنك أيضًا تمرير الصلاحيات (Authorities) إلى هذا الرمز إذا كنت بحاجة إلى ترخيص قائم على الأدوار.

مرشحاتنا جاهزة، والآن نحتاج إلى تفعيلها بمساعدة فئة التكوين.

تكوين الأمان (Security Configuration)

نقوم بتضمين هذه الفئة بالتعليق التوضيحي @EnableWebSecurity ونوسع WebSecurityConfigurerAdapter لتطبيق منطق الأمان المخصص لدينا. نقوم بتضمين BCrypt bean الذي عرفناه سابقًا تلقائيًا (autowire). نقوم أيضًا بتضمين UserDetailsService تلقائيًا للعثور على حساب المستخدم.

الطريقة الأكثر أهمية هي تلك التي تقبل كائن HttpSecurity object. هنا نحدد نقاط النهاية الآمنة والمرشحات التي نريد تطبيقها. نقوم بتكوين CORS، ثم نسمح بجميع طلبات POST إلى عنوان URL الخاص بالتسجيل الذي عرفناه في فئة الثوابت. يمكنك إضافة مطابقات ant matchers أخرى للفلترة بناءً على أنماط URL والأدوار.

الطريقة الأخرى تقوم بتكوين AuthenticationManager لاستخدام كائن المشفر (encoder object) الخاص بنا كمشفر لكلمة المرور أثناء التحقق من بيانات الاعتماد.

اختبار المصادقة والترخيص

دعنا نرسل بعض الطلبات لاختبار ما إذا كان يعمل بشكل صحيح.

طلب GET لمورد محمي بدون رمز JWT

هنا نرسل طلب GET للوصول إلى مورد محمي. يستجيب خادمنا برمز 403 (Forbidden). هذا هو السلوك المتوقع لأننا لم نقدم رمز token في الرأس.

الآن دعنا ننشئ مستخدمًا:

إنشاء مستخدم جديد باستخدام طلب POST

لإنشاء مستخدم، نرسل طلب POST مع بيانات User DTO الخاصة بنا. سنستخدم هذا المستخدم لتسجيل الدخول والحصول على رمز وصول.

تسجيل الدخول والحصول على رمز JWT

رائع! لقد حصلنا على الرمز. بعد هذه النقطة، سنستخدم هذا الرمز للوصول إلى الموارد المحمية.

الوصول إلى مورد محمي باستخدام رمز JWT في رأس Authorization

نقدم الرمز في رأس Authorization header، والآن يُسمح لنا بالوصول إلى نقطة النهاية المحمية الخاصة بنا.

الخلاصة التقنية

لقد استعرضنا في هذا الدليل خطوات تطبيق المصادقة والترخيص باستخدام JWT في تطبيقات Spring Boot، مع التركيز على أهمية تأمين كلمات المرور باستخدام BCrypt. يتضح أن دمج JWT يوفر حلاً مرنًا وقابلاً للتوسع لإدارة جلسات المستخدمين والتحكم في الوصول إلى الموارد. إن استخدام مرشحات المصادقة والترخيص، بالإضافة إلى التكوين الدقيق للأمان، يضمن بيئة تطبيق قوية ومحمية. من الضروري دائمًا الالتزام بأفضل الممارسات الأمنية، مثل استخدام مفاتيح سرية قوية وتحديد فترات صلاحية قصيرة للرموز، لتقليل مخاطر الهجمات المحتملة. هذا النهج لا يعزز أمان التطبيق فحسب، بل يوفر أيضًا تجربة مستخدم سلسة وفعالة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *