Bài 20.3: Module 1: Xây Dựng Authentication, JWT Token Pair & Role-Based Access Control

~3 phút đọc551 từ

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:
    1. Đăng ký tài khoản mới (Mã hóa BCrypt, kiểm tra trùng email).
    2. Đăng nhập (Xác thực mật khẩu và sinh cặp Access Token + Refresh Token).
    3. Cấp lại Token (Refresh Token Rotation).
    4. 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

  1. Module Authentication được thiết kế hoàn toàn theo mô hình Stateless JWT, không tạo Session trên server.
  2. Mật khẩu được mã hóa an toàn bằng BCrypt.
  3. 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).