Spring Security 6: การยืนยันตัวตนด้วย JWT แบบครบถ้วน
คู่มือเชิงปฏิบัติในการนำการยืนยันตัวตนด้วย JWT มาใช้กับ Spring Security 6: การตั้งค่า การสร้างโทเคน การตรวจสอบ และแนวทางปฏิบัติที่ดีด้านความปลอดภัย

การยืนยันตัวตนด้วย 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 ที่เซ็นกำกับ จากนั้นแนบโทเคนนี้ไปกับคำขอทุกครั้งต่อมา เซิร์ฟเวอร์ตรวจสอบลายเซ็นและดึงข้อมูลผู้ใช้จากโทเคน
// 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 <token>
// → Extract and validate token
// → Create SecurityContext
// → Access protected resource
}แนวทางนี้ช่วยขจัดปัญหา sticky session และทำให้การ deploy บนสภาพแวดล้อมแบบกระจายง่ายขึ้น
การตั้งค่า Dependency บน Maven
โปรเจกต์ต้องการ dependency ของ Spring Security และไลบรารี JWT JJWT (Java JWT) มอบ API ที่ไหลลื่นและได้รับการดูแลดี เพื่อจัดการกับโทเคน
<!-- pom.xml -->
<dependencies>
<!-- Spring Security for authentication management -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Web for REST endpoints -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JJWT: token generation and validation -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<!-- Spring Data JPA for user persistence -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>การแยก JJWT ออกเป็นสามโมดูล (api, impl, jackson) ยึดตามหลักการ encapsulation: ในเวลาคอมไพล์จะมองเห็นเพียง API เท่านั้น ส่วนการ implement เป็นรายละเอียดในเวลารัน
บริการสร้างและตรวจสอบ JWT
บริการ JWT รวมการดำเนินการเกี่ยวกับโทเคนทั้งหมดไว้ในจุดเดียว ทั้งการสร้าง การดึง claim และการตรวจสอบ คีย์ลับที่ปลอดภัยจะเซ็นกำกับโทเคนแต่ละตัวและรับประกันความถูกต้อง
@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<String, Object> 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> T extractClaim(String token, Function<Claims, T> 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 มาใช้งาน ทำให้รวมเข้ากับระบบยืนยันตัวตนได้โดยตรง
@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<? extends GrantedAuthority> 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
}public enum Role {
USER, // Standard user
ADMIN // Administrator with extended privileges
}enum Role จำกัดค่าที่เป็นไปได้และทำให้การตรวจสอบสิทธิ์ในนิพจน์ SpEL ง่ายยิ่งขึ้น
ฟิลเตอร์ยืนยันตัวตน JWT
ฟิลเตอร์ JWT ดักทุกคำขอเพื่อดึงและตรวจสอบโทเคน โดยถูกแทรกเข้าไปในห่วงโซ่ฟิลเตอร์ของ Spring Security ก่อนฟิลเตอร์ยืนยันตัวตนมาตรฐาน
@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 Boot แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การตั้งค่า Spring Security 6
Spring Security 6 ใช้แนวทาง functional ด้วย lambda เพื่อกำหนดค่าห่วงโซ่ความปลอดภัย การตั้งค่านี้กำหนดกฎการเข้าถึงและรวมฟิลเตอร์ JWT เข้าไป
@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();
}
}@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 เมื่อยืนยันตัวตนสำเร็จ
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authService;
// POST /api/auth/register - Register a new user
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(
@Valid @RequestBody RegisterRequest request
) {
return ResponseEntity.ok(authService.register(request));
}
// POST /api/auth/login - Login existing user
@PostMapping("/login")
public ResponseEntity<AuthenticationResponse> login(
@Valid @RequestBody AuthenticationRequest request
) {
return ResponseEntity.ok(authService.authenticate(request));
}
// POST /api/auth/refresh - Token refresh (optional)
@PostMapping("/refresh")
public ResponseEntity<AuthenticationResponse> refresh(
@RequestHeader("Authorization") String authHeader
) {
return ResponseEntity.ok(authService.refreshToken(authHeader));
}
}public record AuthenticationRequest(
@NotBlank(message = "Email required")
@Email(message = "Invalid email format")
String email,
@NotBlank(message = "Password required")
String password
) {}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
) {}public record AuthenticationResponse(
String token,
String type,
long expiresIn
) {
public AuthenticationResponse(String token, long expiresIn) {
this(token, "Bearer", expiresIn);
}
}record ของ Java ทำให้นิยาม DTO กระชับและรับประกันว่าไม่สามารถเปลี่ยนแปลงได้
บริการการยืนยันตัวตน
บริการนี้ควบคุมตรรกะของการสมัครและการล็อกอิน โดยมอบหน้าที่สร้างโทเคนให้ JwtService
@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<String, Object> 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 ที่มีอายุยาวกว่ามาใช้ จัดเก็บไว้ในฐานข้อมูล และเพิกถอนได้
การปกป้อง endpoint ด้วย annotation
ความปลอดภัยระดับเมธอดช่วยให้ควบคุมสิทธิ์ได้อย่างละเอียดในโค้ดธุรกิจโดยตรง
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// Accessible to all authenticated users
@GetMapping("/me")
public ResponseEntity<UserDTO> 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<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request
) {
return ResponseEntity.ok(userService.updateUser(id, request));
}
// Reserved for administrators
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
return ResponseEntity.ok(userService.findAll());
}
// Deletion with post-execution verification
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')") // All methods require ADMIN
public class AdminController {
private final AdminService adminService;
@GetMapping("/dashboard")
public ResponseEntity<DashboardDTO> getDashboard() {
return ResponseEntity.ok(adminService.getDashboardStats());
}
@PostMapping("/users/{id}/role")
public ResponseEntity<UserDTO> changeUserRole(
@PathVariable Long id,
@RequestParam Role newRole
) {
return ResponseEntity.ok(adminService.changeUserRole(id, newRole));
}
}annotation @AuthenticationPrincipal ฉีดผู้ใช้ที่ผ่านการยืนยันตัวตนเข้ามาให้โดยตรง และหลีกเลี่ยงการเข้าถึง SecurityContext แบบทำมือ
การจัดการข้อผิดพลาดของการยืนยันตัวตน
การจัดการข้อผิดพลาดแบบรวมศูนย์ให้การตอบสนองที่สอดคล้องและบอกข้อมูลครบถ้วนเมื่อการยืนยันตัวตนล้มเหลว
@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
);
}
}public record ErrorResponse(
String code,
String message,
Map<String, String> details
) {}รหัสข้อผิดพลาดที่เป็นมาตรฐานทำให้ฝั่งไคลเอนต์จัดการและดีบักได้สะดวกยิ่งขึ้น
การตั้งค่า application.yml
การแยกการตั้งค่าออกมาช่วยให้ปรับพารามิเตอร์ JWT ตามแต่ละสภาพแวดล้อมได้
# 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# 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
การทดสอบเชิงบูรณาการ
การทดสอบจะตรวจสอบพฤติกรรมของระบบยืนยันตัวตนทั้งหมด ตั้งแต่การสมัครสมาชิกจนถึงการเข้าถึงทรัพยากรที่ป้องกันไว้
@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<AuthenticationResponse> 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<AuthenticationResponse> 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<ErrorResponse> 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<UserDTO> 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<Void> 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
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Spring Security 6: การตั้งค่า OAuth2 Resource Server
คู่มือเชิงปฏิบัติสำหรับการกำหนดค่า OAuth2 Resource Server ด้วย Spring Security 6 การตรวจสอบ JWT การกำหนดค่า issuer การจัดการ scope และการรวมกับ Keycloak

Spring Modulith: สถาปัตยกรรม Monolith แบบโมดูลาร์
เรียนรู้ Spring Modulith เพื่อสร้าง monolith แบบโมดูลาร์ใน Java สถาปัตยกรรม โมดูล อีเวนต์อะซิงโครนัส และการทดสอบด้วย Spring Boot 3

สัมภาษณ์ Spring Batch 5: Partitioning, Chunk และ Fault Tolerance
เชี่ยวชาญการสัมภาษณ์ Spring Batch 5: 15 คำถามสำคัญเกี่ยวกับ partitioning การประมวลผลแบบ chunk และความทนทานต่อข้อผิดพลาด พร้อมตัวอย่างโค้ด Java 21