ការការពារ Application ជាមួយ Spring Security
មេរៀននេះនឹងណែនាំអ្នកពីការអនុវត្ត Spring Security ចាប់ពីការដំឡើង រហូតដល់ JWT Authentication ពេញលេញ។
ទិដ្ឋភាពទូទៅ (Overview)
Spring Security គឺជា Framework ដ៏មានអានុភាពបំផុតសម្រាប់ការការពារ Java Application ។ វាផ្តល់នូវ Authentication (ការផ្ទៀងផ្ទាត់អ្នកប្រើ) និង Authorization (ការអនុញ្ញាតការចូលប្រើ) ។
Security Filter Chain
Spring Security ដំណើរការតាមរយៈ Filter Chain ។ Request ពី Browser ចូលមក ហើយត្រូវ Filter ផ្សេងៗ ដំណើរការ:
ការដំឡើង (Setup)
បន្ថែម Dependency ទៅក្នុង pom.xml:
<dependencies> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT Library --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.3</version> <scope>runtime</scope> </dependency> </dependencies>
Security Configuration
ការកំណត់ Security Configuration ដោយ Custom តាម @Configuration Class:
@Configuration @EnableWebSecurity @EnableMethodSecurity // អនុញ្ញាតការប្រើ @PreAuthorize public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthFilter; @Autowired private UserDetailsService userDetailsService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // បិទ CSRF ព្រោះប្រើ JWT .csrf(csrf -> csrf.disable()) // កំណត់ Authorization Rules .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) // Stateless Session (ប្រើ JWT) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) // បន្ថែម JWT Filter .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
Authentication (ការផ្ទៀងផ្ទាត់)
ការបង្កើត Auth Controller សម្រាប់ Login និង Register:
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authManager; @Autowired private JwtService jwtService; @Autowired private UserService userService; // Login Endpoint @PostMapping("/login") public ResponseEntity<AuthResponse> login( @RequestBody LoginRequest request) { // ផ្ទៀងផ្ទាត់ username + password Authentication auth = authManager.authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); // បង្កើត JWT Token UserDetails user = (UserDetails) auth.getPrincipal(); String token = jwtService.generateToken(user); return ResponseEntity.ok( new AuthResponse(token, "Login successful") ); } // Register Endpoint @PostMapping("/register") public ResponseEntity<String> register( @RequestBody RegisterRequest request) { userService.createUser(request); return ResponseEntity.ok("User registered successfully!"); } }
Request/Response Models
// Login Request public record LoginRequest( String username, String password ) {} // Auth Response ផ្ញើ Token ត្រឡប់ public record AuthResponse( String token, String message ) {} // Register Request public record RegisterRequest( String username, String email, String password, String role ) {}
Authorization (ការអនុញ្ញាត)
Spring Security ផ្តល់ Annotation ច្រើនសម្រាប់ Authorization:
| Annotation | ការប្រើប្រាស់ | ឧទាហរណ៏ |
|---|---|---|
@PreAuthorize |
ពិនិត្យ Permission មុន Method ដំណើរការ | @PreAuthorize("hasRole('ADMIN')") |
@PostAuthorize |
ពិនិត្យ Permission ក្រោយ Method ត្រឡប់ | @PostAuthorize("returnObject.owner == auth.name") |
@Secured |
ដាក់ Role ដែលអனុញ្ញាត | @Secured({"ROLE_ADMIN","ROLE_USER"}) |
hasAuthority() |
ពិនិត្យ Authority ជាក់លាក់ | hasAuthority('READ_USERS') |
@RestController @RequestMapping("/api/products") public class ProductController { // អ្នកប្រើគ្រប់គ្នា (Authenticated) អាចមើលបាន @GetMapping public List<Product> getAllProducts() { return productService.findAll(); } // តែ ADMIN ប៉ុណ្ណោះអាចបន្ថែម @PostMapping @PreAuthorize("hasRole('ADMIN')") public Product createProduct(@RequestBody Product product) { return productService.save(product); } // User ដែល Login ហើយ ឬ ADMIN @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") public void deleteProduct(@PathVariable Long id) { productService.delete(id); } // ពិនិត្យ Permission ច្រើន @PutMapping("/{id}") @PreAuthorize("hasAuthority('WRITE_PRODUCTS') or hasRole('ADMIN')") public Product updateProduct(@PathVariable Long id, @RequestBody Product product) { return productService.update(id, product); } }
JWT Token Authentication
JSON Web Token (JWT) ប្រើក្នុង Stateless Authentication ។ Token ផ្ទុកព័ត៌មានអ្នកប្រើ ហើយ Server មិនត្រូវការ Session ទេ។
@Service public class JwtService { @Value("${jwt.secret}") private String secretKey; private static final long EXPIRATION = 86400000; // 24 ម៉ោង // បង្កើត Token ថ្មី public String generateToken(UserDetails userDetails) { return Jwts.builder() .subject(userDetails.getUsername()) .claim("roles", userDetails.getAuthorities()) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + EXPIRATION)) .signWith(getSigningKey()) .compact(); } // ទាញ Username ពី Token public String extractUsername(String token) { return extractClaims(token, Claims::getSubject); } // ពិនិត្យ Token ស្ថិតស្ថេរ public boolean isTokenValid(String token, UserDetails user) { final String username = extractUsername(token); return username.equals(user.getUsername()) && !isTokenExpired(token); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private SecretKey getSigningKey() { byte[] bytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(bytes); } private <T> T extractClaims(String token, Function<Claims, T> resolver) { final Claims claims = Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload(); return resolver.apply(claims); } }
jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 jwt.expiration=86400000
UserDetailsService
Spring Security ត្រូវការ UserDetailsService ដើម្បីទាញ User ពី Database:
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException( "User not found: " + username ) ); // បំប្លែង User Entity ទៅ UserDetails return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .roles(user.getRole().name()) .build(); } } // User Entity @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) private String username; private String email; private String password; // BCrypt encoded @Enumerated(EnumType.STRING) private Role role; }
JWT Authentication Filter
Filter នេះដំណើរការក្នុង Request ចូលមក ដើម្បីពិនិត្យ JWT Token:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtService jwtService; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // ទាញ Authorization Header final String authHeader = request.getHeader("Authorization"); // ពិនិត្យថា Header ចាប់ផ្តើមជា "Bearer " if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } // ទាញ Token (លុប "Bearer " ចេញ) final String jwt = authHeader.substring(7); final String username = jwtService.extractUsername(jwt); // ពិនិត្យថា User មិនទាន់ authenticated if (username != null && SecurityContextHolder.getContext() .getAuthentication() == null) { UserDetails user = userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, user)) { // Set Authentication ក្នុង SecurityContext var authToken = new UsernamePasswordAuthenticationToken( user, null, user.getAuthorities() ); authToken.setDetails( new WebAuthenticationDetailsSource() .buildDetails(request) ); SecurityContextHolder.getContext() .setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
OncePerRequestFilter ធានាថាដំណើរការ ម្តងប៉ុណ្ណោះ ក្នុង Request មួយ ទោះបីមាន Forward ឬ Include ក៏ដោយ។Roles & Authorities
ការបែងចែក Role (ADMIN, USER) និង Authority (READ_PRODUCTS, WRITE_PRODUCTS):
public enum Role { USER( Set.of( Permission.READ_OWN_PROFILE, Permission.READ_PRODUCTS ) ), ADMIN( Set.of( Permission.READ_OWN_PROFILE, Permission.READ_PRODUCTS, Permission.WRITE_PRODUCTS, Permission.DELETE_PRODUCTS, Permission.MANAGE_USERS ) ); private final Set<Permission> permissions; Role(Set<Permission> permissions) { this.permissions = permissions; } public List<SimpleGrantedAuthority> getAuthorities() { var authorities = permissions.stream() .map(p -> new SimpleGrantedAuthority(p.name())) .collect(Collectors.toList()); // បន្ថែម ROLE_ prefix authorities.add(new SimpleGrantedAuthority( "ROLE_" + this.name() )); return authorities; } } // Permission Enum public enum Permission { READ_OWN_PROFILE, READ_PRODUCTS, WRITE_PRODUCTS, DELETE_PRODUCTS, MANAGE_USERS }
| Role | Permissions | Endpoint ចូលបាន |
|---|---|---|
| USER | READ_OWN_PROFILE, READ_PRODUCTS | GET /api/products, GET /api/profile |
| ADMIN | ទាំងអស់ | រាល់ Endpoint |