Sau khi đã có lớp UserService và các hàm cài đặt các nghiệp vụ cần thiết cho việc khởi tạo, kích hoạt hay lấy thông tin người dùng, giờ là lúc chúng ta có thể cài đặt API xác thực đầu tiên đó là API cho phép người dùng đăng ký.

Thiết kế sơ đồ lớp cho AuthenticationController

Chúng ta có thể thiết kế sơ đồ lớp AuthenticationController như sau:

Trong sơ đồ này chúng ta có:

  1. Lớp AuthenticationController: Tiếp nhận yêu cầu đăng ký do client gửi đến, kiểm tra yêu cầu thông qua lớp AuthenticationValidator, sử dụng RequestToModelConverter để chuyển đổi yêu cầu đăng ký thành SaveUserModel sau đó gọi đến UserService để thêm người dùng.
  2. Lớp AuthenticationValidator: Kiểm tra yêu cầu đăng ký.
  3. Lớp RequestToModelConverter: Chuyển đổi yêu cầu từ client RegisterRequest sang SaveUserModel.
  4. Lớp UserService: Để xử lý nghiệp vụ thêm mới người dùng vào cơ sở dữ liệu…

Mã nguồn cài đặt

Cài đặt hàm xác thực yêu cầu đăng ký người dùng

Chúng ta sẽ cài đặt hàm validate bên trong lớp AuthenticationValidator như sau:

package vn.techmaster.login.validator;

import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import vn.techmaster.login.entity.UserStatus;
import vn.techmaster.login.exception.HttpBadRequestException;
import vn.techmaster.login.model.UserModel;
import vn.techmaster.login.request.RegisterRequest;
import vn.techmaster.login.service.UserService;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static vn.techmaster.login.constant.LoginConstants.*;

@Component
@AllArgsConstructor
public class AuthenticationValidator {

    private final UserService userService;

    public void validate(RegisterRequest request) {
        Map<String, String> errors = new HashMap<>();
        String username = request.getUsername();
        if (username == null || username.isBlank()) {
            errors.put("username", "required");
        } else if (username.length() < MIN_USERNAME_LENGTH) {
            errors.put("username", "tooShort");
        } else if (username.length() > MAX_USERNAME_LENGTH) {
            errors.put("username", "tooLong");
        } else if (userService.getUserByUsername(username) != null) {
            errors.put("username", "duplicated");
        }
        String email = request.getEmail();
        if (email == null || email.isBlank()) {
            errors.put("email", "required");
        } else if (email.length() < MIN_EMAIL_LENGTH) {
            errors.put("email", "tooShort");
        } else if (email.length() > MAX_EMAIL_LENGTH) {
            errors.put("email", "tooLong");
        } else if (userService.getUserByEmail(email) != null) {
            errors.put("email", "duplicated");
        }
        String password = request.getPassword();
        if (password == null || password.isBlank()) {
            errors.put("password", "required");
        } else if (password.length() < MIN_PASSWORD_LENGTH) {
            errors.put("password", "tooShort");
        } else if (password.length() > MAX_PASSWORD_LENGTH) {
            errors.put("password", "tooLong");
        }
        String displayName = request.getPassword();
        if (displayName == null || displayName.isBlank()) {
            errors.put("displayName", "required");
        } else if (displayName.length() < MIN_DISPLAY_NAME_LENGTH) {
            errors.put("displayName", "tooShort");
        } else if (displayName.length() > MAX_DISPLAY_NAME_LENGTH) {
            errors.put("displayName", "tooLong");
        }
        if (!errors.isEmpty()) {
            throw new HttpBadRequestException(errors);
        }
    }
}

Ở đây chúng ta thực hiện những công việc sau:

  1. Kiểm tra các trường được yêu cầu.
  2. Kiểm tra độ dài của các trường xem có hợp lệ không.
  3. Kiểm tra tên đăng nhập và email đã có người đăng ký chưa.
  4. Nếu có lỗi thì ném ra HttpBadRequestException.
    Ở đây có lẽ mình sẽ cài đặt khác nhiều bạn khi ném ra HttpBadRequestException thay vì trả về kết quả boolean cho hàm vì giá trị boolean sẽ không đủ để client nhận biết được chính xác lỗi để báo cho client, thêm vào đó spring cũng cung cấp cho chúng ta cơ chế xử lý exception tập trung nên rất tiện lợi. Mã nguồn của HttpBadRequestException sẽ như sau:
package vn.techmaster.login.exception;

import lombok.Getter;

@Getter
public class HttpBadRequestException extends RuntimeException {

    private final Object errors;

    public HttpBadRequestException(Object errors) {
        super("Bad request");
        this.errors = errors;
    }
}

Mã nguồn xử lý exception tập trung sẽ như sau:

package vn.techmaster.login.advice;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import vn.techmaster.login.exception.HttpBadRequestException;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(HttpBadRequestException.class)
    public ResponseEntity<Object> handle(
        HttpBadRequestException e
    ) {
        return ResponseEntity
            .badRequest()
            .body(e.getErrors());
    }
}

Khai báo lớp RequestToModelConverter

Chúng ta sẽ khai báo lớp RequestToModelConverter với hàm chuyển đổi từ RegisterRequest sang SaveUserModel như sau:

package vn.techmaster.login.converter;

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import vn.techmaster.login.model.SaveUserModel;
import vn.techmaster.login.request.RegisterRequest;

@Component
@AllArgsConstructor
public class RequestToModelConverter {

    public SaveUserModel toModelSaveUserModel(
        RegisterRequest request
    ) {
        return SaveUserModel.builder()
            .username(request.getUsername())
            .email(request.getEmail())
            .password(request.getPassword())
            .displayName(request.getDisplayName())
            .build();
    }
}

Mã nguồn của lớp RegisterRequest sẽ như sau:

package vn.techmaster.login.request;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RegisterRequest {
    private String username;
    private String email;
    private String password;
    private String displayName;
}

Nó sẽ chứa các thông tin về tên đăng nhập, email, mật khẩu và tên hiển thị.

Cài đặt API đăng ký

Sau khi đã có đủ các thành phần, chúng ta có thể cài đặt API đăng ký với hàm registerPost nằm trong lớp AuthenticationController như sau:

package vn.techmaster.login.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import vn.techmaster.login.converter.RequestToModelConverter;
import vn.techmaster.login.request.LoginRequest;
import vn.techmaster.login.request.RegisterRequest;
import vn.techmaster.login.response.UserLoginResponse;
import vn.techmaster.login.service.UserAccessTokenService;
import vn.techmaster.login.service.UserService;
import vn.techmaster.login.validator.AuthenticationValidator;

import static vn.techmaster.login.constant.LoginConstants.ACCESS_TOKEN_EXPIRED_IN_HOUR;
import static vn.techmaster.login.constant.LoginConstants.COOKIE_NAME_ACCESS_TOKEN;

@RestController
@RequestMapping("/api/v1")
@AllArgsConstructor
public class AuthenticationController {

    private final UserService userService;
    private final RequestToModelConverter requestToModelConverter;
    private final AuthenticationValidator authenticationValidator;

    @PostMapping("/register")
    public ResponseEntity<?> registerPost(
        @RequestBody RegisterRequest request
    ) {
        authenticationValidator.validate(request);
        userService.addUser(
            requestToModelConverter.toModelSaveUserModel(request)
        );
        return ResponseEntity.noContent().build();
    }
}

Đúng như những gì chúng ta đã thiết kế, hàm registerPost không xử lý nghiệp vụ gì phức tạp, nó chỉ đơn giản là gọi đến các lớp thành phần để kiểm tra yêu cầu và tạo người dùng.
Ở đây cũng có một chút đặc biệt mà mình tin một số bạn thắc mắc:

  1. Tên hàm hơi kỳ la: Trên thực tế mình sử dụng quy tắc đặt tên tham khảo của OpenAPI, cú pháp của hàm là tên API viết ở dạng camel case kết hợp với phương thức, ví dụ ở đây register là tên API còn Post là phương thức POST của API.
  2. Hàm trả về no content thay vì 200 và một body nào đó: Ở đây mình không thấy cần phải trả về dữ liệu gì nên không miễn cưỡng trả về 200, mà trả về no content để client xử lý cho đơn giản, vì một số client sử dụng thư viện nhận được 200 mà không có body sẽ báo lỗi.

Khởi chạy chương trình

Để khởi chạy chương trình, chúng ta có thể khởi tạo lớp SpringBootLoginStartUp với mã nguồn như sau:

package vn.techmaster.login;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@EnableCaching
@SpringBootApplication
public class SpringBootLoginStartUp {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootLoginStartUp.class);
    }
}

Chúng ta có thể sử dụng Postman để gọi API đăng ký và sẽ nhận được kết quả như sau:

Tổng kết

Như vậy chúng ta đã cùng nhau tạo ra API đăng ký, trong bài tiếp theo, chúng ta sẽ cài đặt các API còn lại nhé.


Cám ơn bạn đã quan tâm đến bài viết|video này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a