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

الفصل 2: المصادقة الأساسية

مقدمة​

هذا الفصل يعلمك كيفية تكوين طرق المصادقة الأكثر شيوعًا: تسجيل الدخول بالنموذج و HTTP Basic. ستتعلم أيضًا كيفية تخزين المستخدمين في قاعدة البيانات بكلمات مرور آمنة.


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

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

  1. ✅ تكوين المصادقة عبر النموذج
  2. ✅ تكوين HTTP Basic لـ API
  3. ✅ تنفيذ UserDetailsService مع قاعدة البيانات
  4. ✅ استخدام BCrypt لتشفير كلمات المرور
  5. ✅ تخصيص صفحات تسجيل الدخول/الخروج

1. التكوين الأساسي​

التبعيات​

<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- لأجل الإضافات Thymeleaf + Security -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
</dependencies>

التكوين الأدنى​

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.build();
}

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

2. المستخدمون في الذاكرة (اختبار)​

للتطوير فقط​

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.build();
}

@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User.builder()
.username("user")
.password(encoder.encode("password"))
.roles("USER")
.build();

UserDetails admin = User.builder()
.username("admin")
.password(encoder.encode("admin123"))
.roles("ADMIN", "USER")
.build();

return new InMemoryUserDetailsManager(user, admin);
}

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

InMemoryUserDetailsManager هو للاختبار والتطوير فقط. في الإنتاج، استخدم قاعدة بيانات!


3. المستخدمون في قاعدة البيانات​

3.1 كيان المستخدم​

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String username;

@Column(nullable = false)
private String password;

@Column(unique = true, nullable = false)
private String email;

private boolean enabled = true;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
}

3.2 كيان الدور​

@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String name; // ROLE_USER, ROLE_ADMIN

public Role(String name) {
this.name = name;
}
}

3.3 المستودع​

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}

3.4 تفاصيل المستخدم المخصصة​

public class CustomUserDetails implements UserDetails {

private final User user;

public CustomUserDetails(User user) {
this.user = user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getUsername();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return user.isEnabled();
}

// للوصول إلى كيان المستخدم الكامل
public User getUser() {
return user;
}
}

3.5 خدمة تفاصيل المستخدم المخصصة​

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"المستخدم غير موجود: " + username
));

return new CustomUserDetails(user);
}
}

3.6 التكوين مع قاعدة البيانات​

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService userDetailsService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/register", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID")
.permitAll()
)
.userDetailsService(userDetailsService)
.build();
}

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

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

4. خدمة التسجيل​

خدمة المستخدم​

@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public User registerUser(String username, String email, String password) {
// التحقق
if (userRepository.existsByUsername(username)) {
throw new RuntimeException("اسم المستخدم هذا موجود بالفعل");
}
if (userRepository.existsByEmail(email)) {
throw new RuntimeException("هذا البريد الإلكتروني مستخدم بالفعل");
}

// إنشاء المستخدم
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password)); // ← التشفير!
user.setEnabled(true);

// تعيين الدور USER افتراضيًا
Role userRole = roleRepository.findByName("ROLE_USER")
.orElseThrow(() -> new RuntimeException("دور ROLE_USER غير موجود"));
user.getRoles().add(userRole);

return userRepository.save(user);
}

@Transactional
public void promoteToAdmin(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("المستخدم غير موجود"));

Role adminRole = roleRepository.findByName("ROLE_ADMIN")
.orElseThrow(() -> new RuntimeException("دور ROLE_ADMIN غير موجود"));

user.getRoles().add(adminRole);
userRepository.save(user);
}
}

5. HTTP Basic لـ API​

التكوين​

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // تعطيل لـ API REST
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
)
.httpBasic(Customizer.withDefaults()) // ← HTTP Basic
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.build();
}
}

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

# بدون مصادقة
curl http://localhost:8080/api/users
# 401 Unauthorized

# مع المصادقة
curl -u user:password http://localhost:8080/api/users
# 200 OK + بيانات

# مع ترويسة صريحة
curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" http://localhost:8080/api/users
حذر

HTTP Basic يرسل بيانات الاعتماد مشفرة بـ Base64 (ليست مشفرة!). استخدم دائمًا HTTPS في الإنتاج!


6. صفحات تسجيل الدخول المخصصة​

المتحكم​

@Controller
public class AuthController {

@GetMapping("/login")
public String loginPage() {
return "login"; // templates/login.html
}

@GetMapping("/register")
public String registerPage(Model model) {
model.addAttribute("user", new RegisterRequest());
return "register";
}

@PostMapping("/register")
public String register(@Valid @ModelAttribute("user") RegisterRequest request,
BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
return "register";
}

try {
userService.registerUser(
request.getUsername(),
request.getEmail(),
request.getPassword()
);
redirectAttributes.addFlashAttribute("success", "تم التسجيل بنجاح!");
return "redirect:/login";
} catch (Exception e) {
result.rejectValue("username", "error", e.getMessage());
return "register";
}
}
}

قالب تسجيل الدخول (Thymeleaf)​

<!-- templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>تسجيل الدخول</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<div class="login-container">
<h1>تسجيل الدخول</h1>

<!-- الرسائل -->
<div th:if="${param.error}" class="alert alert-danger">
اسم المستخدم أو كلمة المرور غير صحيحة.
</div>
<div th:if="${param.logout}" class="alert alert-info">
لقد تم تسجيل خروجك.
</div>
<div th:if="${success}" class="alert alert-success" th:text="${success}"></div>

<!-- النموذج -->
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username">اسم المستخدم</label>
<input type="text" id="username" name="username" required>
</div>

<div class="form-group">
<label for="password">كلمة المرور</label>
<input type="password" id="password" name="password" required>
</div>

<div class="form-group">
<label>
<input type="checkbox" name="remember-me"> تذكرني
</label>
</div>

<button type="submit">تسجيل الدخول</button>
</form>

<p>ليس لديك حساب بعد؟ <a th:href="@{/register}">سجل الآن</a></p>
</div>
</body>
</html>

تكوين تذكرني​

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// ... تكوينات أخرى
.rememberMe(remember -> remember
.key("uniqueAndSecretKey") // مفتاح التوقيع
.tokenValiditySeconds(86400 * 7) // 7 أيام
.userDetailsService(userDetailsService)
)
.build();
}

7. الوصول إلى المستخدم المتصل​

في المتحكم​

@RestController
@RequestMapping("/api")
public class ProfileController {

// الطريقة 1: Principal
@GetMapping("/me")
public String getCurrentUser(Principal principal) {
return "متصل كـ: " + principal.getName();
}

// الطريقة 2: Authentication
@GetMapping("/profile")
public Map<String, Object> getProfile(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
User user = userDetails.getUser();

return Map.of(
"username", user.getUsername(),
"email", user.getEmail(),
"roles", authentication.getAuthorities()
);
}

// الطريقة 3: @AuthenticationPrincipal
@GetMapping("/details")
public User getDetails(@AuthenticationPrincipal CustomUserDetails userDetails) {
return userDetails.getUser();
}

// الطريقة 4: SecurityContextHolder (في أي مكان)
@GetMapping("/static")
public String getFromContext() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth.getName();
}
}

في Thymeleaf​

<!-- مع thymeleaf-extras-springsecurity6 -->
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<!-- عرض اسم المستخدم -->
<p>مرحبًا، <span sec:authentication="name">المستخدم</span>!</p>

<!-- عرض حسب الدور -->
<div sec:authorize="hasRole('ADMIN')">
<a href="/admin">الإدارة</a>
</div>

<div sec:authorize="hasRole('USER')">
<a href="/profile">ملفي الشخصي</a>
</div>

<!-- عرض إذا كان مصادقًا -->
<div sec:authorize="isAuthenticated()">
<form th:action="@{/logout}" method="post">
<button type="submit">تسجيل الخروج</button>
</form>
</div>

<!-- عرض إذا كان غير مصادق -->
<div sec:authorize="isAnonymous()">
<a href="/login">تسجيل الدخول</a>
</div>

8. إدارة الأخطاء​

معالج AccessDeniedHandler مخصص​

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException ex) throws IOException {

// لـ API REST
if (request.getRequestURI().startsWith("/api/")) {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\": \"الوصول مرفوض\"}");
} else {
// للصفحات الويب
response.sendRedirect("/access-denied");
}
}
}

التكوين​

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// ... تكوينات أخرى
.exceptionHandling(ex -> ex
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint((request, response, authException) -> {
if (request.getRequestURI().startsWith("/api/")) {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"غير مصادق\"}");
} else {
response.sendRedirect("/login");
}
})
)
.build();
}

9. البيانات الأولية​

DataInitializer​

@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

private final RoleRepository roleRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Override
@Transactional
public void run(String... args) {
// إنشاء الأدوار إذا كانت غائبة
if (roleRepository.count() == 0) {
roleRepository.save(new Role("ROLE_USER"));
roleRepository.save(new Role("ROLE_ADMIN"));
roleRepository.save(new Role("ROLE_MANAGER"));
}

// إنشاء مدير افتراضي إذا لم يكن هناك مستخدم
if (userRepository.count() == 0) {
Role adminRole = roleRepository.findByName("ROLE_ADMIN").orElseThrow();
Role userRole = roleRepository.findByName("ROLE_USER").orElseThrow();

User admin = new User();
admin.setUsername("admin");
admin.setEmail("admin@example.com");
admin.setPassword(passwordEncoder.encode("admin123"));
admin.setEnabled(true);
admin.setRoles(Set.of(adminRole, userRole));

userRepository.save(admin);
}
}
}

10. الملخص​

النقاط الرئيسية​

المفهومالوصف
Form Loginالمصادقة عبر نموذج الويب
HTTP Basicترويسة Authorization لـ API
UserDetailsServiceواجهة لتحميل المستخدمين
PasswordEncoderتشفير كلمات المرور (BCrypt)
CustomUserDetailsتكييف User مع UserDetails

تكوين نموذجي​

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout.permitAll())
.build();
}

قائمة التحقق​

✅ تشفير كلمات المرور باستخدام BCrypt
✅ تنفيذ UserDetailsService لقاعدة البيانات
✅ إنشاء CustomUserDetails إذا لزم الأمر
✅ تكوين صفحات تسجيل الدخول/الخروج
✅ إدارة أخطاء الوصول
✅ إنشاء الأدوار والمستخدمين الأوليين

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

المصادقة عبر الجلسة مثالية لتطبيقات الويب، ولكن بالنسبة لـ API REST الحديثة، JWT هو المعيار.

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


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