# Spring Security 6: การยืนยันตัวตนด้วย JWT แบบครบถ้วน > คู่มือเชิงปฏิบัติในการนำการยืนยันตัวตนด้วย JWT มาใช้กับ Spring Security 6: การตั้งค่า การสร้างโทเคน การตรวจสอบ และแนวทางปฏิบัติที่ดีด้านความปลอดภัย - Published: 2026-02-19 - Updated: 2026-04-28 - Author: SharpSkill - Tags: spring security, jwt, java, spring boot, authentication - Reading time: 15 min --- การยืนยันตัวตนด้วย JWT (JSON Web Token) กลายเป็นมาตรฐานในการป้องกัน REST API สมัยใหม่ Spring Security 6 นำเสนอแนวทางการตั้งค่าแบบ functional ซึ่งทำให้การพัฒนาง่ายขึ้นและเพิ่มความปลอดภัยไปในตัว คู่มือนี้ครอบคลุมกระบวนการทั้งหมด ตั้งแต่การตั้งค่าเริ่มต้นไปจนถึงการป้องกันปลายทาง (endpoint) > **ข้อกำหนดเบื้องต้น** > > บทช่วยสอนนี้ใช้ Spring Boot 3.2+ และ Spring Security 6.2+ แนวคิดต่าง ๆ ยังคงใช้ได้ในเวอร์ชันถัดไปโดยอาจต้องปรับเปลี่ยนไวยากรณ์เพียงเล็กน้อย ## สถาปัตยกรรม JWT ใน Spring Security การยืนยันตัวตนแบบ JWT ตั้งอยู่บนหลักการ stateless: เซิร์ฟเวอร์ไม่จัดเก็บเซสชันใด ๆ คำขอแต่ละครั้งจะมีโทเคนที่เซ็นกำกับเพื่อยืนยันตัวตนของผู้ใช้ สถาปัตยกรรมนี้รองรับการขยายระบบในแนวนอนโดยไม่ต้องแบ่งปันเซสชันระหว่างอินสแตนซ์ การไหลของการยืนยันตัวตน JWT แบ่งออกเป็นหลายขั้นตอน ผู้ใช้ยืนยันตัวตนด้วยข้อมูลรับรอง รับโทเคน JWT ที่เซ็นกำกับ จากนั้นแนบโทเคนนี้ไปกับคำขอทุกครั้งต่อมา เซิร์ฟเวอร์ตรวจสอบลายเซ็นและดึงข้อมูลผู้ใช้จากโทเคน ```java // JwtAuthenticationFlow.java // Conceptual representation of the authentication flow public class JwtAuthenticationFlow { // 1. Initial authentication: POST /api/auth/login // → Verify credentials against database // → Generate signed JWT token // → Return token to client // 2. Authenticated requests: GET /api/protected // → Header: Authorization: Bearer // → Extract and validate token // → Create SecurityContext // → Access protected resource } ``` แนวทางนี้ช่วยขจัดปัญหา sticky session และทำให้การ deploy บนสภาพแวดล้อมแบบกระจายง่ายขึ้น ## การตั้งค่า Dependency บน Maven โปรเจกต์ต้องการ dependency ของ Spring Security และไลบรารี JWT JJWT (Java JWT) มอบ API ที่ไหลลื่นและได้รับการดูแลดี เพื่อจัดการกับโทเคน ```xml org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web io.jsonwebtoken jjwt-api 0.12.5 io.jsonwebtoken jjwt-impl 0.12.5 runtime io.jsonwebtoken jjwt-jackson 0.12.5 runtime org.springframework.boot spring-boot-starter-data-jpa ``` การแยก JJWT ออกเป็นสามโมดูล (api, impl, jackson) ยึดตามหลักการ encapsulation: ในเวลาคอมไพล์จะมองเห็นเพียง API เท่านั้น ส่วนการ implement เป็นรายละเอียดในเวลารัน ## บริการสร้างและตรวจสอบ JWT บริการ JWT รวมการดำเนินการเกี่ยวกับโทเคนทั้งหมดไว้ในจุดเดียว ทั้งการสร้าง การดึง claim และการตรวจสอบ คีย์ลับที่ปลอดภัยจะเซ็นกำกับโทเคนแต่ละตัวและรับประกันความถูกต้อง ```java // JwtService.java @Service public class JwtService { // Secret key injected from application.yml @Value("${app.jwt.secret}") private String secretKey; // Token validity duration (24 hours by default) @Value("${app.jwt.expiration:86400000}") private long jwtExpiration; // Generates a JWT token for an authenticated user public String generateToken(UserDetails userDetails) { return generateToken(new HashMap<>(), userDetails); } // Generates a token with custom claims public String generateToken(Map extraClaims, UserDetails userDetails) { return Jwts.builder() .claims(extraClaims) // Additional claims (roles, permissions) .subject(userDetails.getUsername()) // Principal identifier .issuedAt(new Date()) // Creation date .expiration(new Date(System.currentTimeMillis() + jwtExpiration)) .signWith(getSigningKey(), Jwts.SIG.HS256) // HMAC-SHA256 signature .compact(); } // Extracts the username (subject) from the token public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } // Extracts a specific claim via an extraction function public T extractClaim(String token, Function claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } // Validates the token: correct signature and not expired public boolean isTokenValid(String token, UserDetails userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } // Checks if the token has expired private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } // Extracts the expiration date private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } // Parses the token and extracts all claims private Claims extractAllClaims(String token) { return Jwts.parser() .verifyWith(getSigningKey()) // Verifies the signature .build() .parseSignedClaims(token) // Parses the signed token .getPayload(); // Returns the claims } // Generates the signing key from the Base64-encoded secret private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } } ``` คีย์ลับต้องยาวเพียงพอ (อย่างน้อย 256 บิตสำหรับ HS256) และต้องเก็บอย่างปลอดภัย ห้ามเก็บไว้ในซอร์สโค้ด > **ความปลอดภัยของคีย์ลับ** > > ควรใช้ environment variable หรือ secrets manager สำหรับคีย์ JWT คีย์ที่รั่วไหลจะทำให้สามารถปลอมโทเคนที่ใช้ได้สำหรับผู้ใช้คนใดก็ได้ ## การตั้งค่าเอนทิตี User เอนทิตีผู้ใช้นำ `UserDetails` ของ Spring Security มาใช้งาน ทำให้รวมเข้ากับระบบยืนยันตัวตนได้โดยตรง ```java // User.java @Entity @Table(name = "users") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String password; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; // Role stored as enum for type safety @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; // UserDetails implementation: returns user authorities @Override public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } // Username corresponds to email in this implementation @Override public String getUsername() { return email; } // Account always active (adapt as needed) @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } // Getters and setters omitted for brevity } ``` ```java // Role.java public enum Role { USER, // Standard user ADMIN // Administrator with extended privileges } ``` enum `Role` จำกัดค่าที่เป็นไปได้และทำให้การตรวจสอบสิทธิ์ในนิพจน์ SpEL ง่ายยิ่งขึ้น ## ฟิลเตอร์ยืนยันตัวตน JWT ฟิลเตอร์ JWT ดักทุกคำขอเพื่อดึงและตรวจสอบโทเคน โดยถูกแทรกเข้าไปในห่วงโซ่ฟิลเตอร์ของ Spring Security ก่อนฟิลเตอร์ยืนยันตัวตนมาตรฐาน ```java // JwtAuthenticationFilter.java @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { // Retrieve the Authorization header final String authHeader = request.getHeader("Authorization"); // Check for Bearer prefix if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } // Extract the token (without the "Bearer " prefix) final String jwt = authHeader.substring(7); try { // Extract username from token final String userEmail = jwtService.extractUsername(jwt); // Check that user is not already authenticated if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { // Load user details from database UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); // Validate token (signature + expiration + user match) if (jwtService.isTokenValid(jwt, userDetails)) { // Create authentication object UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); // Add request details authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); // Set authentication in security context SecurityContextHolder.getContext().setAuthentication(authToken); } } } catch (ExpiredJwtException e) { // Token expired: user must re-authenticate response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Token expired"); return; } catch (JwtException e) { // Invalid token: incorrect signature or malformed format response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Invalid token"); return; } // Continue the filter chain filterChain.doFilter(request, response); } } ``` `OncePerRequestFilter` รับประกันว่าฟิลเตอร์ทำงานเพียงครั้งเดียวต่อหนึ่งคำขอ แม้กรณีที่มี forward หรือ include ## การตั้งค่า Spring Security 6 Spring Security 6 ใช้แนวทาง functional ด้วย lambda เพื่อกำหนดค่าห่วงโซ่ความปลอดภัย การตั้งค่านี้กำหนดกฎการเข้าถึงและรวมฟิลเตอร์ JWT เข้าไป ```java // SecurityConfig.java @Configuration @EnableWebSecurity @EnableMethodSecurity // Enables @PreAuthorize and @PostAuthorize @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // Disable CSRF since API is stateless (no session cookies) .csrf(csrf -> csrf.disable()) // Configure authorization rules .authorizeHttpRequests(auth -> auth // Public endpoints: authentication and registration .requestMatchers("/api/auth/**").permitAll() // API documentation accessible without authentication .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // Admin endpoints reserved for administrators .requestMatchers("/api/admin/**").hasRole("ADMIN") // All other requests require authentication .anyRequest().authenticated() ) // Stateless mode: no server-side HTTP session .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) // Custom authentication provider .authenticationProvider(authenticationProvider) // Insert JWT filter before standard authentication filter .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } } ``` ```java // ApplicationConfig.java @Configuration @RequiredArgsConstructor public class ApplicationConfig { private final UserRepository userRepository; // User loading service for Spring Security @Bean public UserDetailsService userDetailsService() { return username -> userRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } // Authentication provider with password encoder @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } // Authentication manager exposed as bean @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } // BCrypt encoder for secure password hashing @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` การปิด CSRF ปลอดภัยสำหรับ API ที่เป็น stateless เพราะการยืนยันตัวตนพึ่งพา header ที่ส่งอย่างเจาะจง ไม่ใช่คุกกี้ที่เบราว์เซอร์ส่งเองโดยอัตโนมัติ ## คอนโทรลเลอร์การยืนยันตัวตน คอนโทรลเลอร์เปิด endpoint สำหรับการสมัครสมาชิกและการล็อกอิน Endpoint เหล่านี้เป็นแบบสาธารณะและคืนโทเคน JWT เมื่อยืนยันตัวตนสำเร็จ ```java // AuthenticationController.java @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService authService; // POST /api/auth/register - Register a new user @PostMapping("/register") public ResponseEntity register( @Valid @RequestBody RegisterRequest request ) { return ResponseEntity.ok(authService.register(request)); } // POST /api/auth/login - Login existing user @PostMapping("/login") public ResponseEntity login( @Valid @RequestBody AuthenticationRequest request ) { return ResponseEntity.ok(authService.authenticate(request)); } // POST /api/auth/refresh - Token refresh (optional) @PostMapping("/refresh") public ResponseEntity refresh( @RequestHeader("Authorization") String authHeader ) { return ResponseEntity.ok(authService.refreshToken(authHeader)); } } ``` ```java // AuthenticationRequest.java public record AuthenticationRequest( @NotBlank(message = "Email required") @Email(message = "Invalid email format") String email, @NotBlank(message = "Password required") String password ) {} ``` ```java // RegisterRequest.java public record RegisterRequest( @NotBlank(message = "First name required") String firstName, @NotBlank(message = "Last name required") String lastName, @NotBlank(message = "Email required") @Email(message = "Invalid email format") String email, @NotBlank(message = "Password required") @Size(min = 8, message = "Password must contain at least 8 characters") String password ) {} ``` ```java // AuthenticationResponse.java public record AuthenticationResponse( String token, String type, long expiresIn ) { public AuthenticationResponse(String token, long expiresIn) { this(token, "Bearer", expiresIn); } } ``` record ของ Java ทำให้นิยาม DTO กระชับและรับประกันว่าไม่สามารถเปลี่ยนแปลงได้ ## บริการการยืนยันตัวตน บริการนี้ควบคุมตรรกะของการสมัครและการล็อกอิน โดยมอบหน้าที่สร้างโทเคนให้ `JwtService` ```java // AuthenticationService.java @Service @RequiredArgsConstructor public class AuthenticationService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final AuthenticationManager authenticationManager; @Value("${app.jwt.expiration:86400000}") private long jwtExpiration; // Register a new user @Transactional public AuthenticationResponse register(RegisterRequest request) { // Check that email is not already in use if (userRepository.existsByEmail(request.email())) { throw new EmailAlreadyExistsException("Email already registered"); } // Create user entity with hashed password User user = new User(); user.setFirstName(request.firstName()); user.setLastName(request.lastName()); user.setEmail(request.email()); user.setPassword(passwordEncoder.encode(request.password())); user.setRole(Role.USER); // Persist the user userRepository.save(user); // Generate and return JWT token String jwtToken = jwtService.generateToken(user); return new AuthenticationResponse(jwtToken, jwtExpiration); } // Authenticate an existing user public AuthenticationResponse authenticate(AuthenticationRequest request) { // Delegate verification to AuthenticationManager authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.email(), request.password() ) ); // Load user (authentication succeeded) User user = userRepository.findByEmail(request.email()) .orElseThrow(() -> new UsernameNotFoundException("User not found")); // Generate token with additional claims Map claims = new HashMap<>(); claims.put("role", user.getRole().name()); claims.put("userId", user.getId()); String jwtToken = jwtService.generateToken(claims, user); return new AuthenticationResponse(jwtToken, jwtExpiration); } // Refresh token (extend session) public AuthenticationResponse refreshToken(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new InvalidTokenException("Invalid token"); } String oldToken = authHeader.substring(7); String userEmail = jwtService.extractUsername(oldToken); User user = userRepository.findByEmail(userEmail) .orElseThrow(() -> new UsernameNotFoundException("User not found")); // Generate new token String newToken = jwtService.generateToken(user); return new AuthenticationResponse(newToken, jwtExpiration); } } ``` `AuthenticationManager` รวมการตรวจสอบข้อมูลรับรองเข้าด้วยกันและทำให้เปลี่ยนกลยุทธ์การยืนยันตัวตนได้ง่าย > **Refresh Token** > > เพื่อเสริมความปลอดภัย ควรนำระบบ refresh token ที่มีอายุยาวกว่ามาใช้ จัดเก็บไว้ในฐานข้อมูล และเพิกถอนได้ ## การปกป้อง endpoint ด้วย annotation ความปลอดภัยระดับเมธอดช่วยให้ควบคุมสิทธิ์ได้อย่างละเอียดในโค้ดธุรกิจโดยตรง ```java // UserController.java @RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; // Accessible to all authenticated users @GetMapping("/me") public ResponseEntity getCurrentUser( @AuthenticationPrincipal User currentUser ) { return ResponseEntity.ok(UserDTO.from(currentUser)); } // Only the concerned user or an admin can modify the profile @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") @PutMapping("/{id}") public ResponseEntity updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request ) { return ResponseEntity.ok(userService.updateUser(id, request)); } // Reserved for administrators @PreAuthorize("hasRole('ADMIN')") @GetMapping public ResponseEntity> getAllUsers() { return ResponseEntity.ok(userService.findAll()); } // Deletion with post-execution verification @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{id}") public ResponseEntity deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } } ``` ```java // AdminController.java @RestController @RequestMapping("/api/admin") @PreAuthorize("hasRole('ADMIN')") // All methods require ADMIN public class AdminController { private final AdminService adminService; @GetMapping("/dashboard") public ResponseEntity getDashboard() { return ResponseEntity.ok(adminService.getDashboardStats()); } @PostMapping("/users/{id}/role") public ResponseEntity changeUserRole( @PathVariable Long id, @RequestParam Role newRole ) { return ResponseEntity.ok(adminService.changeUserRole(id, newRole)); } } ``` annotation `@AuthenticationPrincipal` ฉีดผู้ใช้ที่ผ่านการยืนยันตัวตนเข้ามาให้โดยตรง และหลีกเลี่ยงการเข้าถึง `SecurityContext` แบบทำมือ ## การจัดการข้อผิดพลาดของการยืนยันตัวตน การจัดการข้อผิดพลาดแบบรวมศูนย์ให้การตอบสนองที่สอดคล้องและบอกข้อมูลครบถ้วนเมื่อการยืนยันตัวตนล้มเหลว ```java // SecurityExceptionHandler.java @RestControllerAdvice public class SecurityExceptionHandler { private static final Logger log = LoggerFactory.getLogger(SecurityExceptionHandler.class); // Authentication error (incorrect credentials) @ExceptionHandler(BadCredentialsException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleBadCredentials(BadCredentialsException ex) { return new ErrorResponse( "INVALID_CREDENTIALS", "Invalid email or password", null ); } // Access denied (authenticated but not authorized) @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ErrorResponse handleAccessDenied(AccessDeniedException ex) { return new ErrorResponse( "ACCESS_DENIED", "Access to this resource is not authorized", null ); } // Expired JWT token @ExceptionHandler(ExpiredJwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleExpiredToken(ExpiredJwtException ex) { return new ErrorResponse( "TOKEN_EXPIRED", "Session expired, please log in again", null ); } // Invalid JWT token @ExceptionHandler(JwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleInvalidToken(JwtException ex) { log.warn("Invalid JWT token: {}", ex.getMessage()); return new ErrorResponse( "INVALID_TOKEN", "Invalid authentication token", null ); } // Email already in use during registration @ExceptionHandler(EmailAlreadyExistsException.class) @ResponseStatus(HttpStatus.CONFLICT) public ErrorResponse handleEmailExists(EmailAlreadyExistsException ex) { return new ErrorResponse( "EMAIL_EXISTS", ex.getMessage(), null ); } } ``` ```java // ErrorResponse.java public record ErrorResponse( String code, String message, Map details ) {} ``` รหัสข้อผิดพลาดที่เป็นมาตรฐานทำให้ฝั่งไคลเอนต์จัดการและดีบักได้สะดวกยิ่งขึ้น ## การตั้งค่า application.yml การแยกการตั้งค่าออกมาช่วยให้ปรับพารามิเตอร์ JWT ตามแต่ละสภาพแวดล้อมได้ ```yaml # application.yml app: jwt: # Base64 secret key (256 bits minimum for HS256) # Generate with: openssl rand -base64 32 secret: ${JWT_SECRET:yourSuperSecretKeyOf256BitsMinimum} # Validity duration in milliseconds (24 hours) expiration: 86400000 spring: datasource: url: jdbc:postgresql://localhost:5432/springjwt username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres} jpa: hibernate: ddl-auto: validate show-sql: false ``` ```yaml # application-dev.yml app: jwt: # Short expiration for development (1 hour) expiration: 3600000 spring: jpa: show-sql: true hibernate: ddl-auto: update logging: level: org.springframework.security: DEBUG ``` ในสภาพแวดล้อมโปรดักชัน คีย์ลับควรมาจาก environment variable หรือบริการจัดการความลับเช่น Vault ## การทดสอบเชิงบูรณาการ การทดสอบจะตรวจสอบพฤติกรรมของระบบยืนยันตัวตนทั้งหมด ตั้งแต่การสมัครสมาชิกจนถึงการเข้าถึงทรัพยากรที่ป้องกันไว้ ```java // AuthenticationIntegrationTest.java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = Replace.ANY) class AuthenticationIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { userRepository.deleteAll(); } @Test void shouldRegisterAndAuthenticateUser() { // Registration RegisterRequest registerRequest = new RegisterRequest( "John", "Doe", "john@example.com", "password123" ); ResponseEntity registerResponse = restTemplate .postForEntity("/api/auth/register", registerRequest, AuthenticationResponse.class); assertThat(registerResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(registerResponse.getBody().token()).isNotBlank(); // Login with same credentials AuthenticationRequest loginRequest = new AuthenticationRequest( "john@example.com", "password123" ); ResponseEntity loginResponse = restTemplate .postForEntity("/api/auth/login", loginRequest, AuthenticationResponse.class); assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(loginResponse.getBody().token()).isNotBlank(); } @Test void shouldRejectInvalidCredentials() { AuthenticationRequest request = new AuthenticationRequest( "unknown@example.com", "wrongpassword" ); ResponseEntity response = restTemplate .postForEntity("/api/auth/login", request, ErrorResponse.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void shouldAccessProtectedResourceWithValidToken() { // Register to get a token RegisterRequest registerRequest = new RegisterRequest( "Jane", "Doe", "jane@example.com", "password123" ); AuthenticationResponse authResponse = restTemplate .postForObject("/api/auth/register", registerRequest, AuthenticationResponse.class); // Access protected resource with token HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(authResponse.token()); ResponseEntity response = restTemplate.exchange( "/api/users/me", HttpMethod.GET, new HttpEntity<>(headers), UserDTO.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().email()).isEqualTo("jane@example.com"); } @Test void shouldRejectAccessWithoutToken() { ResponseEntity response = restTemplate .getForEntity("/api/users/me", Void.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } } ``` การทดสอบเหล่านี้ครอบคลุมสถานการณ์หลัก ๆ ทั้งการสมัครสมาชิก การล็อกอิน การเข้าถึงที่ได้รับอนุญาต และการเข้าถึงที่ถูกปฏิเสธ ## บทสรุป การนำการยืนยันตัวตน JWT มาใช้กับ Spring Security 6 ดำเนินตามแบบแผนที่ชัดเจน บริการ JWT รับผิดชอบในการสร้างและตรวจสอบโทเคน ฟิลเตอร์ดักคำขอเพื่อตั้งบริบทความปลอดภัย และการตั้งค่ากำหนดกฎการเข้าถึง **รายการตรวจสอบสำหรับการดีพลอย:** - ✅ คีย์ลับขนาดอย่างน้อย 256 บิตเก็บไว้ในตัวแปรสภาพแวดล้อม - ✅ HTTPS เป็นสิ่งจำเป็นในโปรดักชันเพื่อปกป้องโทเคนระหว่างส่ง - ✅ อายุของโทเคนกำหนดให้เหมาะกับบริบท (15 นาที – 24 ชั่วโมง) - ✅ Refresh token สำหรับเซสชันยาวนานโดยไม่ต้องล็อกอินใหม่ - ✅ การจัดการข้อผิดพลาดเป็นมาตรฐานพร้อมรหัสเชิงธุรกิจ - ✅ บันทึกล็อกความพยายามยืนยันตัวตนที่ล้มเหลว - ✅ การทดสอบเชิงบูรณาการครอบคลุมสถานการณ์การยืนยันตัวตน - ✅ ใช้ rate limiting บน endpoint ของการยืนยันตัวตน สถาปัตยกรรมนี้เป็นรากฐานที่แข็งแรงสำหรับการป้องกัน REST API และยังขยายได้สำหรับความต้องการที่ซับซ้อนขึ้น เช่น การยืนยันตัวตนแบบหลายปัจจัยหรือการผนวกกับ OAuth2 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/spring-boot/spring-security-6-jwt-authentication