spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
فقط لتوضيح بعض المعلومات الأساسية، لدينا مقدمة رائعة، مقدمة من jwt.io ! دعونا نلقي نظرة:
JSON Web Token (JWT) هو معيار مفتوح (RFC 7519) يحدد طريقة مدمجة ومكتفية ذاتيًا لنقل المعلومات بشكل آمن بين الأطراف ككائن JSON. يمكن التحقق من هذه المعلومات والوثوق بها لأنها موقعة رقميًا. يمكن توقيع JWTs باستخدام سر (باستخدام خوارزمية HMAC) أو زوج مفاتيح عام/خاص باستخدام RSA.
دعونا نشرح بعض مفاهيم هذا التعريف بشكل أكبر.
مضغوط : نظرًا لصغر حجمها، يمكن إرسال JWTs عبر عنوان URL أو معلمة POST أو داخل رأس HTTP. بالإضافة إلى ذلك، الحجم الأصغر يعني أن الإرسال سريع.
مكتفية ذاتيًا : تحتوي الحمولة على جميع المعلومات المطلوبة عن المستخدم، مما يتجنب الحاجة إلى الاستعلام عن قاعدة البيانات أكثر من مرة.
فيما يلي بعض السيناريوهات التي تكون فيها رموز JSON Web Tokens مفيدة:
المصادقة : هذا هو السيناريو الأكثر شيوعًا لاستخدام JWT. بمجرد تسجيل دخول المستخدم، سيتضمن كل طلب لاحق JWT، مما يسمح للمستخدم بالوصول إلى المسارات والخدمات والموارد المسموح بها باستخدام هذا الرمز المميز. تسجيل الدخول الموحد هو ميزة تستخدم JWT على نطاق واسع في الوقت الحاضر، نظرًا لصغر حجمها وقدرتها على الاستخدام بسهولة عبر مجالات مختلفة.
تبادل المعلومات : تعد رموز JSON Web Tokens طريقة جيدة لنقل المعلومات بشكل آمن بين الأطراف. نظرًا لأنه يمكن توقيع JWTs — على سبيل المثال، باستخدام أزواج المفاتيح العامة/الخاصة — يمكنك التأكد من أن المرسلين هم من يقولون أنهم هم. بالإضافة إلى ذلك، نظرًا لأنه يتم حساب التوقيع باستخدام الرأس والحمولة، يمكنك أيضًا التحقق من عدم التلاعب بالمحتوى.
تتكون رموز الويب JSON من ثلاثة أجزاء مفصولة بنقاط (.) ، وهي:
لذلك، تبدو JWT عادةً كما يلي.
xxxxx
. yyyyy
. zzzzz
دعونا نحلل الأجزاء المختلفة.
رأس
يتكون الرأس عادةً من جزأين: نوع الرمز المميز، وهو JWT، وخوارزمية التجزئة المستخدمة، مثل HMAC SHA256 أو RSA.
على سبيل المثال:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
بعد ذلك، يتم ترميز JSON هذا باستخدام Base64Url لتكوين الجزء الأول من JWT.
الحمولة
الجزء الثاني من الرمز المميز هو الحمولة التي تحتوي على المطالبات. المطالبات عبارة عن بيانات حول كيان (المستخدم عادة) وبيانات وصفية إضافية. هناك ثلاثة أنواع من المطالبات: المطالبات المحجوزة، والعامة، والخاصة.
لاحظ أن أسماء المطالبة مكونة من ثلاثة أحرف فقط، حيث أن JWT من المفترض أن تكون مضغوطة.
المطالبات العامة : يمكن تعريفها حسب الرغبة من قبل أولئك الذين يستخدمون JWTs. ولكن لتجنب التصادمات، يجب تعريفها في IANA JSON Web Token Registry أو تعريفها على أنها URI الذي يحتوي على مساحة اسم مقاومة للتصادم.
المطالبات الخاصة : هذه هي المطالبات المخصصة التي تم إنشاؤها لمشاركة المعلومات بين الأطراف التي توافق على استخدامها.
مثال على الحمولة يمكن أن يكون:
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
يتم بعد ذلك تشفير الحمولة باستخدام Base64Url لتشكل الجزء الثاني من JSON Web Token.
إمضاء
لإنشاء جزء التوقيع، يجب عليك أخذ الرأس المشفر، والحمولة المشفرة، والسر، والخوارزمية المحددة في الرأس، وتوقيع ذلك.
على سبيل المثال، إذا كنت تريد استخدام خوارزمية HMAC SHA256، فسيتم إنشاء التوقيع بالطريقة التالية:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
يتم استخدام التوقيع للتحقق من أن مرسل JWT هو الشخص المذكور وللتأكد من عدم تغيير الرسالة على طول الطريق. وضع كل ذلك معا
الإخراج عبارة عن ثلاث سلاسل Base64 مفصولة بنقاط يمكن تمريرها بسهولة في بيئات HTML وHTTP، بينما تكون أكثر إحكاما عند مقارنتها بالمعايير المستندة إلى XML مثل SAML.
يوضح ما يلي JWT الذي يحتوي على الرأس السابق والحمولة المشفرة، وهو موقع بكلمة سر. JWT المشفرة
في المصادقة، عندما يقوم المستخدم بتسجيل الدخول بنجاح باستخدام بيانات الاعتماد الخاصة به، سيتم إرجاع رمز ويب JSON ويجب حفظه محليًا (عادةً في التخزين المحلي، ولكن يمكن استخدام ملفات تعريف الارتباط أيضًا)، بدلاً من النهج التقليدي المتمثل في إنشاء جلسة في الخادم وإرجاع ملف تعريف الارتباط.
عندما يريد المستخدم الوصول إلى مسار أو مورد محمي، يجب على وكيل المستخدم إرسال JWT، عادةً في رأس التفويض باستخدام مخطط Bearer. يجب أن يبدو محتوى الرأس كما يلي:
Authorization: Bearer <token>
هذه آلية مصادقة عديمة الحالة حيث لا يتم حفظ حالة المستخدم أبدًا في ذاكرة الخادم. ستتحقق المسارات المحمية للخادم من وجود JWT صالح في رأس التفويض، وإذا كان موجودًا، فسيتم السماح للمستخدم بالوصول إلى الموارد المحمية. نظرًا لأن JWTs مستقلة بذاتها، فإن جميع المعلومات الضرورية موجودة، مما يقلل الحاجة إلى الاستعلام عن قاعدة البيانات عدة مرات.
يتيح لك هذا الاعتماد بشكل كامل على واجهات برمجة تطبيقات البيانات عديمة الحالة وحتى تقديم طلبات للخدمات النهائية. لا يهم النطاقات التي تخدم واجهات برمجة التطبيقات الخاصة بك، لذا لن تمثل مشاركة الموارد عبر الأصل (CORS) مشكلة لأنها لا تستخدم ملفات تعريف الارتباط.
ويوضح الرسم البياني التالي هذه العملية:
أصبحت مخططات المصادقة المستندة إلى الرمز المميز تحظى بشعبية كبيرة في الآونة الأخيرة، لأنها توفر فوائد مهمة عند مقارنتها بالجلسات/ملفات تعريف الارتباط:
يجب إجراء بعض المقايضات باستخدام هذا النهج:
تدفق مصادقة JWT بسيط للغاية
من المهم ملاحظة أنه سيتم تضمين مطالبات التفويض مع رمز الوصول. لماذا هذا مهم؟ حسنًا، لنفترض أن مطالبات التفويض (على سبيل المثال، امتيازات المستخدم في قاعدة البيانات) قد تغيرت أثناء فترة حياة رمز الوصول. لن تصبح هذه التغييرات سارية حتى يتم إصدار رمز وصول جديد. في معظم الحالات، لا تعد هذه مشكلة كبيرة، لأن رموز الوصول تكون قصيرة العمر. وإلا فاتبع نمط الرمز المميز غير الشفاف.
دعونا نرى كيف يمكننا تنفيذ المصادقة المستندة إلى رمز JWT باستخدام Java وSpring، أثناء محاولة إعادة استخدام السلوك الافتراضي لأمان Spring حيثما أمكننا ذلك. يأتي إطار عمل Spring Security مع فئات المكونات الإضافية التي تتعامل بالفعل مع آليات الترخيص مثل: ملفات تعريف الارتباط للجلسة، وHTTP Basic، وHTTP Digest. ومع ذلك، فهو يفتقر إلى الدعم الأصلي لـ JWT، ونحن بحاجة إلى بذل قصارى جهدنا لإنجاحه.
يستخدم هذا العرض التوضيحي حاليًا قاعدة بيانات H2 تسمى test_db حتى تتمكن من تشغيلها بسرعة وبدون الحاجة إلى الكثير من التكوين. إذا كنت تريد الاتصال بقاعدة بيانات مختلفة، فيجب عليك تحديد الاتصال في ملف application.yml
داخل دليل الموارد. لاحظ أن hibernate.hbm2ddl.auto=create-drop
سيتم إسقاطه وإنشاء قاعدة بيانات نظيفة في كل مرة نقوم فيها بالنشر (قد ترغب في تغييرها إذا كنت تستخدم هذا في مشروع حقيقي). إليك المثال من المشروع، تعرف على مدى سهولة تبادل التعليقات على url
وخصائص dialect
لاستخدام قاعدة بيانات MySQL الخاصة بك:
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
يتم تطبيق عامل التصفية JwtTokenFilter
على كل واجهة برمجة تطبيقات ( /**
) باستثناء نقطة نهاية الرمز المميز لتسجيل الدخول ( /users/signin
) ونقطة النهاية الفردية ( /users/signup
).
يقوم هذا الفلتر بالمسؤوليات التالية:
JwtTokenProvider
وإلا فسيتم طرح استثناء المصادقة يرجى التأكد من استدعاء chain.doFilter(request, response)
عند المصادقة الناجحة. تريد معالجة الطلب للتقدم إلى عامل التصفية التالي، لأن المرشح الأخير للغاية FilterSecurityInterceptor#doFilter مسؤول عن استدعاء الطريقة فعليًا في وحدة التحكم الخاصة بك التي تتعامل مع مورد API المطلوب.
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
يضيف JwtTokenFilter
إلى DefaultSecurityFilterChain
الخاص بأمان التمهيد الربيعي.
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtTokenProvider
يتولى JwtTokenProvider
المسؤوليات التالية:
تفاصيل المستخدم
ينفذ UserDetailsService
من أجل تحديد وظيفة التحميل المخصصة الخاصة بنا. يتم استخدام واجهة UserDetailsService
لاسترداد البيانات المتعلقة بالمستخدم. يحتوي على طريقة واحدة تسمى LoadUserByUsername والتي تبحث عن كيان مستخدم بناءً على اسم المستخدم ويمكن تجاوزها لتخصيص عملية العثور على المستخدم.
يتم استخدامه بواسطة DaoAuthenticationProvider
لتحميل تفاصيل حول المستخدم أثناء المصادقة.
تكوين أمان الويب
تعمل فئة WebSecurityConfig
على توسيع WebSecurityConfigurerAdapter
لتوفير تكوين أمان مخصص.
يتم تكوين الفاصوليا التالية وإنشاء مثيل لها في هذه الفئة:
JwtTokenFilter
PasswordEncoder
أيضًا، داخل طريقة WebSecurityConfig#configure(HttpSecurity http)
سنقوم بتكوين الأنماط لتحديد نقاط نهاية API المحمية/غير المحمية. يرجى ملاحظة أننا قمنا بتعطيل حماية CSRF لأننا لا نستخدم ملفات تعريف الارتباط.
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
تأكد من تثبيت Java 8 وMaven
تفرع هذا المستودع واستنساخه
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
في متصفحك للتحقق من أن كل شيء يعمل بشكل صحيح. يمكنك تغيير المنفذ الافتراضي في ملف application.yml
server :
port : 8080
/users/me
للتحقق من عدم مصادقتك. من المفترض أن تتلقى ردًا برقم 403
مع رسالة Access Denied
نظرًا لأنك لم تقم بتعيين رمز JWT الصالح الخاص بك بعد $ curl -X GET http://localhost:8080/users/me
/users/signin
باستخدام المستخدم المسؤول الافتراضي الذي أنشأناه برمجيًا للحصول على رمز JWT صالح $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
مرة أخرى $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}