Bài 20.3: Module 1: Xây Dựng Authentication, JWT Token Pair & Role-Based Access Control
Bài 20.3: Module 1: Xây Dựng Authentication, JWT Token Pair & Role-Based Access Control
- Độ khó: Nâng cao
- Thời gian đọc: ~30 phút
- Phiên bản áp dụng: Spring Boot 3.4.x, Spring Security 6.x, JJWT 0.12+
1. Mục Tiêu Bài Học (Learning Objectives)
Sau bài học này, bạn sẽ:
- Xây dựng trọn vẹn Module Authentication của dự án Capstone:
- Đăng ký tài khoản mới (Mã hóa BCrypt, kiểm tra trùng email).
- Đăng nhập (Xác thực mật khẩu và sinh cặp Access Token + Refresh Token).
- Cấp lại Token (Refresh Token Rotation).
- Phân quyền Endpoint:
/api/v1/auth/**(Public),/api/v1/admin/**(Chỉ Admin).
2. Các DTOs Cho Module Authentication
package com.devpool.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public final class AuthDtos {
public record RegisterRequest(
@NotBlank(message = "Họ tên không được để trống")
String fullName,
@NotBlank(message = "Email không được để trống")
@Email(message = "Email sai định dạng")
String email,
@NotBlank(message = "Mật khẩu không được để trống")
@Size(min = 6, message = "Mật khẩu tối thiểu 6 ký tự")
String password
) {}
public record LoginRequest(
@NotBlank(message = "Email không được để trống")
String email,
@NotBlank(message = "Mật khẩu không được để trống")
String password
) {}
public record AuthResponse(
String accessToken,
String refreshToken,
String tokenType,
long expiresIn
) {
public static AuthResponse of(String accessToken, String refreshToken, long expiresIn) {
return new AuthResponse(accessToken, refreshToken, "Bearer", expiresIn);
}
}
public record RefreshTokenRequest(
@NotBlank(message = "Refresh Token không được để trống")
String refreshToken
) {}
}
3. AuthService Xử Lý Đăng Ký, Đăng Nhập & Sinh Token
package com.devpool.service;
import com.devpool.dto.AuthDtos.*;
import com.devpool.entity.User;
import com.devpool.entity.UserRole;
import com.devpool.exception.DuplicateEmailException;
import com.devpool.repository.UserRepository;
import com.devpool.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
public AuthResponse register(RegisterRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new DuplicateEmailException("Email này đã được đăng ký: " + request.email());
}
String hashedPassword = passwordEncoder.encode(request.password());
User user = new User(request.fullName(), request.email(), hashedPassword);
user.setRole(UserRole.ROLE_CUSTOMER); // Mặc định là Khách hàng
User saved = userRepository.save(user);
String accessToken = jwtTokenProvider.generateAccessToken(saved.getId(), saved.getEmail(), List.of(saved.getRole().name()));
String refreshToken = jwtTokenProvider.generateRefreshToken(saved.getId());
return AuthResponse.of(accessToken, refreshToken, 900); // 15 phút (900s)
}
@Transactional(readOnly = true)
public AuthResponse login(LoginRequest request) {
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new BadCredentialsException("Email hoặc mật khẩu không chính xác"));
if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) {
throw new BadCredentialsException("Email hoặc mật khẩu không chính xác");
}
String accessToken = jwtTokenProvider.generateAccessToken(user.getId(), user.getEmail(), List.of(user.getRole().name()));
String refreshToken = jwtTokenProvider.generateRefreshToken(user.getId());
return AuthResponse.of(accessToken, refreshToken, 900);
}
}
4. AuthController Tiếp Nhận Requests
package com.devpool.controller;
import com.devpool.dto.AuthDtos.*;
import com.devpool.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(authService.register(request));
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
return ResponseEntity.ok(authService.login(request));
}
}
5. Tóm Tắt & Key Takeaways
- Module Authentication được thiết kế hoàn toàn theo mô hình Stateless JWT, không tạo Session trên server.
- Mật khẩu được mã hóa an toàn bằng BCrypt.
- Các Endpoint của Auth Controller trả về cặp token tiêu chuẩn kèm mã HTTP Status chuẩn mực (
201 CREATED,200 OK).