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

TP1 - المصادقة عبر النموذج

أهداف التمرين​

في هذا التمرين، سوف تقوم بـ:

  1. ✅ إنشاء تطبيق ويب آمن باستخدام Spring Security
  2. ✅ تنفيذ المصادقة عبر النموذج
  3. ✅ تخزين المستخدمين في قاعدة البيانات
  4. ✅ إدارة الأدوار USER و ADMIN
  5. ✅ تخصيص صفحات تسجيل الدخول/الخروج

المدة المقدرة: 1 ساعة و30 دقيقة


السياق​

أنت تقوم بإنشاء تطبيق لإدارة المهام مع:

  • صفحة رئيسية عامة
  • لوحة تحكم متاحة للمستخدمين المسجلين
  • صفحة إدارة مخصصة للمسؤولين

الجزء 1: إعداد المشروع​

1.1 إنشاء المشروع​

Spring Initializr مع:

  • Artifact: security-form-demo
  • Dependencies:
    • Spring Web
    • Spring Security
    • Spring Data JPA
    • H2 Database
    • Thymeleaf
    • Lombok

1.2 الهيكل​

src/main/java/com/example/security/
├── config/
│ └── SecurityConfig.java
├── model/
│ ├── User.java
│ └── Role.java
├── repository/
│ ├── UserRepository.java
│ └── RoleRepository.java
├── service/
│ ├── CustomUserDetailsService.java
│ └── UserService.java
├── controller/
│ ├── HomeController.java
│ └── AdminController.java
└── SecurityFormDemoApplication.java

الجزء 2: الكيانات​

2.1 Role​

@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

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

2.2 User​

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
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: المستودعات والخدمات​

3.1 المستودعات​

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

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

3.2 CustomUserDetailsService​

@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("Utilisateur non trouvé: " + username));

return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.toList())
.disabled(!user.isEnabled())
.build();
}
}

3.3 UserService​

@Service
@RequiredArgsConstructor
public class UserService {

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

@Transactional
public User createUser(String username, String email, String password, String roleName) {
if (userRepository.existsByUsername(username)) {
throw new RuntimeException("Username déjà utilisé");
}

Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RuntimeException("Rôle non trouvé: " + roleName));

User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
user.getRoles().add(role);

return userRepository.save(user);
}
}

الجزء 4: إعدادات الأمان​

4.1 SecurityConfig​

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService userDetailsService;

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

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

الجزء 5: وحدات التحكم​

5.1 HomeController​

@Controller
public class HomeController {

@GetMapping({"/", "/home"})
public String home() {
return "home";
}

@GetMapping("/login")
public String login() {
return "login";
}

@GetMapping("/dashboard")
public String dashboard(Model model, Principal principal) {
model.addAttribute("username", principal.getName());
return "dashboard";
}

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

@PostMapping("/register")
public String register(@ModelAttribute User user,
@RequestParam String password,
RedirectAttributes redirectAttributes,
UserService userService) {
try {
userService.createUser(user.getUsername(), user.getEmail(), password, "ROLE_USER");
redirectAttributes.addFlashAttribute("success", "Compte créé! Connectez-vous.");
return "redirect:/login";
} catch (Exception e) {
redirectAttributes.addFlashAttribute("error", e.getMessage());
return "redirect:/register";
}
}
}

5.2 AdminController​

@Controller
@RequestMapping("/admin")
@RequiredArgsConstructor
public class AdminController {

private final UserRepository userRepository;

@GetMapping
public String adminDashboard(Model model) {
model.addAttribute("users", userRepository.findAll());
return "admin/dashboard";
}

@PostMapping("/users/{id}/disable")
public String disableUser(@PathVariable Long id) {
userRepository.findById(id).ifPresent(user -> {
user.setEnabled(false);
userRepository.save(user);
});
return "redirect:/admin";
}
}

الجزء 6: قوالب Thymeleaf​

6.1 home.html​

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>الصفحة الرئيسية</title>
</head>
<body>
<h1>مرحبًا!</h1>

<div sec:authorize="isAnonymous()">
<p>أنت غير مسجل الدخول.</p>
<a href="/login">تسجيل الدخول</a> | <a href="/register">التسجيل</a>
</div>

<div sec:authorize="isAuthenticated()">
<p>مرحبًا، <span sec:authentication="name">User</span>!</p>
<a href="/dashboard">لوحة التحكم</a>
<div sec:authorize="hasRole('ADMIN')">
<a href="/admin">الإدارة</a>
</div>
<form th:action="@{/logout}" method="post" style="display:inline;">
<button type="submit">تسجيل الخروج</button>
</form>
</div>
</body>
</html>

6.2 login.html​

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>تسجيل الدخول</title>
</head>
<body>
<h1>تسجيل الدخول</h1>

<div th:if="${param.error}" style="color:red;">
اسم المستخدم أو كلمة المرور غير صحيحة.
</div>
<div th:if="${param.logout}" style="color:green;">
تم تسجيل الخروج بنجاح.
</div>
<div th:if="${success}" style="color:green;" th:text="${success}"></div>

<form th:action="@{/login}" method="post">
<div>
<label>اسم المستخدم:</label>
<input type="text" name="username" required/>
</div>
<div>
<label>كلمة المرور:</label>
<input type="password" name="password" required/>
</div>
<button type="submit">تسجيل الدخول</button>
</form>

<p><a href="/register">ليس لديك حساب؟ سجل الآن</a></p>
</body>
</html>

6.3 dashboard.html​

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>لوحة التحكم</title>
</head>
<body>
<h1>لوحة التحكم</h1>
<p>مرحبًا، <span th:text="${username}">User</span>!</p>

<h2>أدوارك:</h2>
<ul sec:authentication="authorities">
<!-- عرض الأدوار -->
</ul>

<a href="/home">الصفحة الرئيسية</a>
<form th:action="@{/logout}" method="post" style="display:inline;">
<button type="submit">تسجيل الخروج</button>
</form>
</body>
</html>

الجزء 7: البيانات الأولية​

DataInitializer​

@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

private final RoleRepository roleRepository;
private final UserService userService;

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

// إنشاء مسؤول افتراضي
try {
userService.createUser("admin", "admin@test.com", "admin123", "ROLE_ADMIN");
userService.createUser("user", "user@test.com", "user123", "ROLE_USER");
} catch (Exception ignored) {}
}
}

الجزء 8: الإعدادات​

# application.properties
spring.datasource.url=jdbc:h2:mem:securitydb
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

logging.level.org.springframework.security=DEBUG

الاختبارات​

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

  • الصفحة الرئيسية متاحة للجميع
  • تسجيل الدخول باستخدام user/user123 → لوحة التحكم
  • تسجيل الدخول باستخدام admin/admin123 → لوحة التحكم + الوصول إلى الإدارة
  • المستخدم لا يمكنه الوصول إلى /admin (403)
  • تسجيل الخروج يعمل
  • التسجيل ينشئ مستخدمًا جديدًا

الاختبارات اليدوية​

1. http://localhost:8080 → الصفحة الرئيسية
2. انقر على "تسجيل الدخول" → صفحة تسجيل الدخول
3. تسجيل الدخول admin/admin123 → لوحة التحكم
4. الوصول إلى /admin → OK
5. تسجيل الخروج → العودة إلى تسجيل الدخول
6. تسجيل الدخول user/user123 → لوحة التحكم
7. الوصول إلى /admin → 403 Forbidden

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

➡️ TP8.2: API REST مع JWT


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