404 Not Found

404 Not Found


nginx

مصادقة OAuth2 وJWT

JWT هو جواز مرور الخدمات المصغرة—عديم الحالة، مكتفٍ ذاتياً، ويمكّن المصادقة عبر الخدمات—مما ينهي تحديات مشاركة الجلسات.

1. ما ستتعلمه


2. قصة حقيقية لمهندس بنية

(1) نقطة الألم: الجلسات تنتهي في الخدمات المصغرة

بعد أن قام Alice بتوسيع OrderFlow من مثيل واحد إلى ثلاثة مثيلات، ظهرت مشكلة جلسات: عندما يسجل المستخدم الدخول على المثيل A وتتم إعادة توجيه الطلب عبر موازن الحمل إلى المثيل B، لا تكون الجلسة موجودة، ويتم تسجيل خروج المستخدم. حاول Bob استخدام Redis لمشاركة الجلسات، لكن هذا أدخل تبعيات وتعقيداً جديدين. طلب Charlie دعم تسجيل الدخول الخارجي (Google/GitHub)، وهو ما لم تستطع حل الجلسات التقليدية توفيره على الإطلاق.

(2) حل JWT

رموز JWT تحتوي على جميع معلومات المصادقة، لذا لا يحتاج الخادم إلى تخزين الجلسات:

JAVA
// إصدار JWT عند تسجيل الدخول
String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();

// التحقق من JWT في كل طلب (عديم الحالة، بدون حاجة لجلسة)
// Spring Security يستخرج الأدوار تلقائياً من JWT claims

(3) العوائد

بعد أن استبدلت Alice الجلسات بـ JWTs: المثيلات الثلاثة لم تعد بحاجة لمشاركة جلسة، مما يتيح التوسع الأفقي بدون تكلفة؛ الرموز تُخزن على جانب العميل، مما يجعل الخادم عديم الحالة؛ ودعم تسجيل الدخول عبر OAuth 2.0 خارجي أصبح ممكناً.


3. مفاهيم OAuth 2.0 وJWT

(1) تدفق تفويض OAuth 2.0

100%
sequenceDiagram
    participant U as وكيل المستخدم<br/>(المتصفح)
    participant C as العميل<br/>(OrderFlow SPA)
    participant AS as خادم<br/>التفويض
    participant RS as خادم<br/>الموارد (API)

    U->>C: انقر "تسجيل الدخول عبر GitHub"
    C->>AS: إعادة توجيه إلى /authorize
    AS->>U: تسجيل الدخول + صفحة الموافقة
    U->>AS: موافقة
    AS->>C: إعادة توجيه مع رمز التفويض
    C->>AS: POST /token (رمز التفويض + client_secret)
    AS->>C: Access Token (JWT)
    C->>RS: GET /api/orders (Bearer token)
    RS->>RS: التحقق من توقيع JWT
    RS->>C: 200 OK + البيانات
نموذج التفويض حالات الاستخدام أنواع العميل
تدفق رمز التفويض تطبيقات الويب / SPAs خدمات خلفية + واجهة أمامية
بيانات اعتماد العميل استدعاءات بين الخدمات آلة إلى آلة
بيانات اعتماد مالك المورد (ROPC) مهمل، للاختبار فقط تطبيقات الطرف الأول
الوضع الضمني مهمل SPA قديم

(2) بنية رمز JWT

يتكون JWT من ثلاثة أجزاء، مفصولة بـ .:

TEXT
Header.Payload.Signature

eyJhbGciOiJS256.eyJzdWIiOiJib2IiLCJyb2xlIjoiQ1VTVU9NRVIifQ.signature
القسم المحتوى مثال
Header الخوارزمية + نوع الرمز {"alg":"RS256","typ":"JWT"}
Payload Claims {"sub":"bob","role":"CUSTOMER","exp":1705312000}
Signature توقيع Header+Payload تحقق سلامة توقيع RS256
Claim معياري المعنى
sub الموضوع (مُعرّف المستخدم)
iss المُصدر
aud الجمهور
exp انتهاء الصلاحية
iat صدر في (تاريخ الإصدار)
scope نطاق الصلاحيات

4. تهيئة OAuth 2.0 Resource Server

(1) التبعيات والتهيئة

(1) ▶ مثال: تبعيات pom.xml

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: تهيئة SecurityFilterChain لـ JWT

JAVA
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s ->
                s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/login").permitAll()
                .requestMatchers("/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/orders/**").authenticated()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 ->
                oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            String role = jwt.getClaimAsString("role");
            if (role == null) return List.of();
            return List.of(new SimpleGrantedAuthority("ROLE_" + role));
        });
        return converter;
    }
}

الناتج:

TEXT
// التنفيذ ناجح

5. إصدار JWT والتحقق منه

(1) تهيئة زوج مفاتيح RSA

(1) ▶ مثال: توليد زوج مفاتيح RSA

JAVA
@Configuration
public class JwtConfig {

    @Bean
    public KeyPair keyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048);
        return generator.generateKeyPair();
    }

    @Bean
    public JwtEncoder jwtEncoder(KeyPair keyPair) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
        JWKSource<SecurityContext> jwkSource =
            new ImmutableJWKSet<>(new JWKSet(rsaKey));
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtDecoder jwtDecoder(KeyPair keyPair) {
        return JwtDecoders.withPublicKey((RSAPublicKey) keyPair.getPublic());
    }
}

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: إصدار JWT عند تسجيل الدخول

JAVA
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {

    private final JwtEncoder jwtEncoder;
    private final AuthenticationManager authenticationManager;

    public AuthController(JwtEncoder jwtEncoder,
                          AuthenticationManager authenticationManager) {
        this.jwtEncoder = jwtEncoder;
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) {
        Authentication auth = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.username(), request.password()));

        Instant now = Instant.now();
        Instant expiry = now.plus(1, ChronoUnit.HOURS);

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("orderflow-service")
            .subject(auth.getName())
            .issuedAt(now)
            .expiresAt(expiry)
            .claim("role", auth.getAuthorities().stream()
                .filter(a -> a.getAuthority().startsWith("ROLE_"))
                .map(a -> a.getAuthority().replace("ROLE_", ""))
                .findFirst().orElse("CUSTOMER"))
            .build();

        String token = jwtEncoder
            .encode(JwtEncoderParameters.from(claims))
            .getTokenValue();

        return Map.of("accessToken", token);
    }

    public record LoginRequest(String username, String password) {}
}

الناتج:

TEXT
// التنفيذ ناجح

6. Claims المخصصة لـ JWT وتعيين الأدوار

(1) استراتيجية تعيين الأدوار

حقل Claim القيمة التعيين إلى صلاحية
role: "ADMIN" سلسلة دور واحدة ROLE_ADMIN
roles: ["ADMIN","CUSTOMER"] مصفوفة أدوار ROLE_ADMIN، ROLE_CUSTOMER
scope: "read write" نطاق OAuth2 SCOPE_read، SCOPE_write

(1) ▶ مثال: JwtAuthenticationConverter مخصص

JAVA
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        // دعم كل من claims "role" (مفرد) و"roles" (مصفوفة)
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        String role = jwt.getClaimAsString("role");
        if (role != null) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }

        List<String> roles = jwt.getClaimAsStringList("roles");
        if (roles != null) {
            roles.forEach(r -> authorities.add(
                new SimpleGrantedAuthority("ROLE_" + r)));
        }

        return authorities;
    });
    return converter;
}

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: استخدام معلومات JWT في المتحكم

JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @GetMapping("/my")
    public List<OrderResponse> getMyOrders(
            @AuthenticationPrincipal Jwt jwt) {
        String username = jwt.getClaimAsString("sub");
        String role = jwt.getClaimAsString("role");
        return orderService.getOrdersByUsername(username);
    }
}

الناتج:

TEXT
// التنفيذ ناجح
الطريقة الحصول على معلومات المستخدم السيناريوهات المناسبة
@AuthenticationPrincipal Jwt jwt JWT Claims الكاملة حاجة إلى Claim مخصص
@AuthenticationPrincipal UserDetails user UserDetails حاجة لمعلومات مستخدم معيارية
Principal principal getName() اسم المستخدم فقط

7. مثال شامل: التنفيذ الكامل لمصادقة JWT في OrderFlow

JAVA
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/orders/**").authenticated()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt ->
            Optional.ofNullable(jwt.getClaimAsString("role"))
                .map(role -> List.of(new SimpleGrantedAuthority("ROLE_" + role)))
                .orElse(List.of()));
        return converter;
    }

    @Bean
    public AuthenticationManager authenticationManager(
            UserDetailsService userDetailsService, PasswordEncoder encoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(encoder);
        return new ProviderManager(provider);
    }
}

// AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {

    private final JwtEncoder jwtEncoder;
    private final AuthenticationManager authManager;

    public AuthController(JwtEncoder jwtEncoder, AuthenticationManager authManager) {
        this.jwtEncoder = jwtEncoder;
        this.authManager = authManager;
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest req) {
        Authentication auth = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(req.username(), req.password()));
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("orderflow").subject(auth.getName())
            .issuedAt(now).expiresAt(now.plus(1, ChronoUnit.HOURS))
            .claim("role", auth.getAuthorities().stream()
                .map(a -> a.getAuthority().replace("ROLE_", ""))
                .findFirst().orElse("CUSTOMER"))
            .build();
        String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
        return Map.of("accessToken", token);
    }

    public record LoginRequest(String username, String password) {}
}
💻 عملية الاختبار:

BASH
# 1. تسجيل الدخول والحصول على JWT
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"bob","password":"pass123"}' | jq -r '.accessToken')

# 2. الوصول إلى واجهة API محمية باستخدام JWT
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/api/v1/orders/my

# 3. نقطة نهاية الإدارة باستخدام JWT مدير
ADMIN_TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"admin123"}' | jq -r '.accessToken')

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://localhost:8080/api/v1/admin/dashboard

❓ أسئلة شائعة

س ما الفرق الرئيسي بين JWT والجلسات؟
ج الجلسات ذات حالة (مُخزنة على الخادم)، بينما JWT عديم الحالة (رمز مكتفٍ ذاتياً). JWT مناسب للخدمات المصغرة والتوسع الأفقي، بينما الجلسات مناسبة للتطبيقات المتجانسة. لا يمكن إبطال JWT بشكل نشط (ما لم يتم تنفيذ قائمة سوداء)، بينما يمكن تدمير الجلسات فوراً.
س هل يستخدم JWT تشفيراً متماثلاً أم غير متماثل؟
ج يُوصى بالتشفير غير المتماثل (RSA/ECDSA) لبيئات الإنتاج. خادم الموارد يحتاج فقط للتحقق بالمفتاح العام؛ المفتاح الخاص يحتفظ به خادم التفويض فقط. التشفير المتماثل (HMAC) يحمل خطراً عالياً لكشف المفتاح.
س ماذا يجب فعله عند انتهاء صلاحية JWT؟
ج الأساليب الشائعة: 1) آلية رمز التحديث، باستخدام رمز وصول قصير الأجل ورمز تحديث طويل الأجل؛ 2) يكتشف العميل انتهاء الصلاحية ويُعيد التوجيه تلقائياً لصفحة تسجيل الدخول؛ 3) تعيين وقت انتهاء صلاحية معقول (1-4 ساعات).
س كيف أُبطل JWT صدر مسبقاً؟
ج لا يمكن إبطال JWT من تلقاء نفسه. الأساليب الشائعة: 1) وقت انتهاء صلاحية قصير + رمز تحديث؛ 2) قائمة سوداء للرموز (مُخزنة في Redis)؛ 3) تغيير مفتاح التوقيع لإبطال جميع الرموز القديمة.
س ما العلاقة بين خادم موارد OAuth 2.0 وخادم التفويض؟
ج خادم التفويض مسؤول عن إصدار الرموز، بينما خادم الموارد مسؤول عن التحقق من الرموز لحماية الموارد. في هذا الدرس، يعمل OrderFlow كلاهما كخادم موارد وتنفيذ مبسط لخادم التفويض. لبيئات الإنتاج، نوصي باستخدام حلول متخصصة مثل Keycloak.
س كم من المعلومات يجب تضمينها في JWT claims؟
ج اتبع مبدأ الحد الأدنى. أضف فقط مُعرّف المستخدم (sub) والدور؛ لا تضمن معلومات حساسة (مثل كلمات المرور أو أرقام الهواتف). JWT مشفر بـ Base64، لذا يمكن لأي شخص فك تشفيره؛ السلامة مضمونة فقط بالتوقيع.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): نفّذ تسجيل دخول JWT ومصادقة API لـ OrderFlow، مستبدلاً httpBasic. استخدم curl لاختبار عملية تسجيل الدخول والحصول على رمز → استخدم الرمز للوصول إلى واجهة API المحمية.

  2. تمرين متقدم (الصعوبة: ⭐⭐): نفّذ آلية Refresh Token—Access Token صالح لمدة 15 دقيقة، Refresh Token صالح لمدة 7 أيام، ويُستخدم Refresh Token للحصول على Access Token جديد.

  3. تحدٍ (الصعوبة: ⭐⭐⭐): ادمج Keycloak كخادم تفويض خارجي، حيث يعمل OrderFlow فقط كخادم موارد للتحقق من JWTs التي أصدرها Keycloak. فكّر في كيفية إنشاء علاقات ثقة بين الخدمات.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%