Việc bắt đầu tư những kiến thức nền tảng nhất sẽ giúp chúng ta hiểu sâu và có thể tạo ra những thứ ở tầng cao hơn một cách dễ dàng. Trong bài này Dũng sẽ cùng các bạn sử dụng những kiến thức đã học được để tạo ra một website đơn giản gồm các chức năng như sau nhé:

  1. Cho phép người dùng đăng ký.
  2. Cho phép người dùng login.
  3. Khi người dùng login thì thành công thì cho họ vào trang chủ với dòng chữ chào mừng.

Chuẩn bị

Bạn sẽ cần khởi tạo một cơ sở dữ liệu mastering_spring_boot, nếu có rồi bạn hãy xoá đi tạo lại nhé, mã nguồn tạo cơ sở dữ liệu sẽ như sau:

CREATE SCHEMA `mastering_spring_boot` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin ;

Tiếp theo chúng ta sẽ tạo ra hai bảng để lưu thông tin ngừoi dùng và access token.

CREATE SCHEMA `mastering_spring_boot` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin ;

CREATE TABLE IF NOT EXISTS users (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT,
    `username` varchar(60) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `email` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `password` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `display_name` varchar(120) COLLATE utf8mb4_unicode_520_ci,
    `status` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `created_at` datetime NOT NULL,
    `updated_at` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `key_username` (`username`),
    UNIQUE KEY `key_email` (`email`),
    INDEX `index_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

CREATE TABLE IF NOT EXISTS access_tokens (
    `access_token` varchar(120) COLLATE utf8mb4_unicode_520_ci NOT NULL,
    `user_id` bigint unsigned NOT NULL,
    PRIMARY KEY (`access_token`),
    INDEX `index_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

Khởi tạo module

Như thường lệ chúng ta sẽ tạo một module có tên simple-website cho bài viết này.

Cấu hình dự án

Việc cấu hình sẽ là tổng hợp lại các thư viện phụ thuộc vào chung tập tin simple-website/pom.xml mà thôi

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>vn.techmaster</groupId>
        <artifactId>mastering-spring-boot</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>simple-website</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>${dbcp2.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>${tomcat.version}</version>
        </dependency>
    </dependencies>
</project>

Chúng ta sẽ đưa vào tập tin pom các phụ thuộc:

  1. Cho phần webmvc: spring-webmc, tomcat.
  2. Cho phần cơ sở dữ liệu: spring-jdbc, commons-dbcp2.

Cài đặt các lớp cấu hình

Chúng ta sẽ tạo ra lớp JdbcConfig để cấu hình cho cơ sở dữ liệu:

package vn.techmaster.simple_module.config;

import lombok.Setter;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Setter
@Configuration
@PropertySource("classpath:application.properties")
public class JdbcConfig {

    @Value("${jdbc.driverClassName}")
    private String jdbcDriverClassName;

    @Value("${jdbc.url}")
    private String jdbcUrl;

    @Value("${jdbc.username}")
    private String jdbcUsername;

    @Value("${jdbc.password}")
    private String jdbcPassword;

    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(jdbcDriverClassName);
        dataSource.setUrl(jdbcUrl);
        dataSource.setUsername(jdbcUsername);
        dataSource.setPassword(jdbcPassword);
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

Cũng chỉ là việc chúng ta sao chép từ bài viết trước và đổi tên lớp cho hợp lý mà thôi.
Tương tự, chúng ta cũng sao chép lớp WebAppInitializer từ bài spring webmvc sang:

package vn.techmaster.simple_module.config;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebAppInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext context =
            new AnnotationConfigWebApplicationContext();
        context.setConfigLocation("vn.techmaster.simple_module");
        ServletRegistration.Dynamic dispatcher = servletContext
            .addServlet("dispatcher", new DispatcherServlet(context));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

Chúng ta sẽ vẫn có lớp WebConfig tuy nhiên nó cũng sẽ có nhiều sự khác biệt:

package vn.techmaster.simple_module.config;

import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import vn.techmaster.simple_module.interceptor.AuthenticationInterceptor;
import vn.techmaster.simple_module.interceptor.LoggingInterceptor;

@Configuration
@EnableWebMvc
@AllArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final LoggingInterceptor loggingInterceptor;
    private final AuthenticationInterceptor authenticationInterceptor;

    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/templates/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**")
            .addResourceLocations("/static/css/");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor);
        registry.addInterceptor(authenticationInterceptor)
            .addPathPatterns("/**")
            .excludePathPatterns("/login", "/register", "/css/**", "/js/**");
    }
}

Lớp này bây giờ khác phức tạp khi chúng ta bổ sung thêm hai hàm:

  1. addResourceHandlers: Cho phép chúng ta cấu hình các thư mục chứa các tập tin tĩnh sẽ được vào cho spring quản lý. Cụ thể là việc nó sẽ lấy các tập tin css trong thư mục /src/main/resources/static/css và đưa vào danh sách quản lý uri để cho phép client truy cập vào các tập tin này và điều này cũng giúp lập trình viên không phải tạo ra các lớp controller cho phần tập tin tĩnh này nữa, nó khá tiện.
  2. addInterceptors: Cho phép bổ sung thêm các lớp đánh chặn các yêu cầu để kiểm tra tính hợp lệ của yêu cầu, 1 lát nữa chúng ta sẽ cài đặt hai lớp đánh chặn để hiển thị log và xác thực. Bạn có thể xem hình dưới đây để hiểu được vai trò của các lớp Intercetor.

Ngoài ra chúng ta cũng không được quên việc tạo ra tập tin application trong src/main/resources với nội dung như sau:

Ngoài ra chúng ta cũng không được quên việc tạo ra tập tin application trong src/main/resources với nội dung như sau:

Cài đặt các view

Chúng ta sẽ cài đặt giao diện cho màn hình đăng ký

Thông qua tập tin src/main/resources/templates/register mã nguồn như sau:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Register</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <div class="login-form">
    <form method="post" action="/register" id="registerForm">
      <h1 class="h3 mb-3 fw-normal text-center">Sign up</h1>
      <div class="form-floating mb-3">
        <input type="text" name="username" class="form-control" placeholder="Username">
        <label for="floatingInput">Username</label>
      </div>
      <div class="form-floating mb-3">
        <input type="email" name="email" class="form-control" placeholder="Email">
        <label for="floatingInput">Email</label>
      </div>
      <div class="form-floating mb-3">
        <input type="text" name="displayName" class="form-control"  placeholder="Display Name">
        <label for="floatingInput">Display Name</label>
      </div>
      <div class="form-floating mb-3">
        <input type="password" name="password" class="form-control" placeholder="Password">
        <label for="floatingPassword">Password</label>
      </div>
      <button class="w-100 btn btn-lg btn-primary" type="submit">Sign up</button>
      <p class="mt-3 mb-3 text-center">Have an account? <a href="/login">Login Now!</a></p>
      <p class="mt-5 mb-3 text-muted text-center">&copy; 2024 Techmaster.vn</p>
    </form>
  </div>
</body>
</html>

Cài đặt giao diện cho màn hình login

Thông qua tập tin src/main/resources/templates/login với mã nguồn như sau:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Login</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <div class="login-form">
    <form method="post" action="/login" id="loginForm">
      <h1 class="h3 mb-3 fw-normal text-center">Sign in</h1>
      <div class="form-floating mb-3">
        <input type="text" name="username" class="form-control" placeholder="Username">
        <label for="floatingInput">Username</label>
      </div>
      <div class="form-floating mb-3">
        <input type="password" name="password" class="form-control" placeholder="Password">
        <label for="floatingPassword">Password</label>
      </div>
      <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
      <p class="mt-3 mb-3 text-center">Don't have an account? <a href="/register">Register Now!</a></p>
      <p class="mt-5 mb-3 text-muted text-center">&copy; 2024 Techmaster.vn</p>
    </form>
  </div>
</body>
</html>

Cài đặt trang chủ với dòng chữ chào mừng

Thông qua tập tin src/main/resources/templates/home với mã nguồn như sau:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Hom</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <h3>Welcome <span class="text-primary">${user.displayName}</span>!</h3>
</body>
</html>

Ở đây chúng ta đang sử dụng thư viện boostrap 5 cho phần css, đây là một thư viện hỗ trợ khá nhiều các lớp css có sẵn và cũng đã có từ khá lâu nên mình cũng hay sử dụng nó.

Cài đặt các lớp nghiệp vụ

Nghiệp vụ của bài này tương đối phức tạp với nhiều tầng, chúng ta sẽ chia ra thành các tầng sau để cài đặt.

  1. Tầng Repository chứa các lớp cầu nối để giao tiếp với cơ sở dữ liệu.
  2. Các lớp Interceptor để đánh chặn và kiểm tra yêu cầu của người dùng.
  3. Các lớp Controller để tiếp nhận yêu cầu của người dùng.

Cài đặt các lớp Repository

Chúng ta sẽ cần cài đặt hai lớp để tương tác với hai bảng là:

  1. users: chứa thông tin người dùng.
  2. access_tokens: Chứa thông tin access token của người dùng.

Cài đặt lớp UserRepository

Trước tiên chúng ta cần cài đặt lớp UserRowMapper với mã nguồn như sau:

package vn.techmaster.simple_module.mapper;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import vn.techmaster.simple_module.entity.User;
import vn.techmaster.simple_module.entity.UserStatus;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class UserRowMapper implements RowMapper<User> {

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getLong("id"));
        user.setUsername(rs.getString("username"));
        user.setEmail(rs.getString("email"));
        user.setPassword(rs.getString("password"));
        user.setDisplayName(rs.getString("display_name"));
        user.setStatus(UserStatus.valueOf(rs.getString("status")));
        user.setCreatedAt(
            LocalDateTime.parse(
                rs.getString("created_at"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            )
        );
        user.setCreatedAt(
            LocalDateTime.parse(
                rs.getString("updated_at"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            )
        );
        return user;
    }
}

Sau đó chúng ta sẽ cài đặt lớp UserRepository với mã nguồn như sau:

package vn.techmaster.simple_module.repository;

import lombok.AllArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import vn.techmaster.simple_module.entity.User;
import vn.techmaster.simple_module.mapper.UserRowMapper;

import java.time.format.DateTimeFormatter;

@Repository
@AllArgsConstructor
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;
    private final UserRowMapper userRowMapper;

    public void save(User user) {
        String sql = "INSERT INTO " +
            "users (username, email, password, display_name, status, created_at, updated_at) " +
            "VALUES (?, ?, ?, ?, ?, ?, ?)";
        jdbcTemplate.update(
            sql,
            user.getUsername(),
            user.getEmail(),
            user.getPassword(),
            user.getDisplayName(),
            user.getStatus().toString(),
            user.getCreatedAt().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            ),
            user.getUpdatedAt().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            )
        );
    }

    public User findById(long userId) {
        String sql = "SELECT * FROM users WHERE id = ?";
        try {
            return jdbcTemplate.queryForObject(sql, userRowMapper, userId);
        } catch (Exception e) {
            return null;
        }
    }

    public User findByUsername(String username) {
        String sql = "SELECT * FROM users WHERE username = ?";
        try {
            return jdbcTemplate.queryForObject(sql, userRowMapper, username);
        } catch (Exception e) {
            return null;
        }
    }
}

Cài đặt lớp AccessTokenRepository

Tương tự như trên, chúng ta cũng cần cài đặt lớp AccessTokenRowMapper trước:

package vn.techmaster.simple_module.mapper;

import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
import vn.techmaster.simple_module.entity.AccessToken;

import java.sql.ResultSet;
import java.sql.SQLException;

@Component
public class AccessTokenRowMapper implements RowMapper<AccessToken> {

    @Override
    public AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {
        AccessToken accessToken = new AccessToken();
        accessToken.setAccessToken(rs.getString("access_token"));
        accessToken.setUserId(rs.getLong("user_id"));
        return accessToken;
    }
}

Và sau đó là lớp AccessTokenRepository:

package vn.techmaster.simple_module.repository;

import lombok.AllArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import vn.techmaster.simple_module.entity.AccessToken;
import vn.techmaster.simple_module.mapper.AccessTokenRowMapper;

import java.util.List;
import java.util.UUID;

@Repository
@AllArgsConstructor
public class AccessTokenRepository {

    private final JdbcTemplate jdbcTemplate;
    private final AccessTokenRowMapper accessTokenRowMapper;

    public void save(AccessToken accessToken) {
        String sql = "INSERT INTO " +
            "access_tokens (access_token, user_id) " +
            "VALUES (?, ?)";
        jdbcTemplate.update(
            sql,
            accessToken.getAccessToken(),
            accessToken.getUserId()
        );
    }

    public AccessToken findOrCreateByUserId(long userId) {
        String sql = "SELECT * FROM access_tokens WHERE user_id = ?";
        try {
            List<AccessToken> accessTokens = jdbcTemplate
                .query(sql, accessTokenRowMapper, userId);
            AccessToken accessToken = accessTokens.isEmpty()
                ? null
                : accessTokens.get(0);
            if (accessToken == null) {
                accessToken = new AccessToken();
                accessToken.setAccessToken(UUID.randomUUID().toString());
                accessToken.setUserId(userId);
                save(accessToken);
            }
            return accessToken;
        } catch (Exception e) {
            return null;
        }
    }

    public AccessToken findByAccessToken(String accessToken) {
        if (accessToken == null || accessToken.isEmpty()) {
            return null;
        }
        String sql = "SELECT * FROM access_tokens WHERE access_token = ?";
        try {
            return jdbcTemplate.queryForObject(sql, accessTokenRowMapper, accessToken);
        } catch (Exception e) {
            return null;
        }
    }
}

Lưu ý một chút là ở đây chúng ta có hàm findOrCreateByUserId, hàm này sẽ tìm kiếm một bản ghi access token cho một người dùng thông qua mã người dùng (userId), nếu chưa có bản ghi nào nó sẽ tạo mới một bản ghi.

Cài đặt các lớp Interceptor

Như đã nói ở trên chúng ta sẽ tạo ra hai lớp đánh chặn. Lớp đầu tiên là LoggingInterceptor để in ra thông tin yêu cầu và phản hồi cho người dùng để giúp chúng ta dễ dàng gỡ lỗi hơn, mã nguồn của nó sẽ như sau:

package vn.techmaster.simple_module.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class LoggingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
    ) throws Exception {
        System.out.println("Request: " + request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        ModelAndView modelAndView
    ) throws Exception {
        System.out.println(
            "Response: " + request.getRequestURI() + " - " + response.getStatus()
        );
    }
}

Tiếp theo chúng ta sẽ cài đặt một lớp đánh chặn để xác thực yêu cầu của người dùng với mã nguồn như sau:

package vn.techmaster.simple_module.interceptor;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import vn.techmaster.simple_module.entity.AccessToken;
import vn.techmaster.simple_module.repository.AccessTokenRepository;

@Component
@AllArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {

    private final AccessTokenRepository accessTokenRepository;

    @Override
    public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
    ) throws Exception {
        String accessTokenValue = getAccessToken(request);
        AccessToken accessToken = accessTokenRepository
            .findByAccessToken(accessTokenValue);
        if (accessToken == null) {
            response.sendRedirect("/login");
            return false;
        }
        request.setAttribute("userId", accessToken.getUserId());
        return true;
    }

    private String getAccessToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        String accessToken = null;
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("X-AccessToken".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

Ở đây chúng ta sẽ lấy ra access token của người dùng từ cookie, lưu ý rằng cookie sẽ được trình duyệt tự động gửi cho server cho mọi request. Chúng ta sẽ kiểm tra xem trong cơ sở dữ liệu có bản ghi nào tương ứng với access token không, nếu có chúng ta return true để cho yêu cầu đến được controller, ngược lại chúng ta sẽ chuyển hướng người dùng sang trang đăng nhập. Thiết kế này sẽ giúp chúng ta giảm được cự kỳ nhiều công sức vì có thể tập trung xử lý việc xác thực ở một nơi thay vì mỗi controller chúng ta lại phải gọi xác thực 1 lần.

Cài đặt các lớp Controller

Trước tiên chúng ta sẽ cài đặt một lớp để mã hoá mật khẩu của người dùng cho bảo mật:

package vn.techmaster.simple_module.service;

import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@Service
public class PasswordService {

    public String hashPassword(String password) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(
            password.getBytes(StandardCharsets.UTF_8)
        );
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }
}

Tiếp theo chúng ta sẽ cài đặt hai controller một là AuthenticationController dành cho các trang xác thực như đăng ký hay login với mã nguồn như sau:

package vn.techmaster.simple_module.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import vn.techmaster.simple_module.entity.AccessToken;
import vn.techmaster.simple_module.entity.User;
import vn.techmaster.simple_module.entity.UserStatus;
import vn.techmaster.simple_module.repository.AccessTokenRepository;
import vn.techmaster.simple_module.repository.UserRepository;
import vn.techmaster.simple_module.service.PasswordService;

import java.time.LocalDateTime;

@Controller
@RequestMapping("/")
@AllArgsConstructor
public class AuthenticationController {

    private final PasswordService passwordService;
    private final AccessTokenRepository accessTokenRepository;
    private final UserRepository userRepository;

    @GetMapping("login")
    public ModelAndView loginGet(Model model) {
        return new ModelAndView("login");
    }

    @PostMapping("login")
    public String loginPost(
        @RequestParam("username") String username,
        @RequestParam("password") String password,
        HttpServletResponse response
    ) throws Exception {
        User user = userRepository.findByUsername(username);
        String hashPassword = passwordService.hashPassword(password);
        if (hashPassword.equals(user.getPassword())) {
            AccessToken accessToken = accessTokenRepository.findOrCreateByUserId(
                user.getId()
            );
            Cookie cookie = new Cookie(
                "X-AccessToken",
                accessToken.getAccessToken()
            );
            cookie.setPath("/");
            response.addCookie(cookie);
            return "redirect:/";
        }
        return "redirect:/login";
    }

    @GetMapping("register")
    public ModelAndView registerGet(Model model) {
        return new ModelAndView("register");
    }

    @PostMapping("register")
    public String registerPost(
        @RequestParam("username") String username,
        @RequestParam("email") String email,
        @RequestParam("displayName") String displayName,
        @RequestParam("password") String password
    ) throws Exception {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            user = new User();
            user.setUsername(username);
            user.setEmail(email);
            user.setDisplayName(displayName);
            user.setPassword(passwordService.hashPassword(password));
            user.setStatus(UserStatus.ACTIVATED);
            LocalDateTime now = LocalDateTime.now();
            user.setCreatedAt(now);
            user.setUpdatedAt(now);
            userRepository.save(user);
        }
        return "redirect:/login";
    }
}

Lớp này không chỉ cung cấp các mã nguồn cho việc trả về trang web mà nó còn cung cấp các phương thức dạng thực thi để đăng ký và xác thực người dùng.
Tiếp theo chúng ta sẽ tạo ra lớp HomeController cho trang chủ:

package vn.techmaster.simple_module.controller;

import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import vn.techmaster.simple_module.repository.UserRepository;

@Controller
@RequestMapping("/")
@AllArgsConstructor
public class HomeController {

    private final UserRepository userRepository;

    @GetMapping
    public ModelAndView home(HttpServletRequest request) {
        long userId = (long) request.getAttribute("userId");
        ModelAndView modelAndView = new ModelAndView("home");
        modelAndView.addObject(
            "user",
            userRepository.findById(userId)
        );
        return modelAndView;
    }
}

Khởi động chương trình

Không có gì thay đổi, chúng ta vẫn tạo ra một lớp SimpleWebsiteStartUp với mã nguồn như sau:

package vn.techmaster.simple_module;

import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;

import java.io.File;

public class SimpleWebsiteStartUp {

    public static void main(String[] args) throws Exception {
        String webappDirLocation = "src/main/resources/";
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);
        tomcat.addWebapp(
            "/",
            new File(webappDirLocation).getAbsolutePath()
        );
        tomcat.start();
        System.out.println("server started");
        tomcat.getServer().await();
    }
}

Chạy nó và chúng ta sẽ thấy trang đăng ký được mở ra. Sau khi bạn đăng ký thành công thì nó sẽ chuyển bạn đến trang login, sau khi bạn login thành công thì bạn sẽ được truy cập vào trang chủ.

Cũng không quá khó đúng không? Vì chúng ta đã nắm chắc các kiến thức ở tầng dưới rồi, đó là nguyên nhân vì sao chúng ta cần học từ gốc lên ngọn sẽ tốt hơn.

Tổng kết

Như vậy chúng ta đã cùng nhau khởi tạo một website đơn giản thông qua việc:

  1. Cài đặt các lớp cấu hình.
  2. Tạo ra các view.
  3. Cài đặt các lớp nghiệp vụ.

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
  4. Đăng ký khoá học java spring fullstack https://java.techmaster.vn/ của techmaster nhé.