بالدرس السابق أضفنا JWT. اليوم رح نحل مشكلتين بالـ Backend:
بالـ Security Config، فينا نحدد لكل Endpoint مين مسموح له يوصله:
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/employees").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/employees/{employeeId}").hasAnyRole("ADMIN", "USER")
.requestMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/employees/{employeeId}").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/employees/{employeeId}").
اشترك في النشرة البريدية
دروس جديدة، مقالات، وأدوات مباشرة لبريدك.
منطق الصلاحيات:
| العملية | الـ Role المطلوب | السبب |
|---|---|---|
| Get All Employees | Admin فقط | قائمة كل الموظفين حساسة |
| Get One Employee | Admin أو User | اليوزر ممكن يحتاج بياناته |
| Create Employee | Admin فقط | إضافة موظفين صلاحية إدارية |
| Delete Employee | Admin فقط | حذف موظف صلاحية إدارية |
| Update Employee | Admin أو User | اليوزر ممكن يعدل بياناته |
| Leave Requests | Admin أو User | الاثنين يحتاجوا هالوصول |
الفرق بين hasRole() وhasAnyRole(): الأولى بتشترط Role واحدة بالتحديد، والثانية بتقبل أي Role من القائمة.
لحل مشكلة Ownership، بنضيف @EnableMethodSecurity للـ Security Config:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ...
}هاي بتفعل الـ @PreAuthorize Annotation اللي بنستخدمها على الـ Service Methods.
بننشئ Package اسمها utils وجواتها كلاس SecurityUtils:
@Component
public class SecurityUtils {
@Autowired
private UserAccountRepository userAccountRepository;
public boolean isOwner(UUID incomingEmployeeId) {
Authentication authentication = SecurityContextHolder
.getContext()
.getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String username = userDetails.getUsername();
return userAccountRepository.isOwner(username, incomingEmployeeId);
}
}الـ SecurityContextHolder بيعطينا الـ Authentication الموجودة بالـ Security Context - اللي حطها الـ JWT Filter بالدرس الماضي. من خلالها نقدر ناخذ اليوزرنيم اللي عم يبعث الـ Request.
بالـ UserAccountRepository بنضيف Query بتتأكد إذا اليوزر هو صاحب البيانات المطلوبة:
@Query(value = "SELECT COUNT(u) > 0 FROM UserAccount u " +
"WHERE u.username = :username " +
"AND u.employee.id = :employeeId",
nativeQuery = false)
boolean isOwner(@Param("username") String username,
@Param("employeeId") UUID employeeId);الـ Query هاي بتشوف إذا في Account بالـ Database عنده نفس الـ Username المطلوب ونفس الـ Employee ID المطلوب. إذا الـ Count أكبر من صفر تعني إنه هاد اليوزر هو صاحب هالبيانات.
بالـ EmployeeServiceImpl، بنضيف الـ Annotation على المثل اللي بتحتاج Ownership Check:
@Autowired
private SecurityUtils securityUtils;
@Override
@PreAuthorize("@securityUtils.isOwner(#employeeId)")
public Employee findOne(UUID employeeId) {
return employeeRepository.findById(employeeId)
.orElseThrow(() -> CustomResponseException.resourceNotFound(
"Employee not found"
));
}
@Override
@PreAuthorize("@securityUtils.isOwner(#employeeId)")
public Employee update(UUID employeeId, EmployeeUpdate dto) {
// ...
}الـ @securityUtils بيشير للـ Bean بالـ Application Context، والـ #employeeId بياخذ قيمة الـ Parameter من الـ Method.
قبل ما تنفذ الـ Method، Spring Security بتعمل run للـ isOwner وإذا رجعت false بترجع 403 Forbidden مباشرة ما توصل للـ Method أصلاً.
بالـ LeaveRequestServiceImpl:
@Override
@PreAuthorize("@securityUtils.isOwner(#employeeId)")
public LeaveRequest create(UUID employeeId, LeaveRequestCreate dto) {
// ...
}
@Override
@PreAuthorize("@securityUtils.isOwner(#employeeId)")
public List<LeaveRequest> findAllByEmployeeId(UUID employeeId) {
// ...
}| الحالة | الـ Status Code |
|---|---|
| يوزر بدو يعمل Create Employee | 403 Forbidden |
| يوزر بدو ياخذ بيانات يوزر ثاني | 403 Forbidden |
| يوزر بدو ياخذ بياناته هو | 200 OK |
| Admin بيعمل Get All Employees | 200 OK |
| يوزر بدو يعمل Get All Employees | 403 Forbidden |
هيك قدرنا نحمي الـ Backend من ناحيتين: Role-based بالـ Config Level، و Ownership-based بالـ Service Level عن طريق @PreAuthorize والـ SecurityUtils.