بالدرس اليوم رح نضيف Spring Security للتطبيق ونبني نظام Sign Up وSign In.
أول خطوة هي إنشاء Entity للحسابات:
@Entity
@Table(name = "user_account")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserAccount implements UserDetails {
@Id
@GeneratedValue
@UuidGenerator
private UUID id;
@Column(name = "username", nullable = false, unique = true, length = 100)
اشترك في النشرة البريدية
دروس جديدة، مقالات، وأدوات مباشرة لبريدك.
العلاقة مع Employee هي One-to-One: كل موظف عنده حساب واحد. نفرض هاد بوضع unique = true على الـ employee_id Foreign Key.
ملاحظة: الـ One-to-One في قاعدة البيانات هي في الأساس One-to-Many، بس بنفرضها بالـ Unique Constraint.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>بمجرد إضافة هاد الـ Dependency، Spring Security بيسكّر تلقائياً كل الـ Endpoints. أي طلب رح يطلب Username وPassword.
بننشئ Package اسمها config وفيها كلاس SecurityConfig:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/employees/**").authenticated()
.anyRequest().authenticated()
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}@Configuration + @EnableWebSecurity - يجعل هذا الكلاس مركز إعداد Spring Securitycsrf.disable() - نعطله للـ REST APIspermitAll() - يفتح الـ Endpoint لأي شخص بدون Authenticationauthenticated() - يطلب Authentication@Bean PasswordEncoder - بيولّد BCrypt Hasher لتشفير الباسوردات@Repository
public interface UserAccountRepository extends JpaRepository<UserAccount, UUID> {
Optional<UserAccount> findOneByUsername(String username);
}Spring Data JPA بيولّد الـ SQL تلقائياً من اسم الـ Method.
Spring Security تحتاج UserDetailsService لتجيب المستخدم من الـ Database:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserAccountRepository userAccountRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserAccount account = userAccountRepository.findOneByUsername(username)
.orElseThrow(() -> CustomResponseException.badCredentials());
return User.builder()
.username(account.getUsername())
.password(account.getPassword())
.roles(account.getRole())
.build();
}
}الـ loadUserByUsername بتجيب الحساب من الـ Database وبتحوله لـ UserDetails اللي Spring Security تفهمه.
بنضيف الـ Authentication Manager للـ SecurityConfig:
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity http,
UserDetailsService userDetailsService) throws Exception {
var builder = http.getSharedObject(AuthenticationManagerBuilder.class);
builder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
return builder.build();
}الـ Authentication Manager هو اللي بيقرر إذا الـ Username والـ Password صح أو لا.
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/sign-up")
public ResponseEntity<GlobalResponse<String>> signUp(
@Valid @RequestBody SignUpRequest request) {
authService.signUp(request);
return new ResponseEntity<>(
new GlobalResponse<>("Signed up successfully"), HttpStatus.CREATED
);
}
}public record SignUpRequest(
@NotBlank(message = "Username is required")
String username,
@Size(min = 5, max = 50, message = "Password must be between 5 and 50 characters")
String password,
@NotNull
UUID employeeId
) {}@Service
public class AuthService {
@Autowired
private UserAccountRepository userAccountRepository;
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public void signUp(SignUpRequest request) {
Employee employee = employeeRepository.findById(request.employeeId())
.orElseThrow(() -> CustomResponseException.resourceNotFound(
"Employee not found"
));
UserAccount account = new UserAccount();
account.setUsername(request.username());
account.setPassword(passwordEncoder.encode(request.password()));
account.setEmployee(employee);
userAccountRepository.save(account);
}
}passwordEncoder.encode() بيستخدم BCrypt لتشفير الباسورد قبل الحفظ. لا تحفظ الباسورد كـ Plain Text أبداً.
| العملية | Status Code | الشرح |
|---|---|---|
| Sign Up ناجح | 201 Created | تم إنشاء الحساب |
| طلب بدون Auth لـ Endpoint مقيّد | 401 Unauthorized | ما عمل Login |
| طلب بصلاحيات غلط | 403 Forbidden | Role ناقصة |
بالدرس الجاي رح نضيف JWT Tokens بدل ما نبعث Username وPassword مع كل طلب.