انتقل إلى المحتوى الرئيسي

الفصل 3: JWT (JSON Web Tokens)

مقدمة​

الجلسات مثالية لتطبيقات الويب التقليدية، ولكن API REST تحتاج إلى أن تكون stateless (بدون حالة). JWT (JSON Web Token) هو الحل القياسي لمصادقة طلبات API دون تخزين الجلسة على جانب الخادم.


🎯 أهداف الفصل​

في نهاية هذا الفصل، ستعرف:

  1. ✅ فهم كيفية عمل JWT
  2. ✅ فك شفرة هيكل التوكن
  3. ✅ توليد والتحقق من صحة التوكنات
  4. ✅ تنفيذ فلتر مصادقة JWT
  5. ✅ إدارة التوكنات المتجددة

1. لماذا JWT؟​

الجلسة مقابل JWT​

الجانبالجلسةJWT
التخزينالخادم (الذاكرة/قاعدة البيانات)العميل (localStorage/cookie)
القابلية للتوسعصعب (الجلسات اللاصقة)سهل (بدون حالة)
المحمولمعقدمثالي
الخدمات المصغرةإشكاليمثالي

تدفق JWT​


2. هيكل JWT​

الأجزاء الثلاثة​

يتكون JWT من 3 أجزاء مفصولة بنقاط:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
└──────────── HEADER ──────────────────┴────────────────────── PAYLOAD ────────────────────────────────┴── SIGNATURE ──

Header (الرأس)​

{
"alg": "HS256", // خوارزمية التوقيع
"typ": "JWT" // نوع التوكن
}

مشفرة بBase64: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload (الحمولة)​

{
"sub": "1234567890", // الموضوع (معرف المستخدم)
"name": "John Doe", // مطالبات مخصصة
"email": "john@example.com",
"roles": ["ROLE_USER"],
"iat": 1516239022, // وقت الإصدار (الطابع الزمني)
"exp": 1516242622 // انتهاء الصلاحية (الطابع الزمني)
}

Signature (التوقيع)​

HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
warning

الحمولة مشفرة، ليست مشفرة! لا تضع أبدًا بيانات حساسة (كلمة مرور، بطاقة بنكية...).


3. التنفيذ مع Spring Boot​

3.1 التبعيات​

<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JJWT (Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>

3.2 التكوين​

# application.properties
jwt.secret=مفتاحك السري الطويل والمعقد للغاية مع 256 بت على الأقل
jwt.expiration=86400000
jwt.refresh-expiration=604800000
@Configuration
@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtProperties {
private String secret;
private long expiration; // 24 ساعة بالمللي ثانية
private long refreshExpiration; // 7 أيام بالمللي ثانية
}

3.3 JwtService​

@Service
@RequiredArgsConstructor
public class JwtService {

private final JwtProperties jwtProperties;

private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecret());
return Keys.hmacShaKeyFor(keyBytes);
}

// توليد Access Token
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}

public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
.signWith(getSigningKey())
.compact();
}

// توليد Refresh Token
public String generateRefreshToken(UserDetails userDetails) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + jwtProperties.getRefreshExpiration()))
.signWith(getSigningKey())
.compact();
}

// استخراج اسم المستخدم
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

// استخراج مطالبة
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}

// التحقق من صحة التوكن
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}

3.4 JwtAuthenticationFilter​

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

// استرجاع رأس Authorization
final String authHeader = request.getHeader("Authorization");

// التحقق من تنسيق Bearer
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

// استخراج التوكن
final String jwt = authHeader.substring(7);
final String username = jwtService.extractUsername(jwt);

// التحقق إذا لم يكن مصادقًا بالفعل
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

// تحميل المستخدم
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

// التحقق من صحة التوكن
if (jwtService.isTokenValid(jwt, userDetails)) {

// إنشاء المصادقة
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

// التخزين في SecurityContext
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}

filterChain.doFilter(request, response);
}
}

3.5 SecurityConfig لـ JWT​

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // تعطيل CSRF لـ API REST
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}

4. وحدة تحكم المصادقة​

DTOs​

// طلب تسجيل الدخول
@Data
public class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
}

// طلب التسجيل
@Data
public class RegisterRequest {
@NotBlank
@Size(min = 3, max = 50)
private String username;

@NotBlank
@Email
private String email;

@NotBlank
@Size(min = 6)
private String password;
}

// استجابة مع التوكنات
@Data
@Builder
public class AuthResponse {
private String accessToken;
private String refreshToken;
private String username;
private List<String> roles;
}

// طلب التوكن المتجدد
@Data
public class RefreshTokenRequest {
@NotBlank
private String refreshToken;
}

AuthController​

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

private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtService jwtService;
private final UserService userService;

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
// المصادقة
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);

// تحميل المستخدم
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());

// توليد التوكنات
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);

return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.username(userDetails.getUsername())
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.build());
}

@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
// إنشاء المستخدم
User user = userService.registerUser(
request.getUsername(),
request.getEmail(),
request.getPassword()
);

// تحميل UserDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());

// توليد التوكنات
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);

return ResponseEntity.status(HttpStatus.CREATED)
.body(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.username(user.getUsername())
.roles(List.of("ROLE_USER"))
.build());
}

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@Valid @RequestBody RefreshTokenRequest request) {
String username = jwtService.extractUsername(request.getRefreshToken());
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (jwtService.isTokenValid(request.getRefreshToken(), userDetails)) {
String accessToken = jwtService.generateToken(userDetails);

return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(request.getRefreshToken()) // إعادة الاستخدام
.username(username)
.roles(userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.build());
}

return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}

5. اختبار باستخدام cURL​

تسجيل الدخول​

# تسجيل الدخول
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"password"}'

# استجابة:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"username": "user",
"roles": ["ROLE_USER"]
}

الوصول المحمي​

# بدون توكن
curl http://localhost:8080/api/users
# 401 Unauthorized

# مع توكن
curl http://localhost:8080/api/users \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
# 200 OK

توكن المتجدد​

curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"eyJhbGciOiJIUzI1NiJ9..."}'

6. أفضل الممارسات​

تخزين التوكن (الواجهة الأمامية)​

// ✅ httpOnly Cookie (الأكثر أمانًا)
// يُدار بواسطة الخادم

// ⚠️ localStorage (عرضة لـ XSS)
localStorage.setItem('token', response.accessToken);

// ⚠️ sessionStorage (أفضل من localStorage)
sessionStorage.setItem('token', response.accessToken);

مدة الحياة​

Access Token:  15 دقيقة - 1 ساعة (قصير)
Refresh Token: 7 أيام - 30 يومًا (طويل)

الإلغاء​

// الخيار 1: القائمة السوداء في قاعدة البيانات
@Entity
public class RevokedToken {
private String tokenId;
private LocalDateTime revokedAt;
}

// الخيار 2: إصدار المستخدم
// تغيير user.tokenVersion يلغي جميع التوكنات القديمة

7. ملخص​

هيكل JWT​

HEADER.PAYLOAD.SIGNATURE

المكونات الرئيسية​

المكونالدور
JwtServiceتوليد والتحقق من صحة التوكنات
JwtAuthenticationFilterيعترض ويعالج التوكنات
AuthControllerنقاط النهاية login/register/refresh

التدفق الكامل​

1. POST /api/auth/login → Access + Refresh tokens
2. GET /api/data (Bearer token) → البيانات
3. انتهاء صلاحية التوكن → POST /api/auth/refresh → توكن وصول جديد

🎯 الخطوة التالية​

JWT مثالي للمصادقة المباشرة، ولكن في بعض الأحيان نريد تفويض المصادقة لطرف ثالث (Google، GitHub...). هذا هو دور OAuth2.

➡️ الفصل 4: OAuth2 وتسجيل الدخول الاجتماعي


⬅️ العودة إلى الوحدة 8