السلام عليكم، في هذا الدرس سنتحدث عن كيفية بناء خاصية Forgot Password و Reset Password في تطبيق Backend باستخدام Spring Boot.
الآلية بسيطة جداً:
Forgot Password ويرسل معه اسم المستخدمالمشكلة في حفظ بيانات التوكن والتحقق في جدول User Account أنها تنتهك مبدأ Single Responsibility.
لذلك نحتاج إلى إنشاء entity منفصل باسم PasswordResetToken:
@Entity
@Table(name = "password_reset_token")
public class PasswordResetToken {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false
اشترك في النشرة البريدية
دروس جديدة، مقالات، وأدوات مباشرة لبريدك.
المكونات الأساسية:
| المكون | الوصف |
|---|---|
token | سلسلة نصية تمثل الكود أو التوكن |
user | علاقة واحد-لواحد مع جدول UserAccount |
expiryDate | تاريخ انتهاء صلاحية التوكن (مثلاً 15 دقيقة) |
unique = true | لضمان أن كل مستخدم له توكن واحد فقط |
عندما تقوم بتشغيل التطبيق، سيقوم Hibernate بإنشاء الجدول تلقائياً:
CREATE TABLE password_reset_token (
id UUID PRIMARY KEY,
token VARCHAR(255) NOT NULL UNIQUE,
user_id UUID NOT NULL,
expiry_date TIMESTAMP NOT NULL,
CONSTRAINT fk_user_id FOREIGN KEY (user_id)
REFERENCES user_account(id)
)الخطوة التالية هي إنشاء Repository للتعامل مع قاعدة البيانات:
@Repository
public interface PasswordResetTokenRepository
extends JpaRepository<PasswordResetToken, UUID> {
Optional<PasswordResetToken> findByToken(String token);
Optional<PasswordResetToken> findByUser(UserAccount user);
}سننشئ method في AuthService باسم initiatePasswordReset:
@Service
@Transactional
public class AuthService {
@Autowired
private UserAccountRepository userRepository;
@Autowired
private PasswordResetTokenRepository tokenRepository;
@Autowired
private EmailService emailService;
@Autowired
private PasswordEncoder passwordEncoder;
public boolean initiatePasswordReset(String userName) {
// البحث عن المستخدم
UserAccount user = userRepository.findByUserName(userName)
.orElseThrow(() -> new CustomResponseException(
HttpStatus.NOT_FOUND,
"User not found"
));
// حذف أي توكن سابق للمستخدم
tokenRepository.findByUser(user).ifPresent(tokenRepository::delete);
// إنشاء التوكن
String token = UUID.randomUUID().toString();
LocalDateTime expiryDate = LocalDateTime.now().plusMinutes(15);
// حفظ التوكن
PasswordResetToken resetToken = new PasswordResetToken();
resetToken.setToken(token);
resetToken.setExpiryDate(expiryDate);
resetToken.setUser(user);
tokenRepository.save(resetToken);
// إرسال البريد الإلكتروني
emailService.sendPasswordResetEmail(user.getEmail(), token);
return true;
}
}ما يحدث هنا:
userNameNOT_FOUNDUUIDPasswordResetToken في قاعدة البياناتفي EmailService، نضيف method جديد لإرسال بريد إعادة تعيين كلمة المرور:
@Service
public class EmailService {
@Autowired
private JavaMailSender mailSender;
public void sendPasswordResetEmail(String email, String token) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("noreply@app.com");
message.setTo(email);
message.setSubject("Reset Your Password");
message.setText(
"Click the link below to reset your password:\n" +
"http://localhost:3000/reset-password?token=" + token
);
mailSender.send(message);
}
}الآن ننشئ Controller للتعامل مع طلب Forgot Password:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/forgot-password/{userName}")
public ResponseEntity<String> forgotPassword(
@PathVariable String userName) {
authService.initiatePasswordReset(userName);
return ResponseEntity.ok("Password reset email sent successfully");
}
}لا بد من إضافة الـ endpoint إلى قائمة البيانات العامة (Whitelist):
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/auth/forgot-password/**").permitAll()
.antMatchers("/auth/reset-password").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
return http.build();
}
}عند إرسال طلب:
POST /auth/forgot-password/testuserيجب أن تتلقى رسالة بريد إلكترونية تحتوي على:
Reset Your Password
Click the link below to reset your password:
http://localhost:8080/auth/reset-password?token=550e8400-e29b-41d4-a716-446655440000
الآن نحتاج إلى إنشاء method لإعادة تعيين كلمة المرور فعلياً.
أولاً، ننشئ DTO لاستقبال البيانات من المستخدم:
public record ResetPasswordRequest(
String token,
String newPassword
) {}ثم نضيف method في AuthService:
public boolean resetPassword(ResetPasswordRequest request) {
// البحث عن التوكن
PasswordResetToken resetToken = tokenRepository
.findByToken(request.token())
.orElseThrow(() -> new CustomResponseException(
HttpStatus.BAD_REQUEST,
"Invalid token"
));
// التحقق من أن التوكن لم ينتهي
boolean isTokenExpired = resetToken.getExpiryDate()
.isBefore(LocalDateTime.now());
if (isTokenExpired) {
tokenRepository.delete(resetToken);
throw new CustomResponseException(
HttpStatus.BAD_REQUEST,
"Token has expired. Request a new one"
);
}
// تحديث كلمة المرور
UserAccount user = resetToken.getUser();
user.setPassword(passwordEncoder.encode(request.newPassword()));
userRepository.save(user);
// حذف التوكن بعد استخدامه
tokenRepository.delete(resetToken);
return true;
}خطوات العملية:
PasswordEncoderننشئ endpoint آخر في Controller لاستقبال طلب إعادة تعيين كلمة المرور:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/reset-password")
public ResponseEntity<String> resetPassword(
@RequestBody ResetPasswordRequest request) {
authService.resetPassword(request);
return ResponseEntity.ok("Password reset successfully");
}
}نرسل طلب مع البيانات التالية:
{
"token": "550e8400-e29b-41d4-a716-446655440000",
"newPassword": "NewPassword123"
}إذا كان التوكن صحيحاً ولم تنته صلاحيته، ستحصل على الرد:
Password reset successfully
الآن يمكنك تسجيل الدخول بكلمة المرور الجديدة!
إليك رسم توضيحي لمسار العملية من البداية إلى النهاية:
┌──────────────────────────────────────────┐
│ User يطلب Forgot Password │
│ (POST /auth/forgot-password/username) │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ التحقق من وجود المستخدم │
│ إنشاء Password Reset Token │
│ حفظ في قاعدة البيانات │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ إرسال بريد إلكتروني مع رابط التوكن │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ User يستقبل البريد ويضغط على الرابط │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ User يرسل كلمة مرور جديدة │
│ (POST /auth/reset-password) │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ التحقق من التوكن والتاريخ │
│ تحديث كلمة المرور │
│ حذف التوكن المستخدم │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ رسالة نجاح للمستخدم │
└──────────────────────────────────────────┘
PasswordEncoder لتشفير كلمات المرور