Cài đặt xác thực đăng nhập đơn lẻ với SpringBoot 4.0.1, SpringSecurity 6.2 và Redis

Phiên bản các thành phần

Các thành phần chính:

Thành phầnPhiên bảnMô tả
JDK25Hỗ trợ SpringBoot4
SpringBoot4.xPhiên bản mới
SpringSecurity6.2Hỗ trợ SpringBoot4
Redis8.4Lưu trữ session
MySQL8.0Cơ sở dữ liệu cấu hình

Mã POM dự án:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>4.0.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>ssoredis-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>ssoredis-demo</name>
	<description>Dự án mẫu cho Spring Boot 4.0.1 và JDK25</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>25</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc</artifactId>
		</dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>4.0.0</version>
		</dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter-test</artifactId>
			<version>4.0.0</version>
			<scope>test</scope>
		</dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
            <version>3.5.15</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>3.5.15</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.20.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310 -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.20.1</version>
        </dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
	</build>

</project>

Triển khai mã hóa

1. Cấu hình cơ sở dữ liệu

Sử dụng MySQL làm cơ sở dữ liệu cấu hình, Redis làm nơi lưu trữ phiên làm việc (hỗ trợ duy trì phiên sau khi khởi động lại), tệp application.properties:

spring.application.name=ssoredis-demo
server.servlet.context-path=/app
# hỗ trợ luồng ảo
spring.threads.virtual.enabled=true
# tập tin cấu hình hoạt động
spring.profiles.active=development

# cấu hình khởi tạo
spring.mvc.servlet.load-on-startup=10
spring.main.allow-bean-definition-overriding=true
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.servlet.multipart.max-file-size=-1
spring.servlet.multipart.max-request-size=-1
spring.mvc.static-path-pattern=/**
spring.mvc.view.suffix=.html
spring.web.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:www/
spring.web.resources.add-mappings=true

# Cấu hình cơ sở dữ liệu
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.pool-name=hikari-jdbc-pool
spring.datasource.hikari.minimum-idle=1
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.validation-timeout=3000
spring.datasource.hikari.max-lifetime=600000
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.connection-test-query=SELECT 1

# Cấu hình MybatisPlus
mybatis-plus.mapper-locations=classpath*:mybatis/mapper/*.xml
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.cache-enabled=false
#logging.level.com.example.ssoredis.dao.mapper=debug
#logging.level.org.apache.ibatis=debug
# xuất SQL ra console tiêu chuẩn
# mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# Cấu hình nhật ký
logging.config=classpath:logback-spring.xml

# Phiên bảo mật
server.servlet.session.timeout=30d
server.servlet.session.cookie.max-age=30d
spring.session.redis.namespace=ssoredis:session

Tệp application-development.properties:

server.port=8080

# Cơ sở dữ liệu
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/ssoredisdb?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=

# Redis
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.database=0

Tạo bảng thử nghiệm trong cơ sở dữ liệu:

CREATE TABLE test_data (
    id BIGINT PRIMARY KEY,
    full_name VARCHAR(200),
    age_value INT,
    monthly_salary DECIMAL(10,2),
    creation_date DATETIME
);
CREATE TABLE account_info (
    id BIGINT PRIMARY KEY,
    user_name VARCHAR(200),
    encrypted_password VARCHAR(200),
    access_role VARCHAR(200),
    creation_date DATETIME
);

Tạo dữ liệu mẫu:

import random
import time
import mysql.connector
from mysql.connector import Error
from datetime import datetime, timedelta
import logging
import uuid


class DataPopulationTool:
    def __init__(self, host='localhost', database='test_db',
                 user='root', password='password', port=3306):
        """
        Khởi tạo cấu hình kết nối MySQL

        Args:
            host: Địa chỉ máy chủ MySQL
            database: Tên cơ sở dữ liệu
            user: Tên người dùng
            password: Mật khẩu
            port: Cổng
        """
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port
        self.connection = None

        # Thiết lập nhật ký
        logging.basicConfig(level=logging.INFO,
                            format='%(asctime)s - %(levelname)s - %(message)s')
        self.logger = logging.getLogger(__name__)

    def connect(self):
        """Kết nối đến cơ sở dữ liệu MySQL"""
        try:
            self.connection = mysql.connector.connect(
                host=self.host,
                database=self.database,
                user=self.user,
                password=self.password,
                port=self.port
            )
            if self.connection.is_connected():
                self.logger.info(f"Kết nối thành công đến cơ sở dữ liệu MySQL: {self.database}")
                return True
        except Error as e:
            self.logger.error(f"Kết nối thất bại: {e}")
            return False

    def disconnect(self):
        """Ngắt kết nối cơ sở dữ liệu"""
        if self.connection and self.connection.is_connected():
            self.connection.close()
            self.logger.info("Đã đóng kết nối cơ sở dữ liệu")

    def generate_full_name(self):
        """Tạo tên ngẫu nhiên"""
        surnames = ['Trần', 'Lê', 'Phạm', 'Hoàng', 'Vũ', 'Phan', 'Đặng', 'Bùi', 'Đỗ', 'Hồ',
                   'Nguyễn', 'Lý', 'Trương', 'Mai', 'Lâm', 'Phùng', 'Thái', 'Tăng', 'Vương', 'Chung',
                   'Hà', 'Tô', 'Cao', 'Dương', 'Đinh', 'Lương', 'Tạ', 'Lại', 'Ngô', 'Văn',
                   'Đoàn', 'Thạch', 'Tiêu', 'Lưu', 'Sơn', 'Yên', 'Quách', 'Tôn', 'Phó', 'Từ',
                   'Hán', 'Vi', 'Doãn', 'Thiệu', 'Biện', 'Mẫn', 'Liễu', 'Lục', 'Kim', 'Sa',
                   'Ông', 'Sử', 'Phan', 'Lục', 'Trác', 'Hồng', 'Sầm', 'Châu', 'Nhiệm', 'Hữu']
        given_names = ['Minh', 'Hùng', 'Lân', 'Quân', 'Dũng', 'Kiệt', 'Đạt', 'Nam', 'Khánh', 'Cường',
                      'Phong', 'Long', 'Hải', 'Duy', 'Phát', 'Bình', 'Phú', 'Thái', 'Khoa', 'Tuấn',
                      ['Minh', 'Anh'], ['Thu', 'Hương'], ['Thị', 'Mỹ'], ['Ngọc', 'Diễm'], ['Kim', 'Xuyến'], ['Phương', 'Dung'], ['Thúy', 'Vy'], ['Lệ', 'Hằng'], ['Bích', 'Loan'], ['Hoa', 'Phượng'],
                      ['Anh', 'Thư'], ['Mộng', 'Tuyền'], ['Thanh', 'Lan'], ['Xuân', 'Nghi'], ['Như', 'Quỳnh'], ['Gia', 'Hân'], ['Tú', 'Anh'], ['Bảo', 'Trân'], ['Diễm', 'My'], ['Huyền', 'Trang'],
                      ['Tâm', 'Đức'], ['Thùy', 'Linh'], ['Quỳnh', 'Anh'], ['Mỹ', 'Duyên'], ['Ái', 'Thư'], ['Ngọc', 'Trâm'], ['Phượng', 'Hồng'], ['Tú', 'Uyên'], ['Diệu', 'Hương'], ['Băng', 'Tâm']]

        return random.choice(surnames) + random.choice(given_names)

    def generate_creation_time(self, days_back=365):
        """Tạo thời gian tạo ngẫu nhiên (trong days_back ngày gần đây)"""
        now = datetime.now()
        random_days = random.randint(0, days_back)
        random_hours = random.randint(0, 23)
        random_minutes = random.randint(0, 59)
        random_seconds = random.randint(0, 59)

        return now - timedelta(days=random_days,
                               hours=random_hours,
                               minutes=random_minutes,
                               seconds=random_seconds)

    def generate_single_entry(self, entry_id):
        """Tạo dữ liệu cho một mục"""
        return {
            'id': entry_id,
            'identifier': str(uuid.uuid4()).replace('-', ''),
            'full_name': self.generate_full_name(),
            'age_value': random.randint(18, 65),
            'monthly_salary': round(random.uniform(3000.00, 50000.00), 2),
            'creation_date': self.generate_creation_time()
        }

    def batch_insert(self, start_id=1, batch_size=1000):
        """
        Chèn dữ liệu theo lô

        Args:
            start_id: ID bắt đầu
            batch_size: Số lượng bản ghi chèn theo lô

        Returns:
            Số bản ghi chèn thành công
        """
        if not self.connection or not self.connection.is_connected():
            self.logger.error("Chưa kết nối cơ sở dữ liệu")
            return 0

        insert_query = """
        INSERT INTO test_data (id, full_name, age_value, monthly_salary, creation_date)
        VALUES (%s, %s, %s, %s, %s)
        """

        try:
            cursor = self.connection.cursor()
            # Tạo dữ liệu
            entries = []
            for i in range(batch_size):
                entry = self.generate_single_entry(start_id + i)
                entries.append((
                    entry['id'],
                    entry['full_name'],
                    entry['age_value'],
                    entry['monthly_salary'],
                    entry['creation_date']
                ))

            # Chèn theo lô
            start_time = time.time()
            cursor.executemany(insert_query, entries)
            self.connection.commit()

            elapsed_time = time.time() - start_time
            self.logger.info(f"Chèn thành công {cursor.rowcount} bản ghi, thời gian: {elapsed_time:.2f} giây")

            return cursor.rowcount

        except Error as e:
            self.connection.rollback()
            self.logger.error(f"Chèn lô thất bại: {e}")
            return 0

def execute_main():
    """Hàm chính"""

    # Cấu hình tham số kết nối (vui lòng điều chỉnh theo thực tế)
    config = {
        'host': 'localhost',
        'database': 'ssoredisdb',  # tên cơ sở dữ liệu
        'user': 'root',  # tên người dùng
        'password': 'your_password',  # mật khẩu của bạn
        'port': 3306
    }

    # Tạo instance công cụ tạo dữ liệu
    tool = DataPopulationTool(**config)

    try:
        # Kết nối cơ sở dữ liệu
        if not tool.connect():
            return

        # Chèn dữ liệu
        print("=" * 50)
        print("Bắt đầu tạo dữ liệu thử nghiệm")
        print("=" * 50)

        # Chèn dữ liệu theo lô, có thể điều chỉnh kích thước lô
        total_inserted = 0
        # Chèn 80 lô dữ liệu
        batches = 80
        # Mỗi lô 2500 bản ghi
        batch_size = 2500

        for batch_num in range(1, batches + 1):
            print(f"\nLô {batch_num}/{batches} đang tạo dữ liệu...")
            inserted = tool.batch_insert(
                start_id=total_inserted + 1,
                batch_size=batch_size
            )
            total_inserted += inserted

            # Tạm dừng sau mỗi lô nhỏ để tránh áp lực lên cơ sở dữ liệu
            time.sleep(0.5)

        print(f"\nTất cả lô hoàn tất! Tổng cộng đã chèn {total_inserted} bản ghi")

        # Hiển thị thông tin bảng
        print("\n" + "=" * 50)

    except Exception as e:
        tool.logger.error(f"Lỗi chạy chương trình: {e}")
    finally:
        # Ngắt kết nối cơ sở dữ liệu
        tool.disconnect()


if __name__ == "__main__":
    # Chạy trực tiếp hàm chính, kết nối cơ sở dữ liệu và chèn dữ liệu
    execute_main()

Tạo dữ liệu người dùng (mật khẩu 123456):

INSERT INTO account_info VALUES(1,'user1','$2a$10$M7tqdf8GC3cqQUGdeMg.b.sF4GLjrqNd1C4iOfTpSiQHDYQRJlSLm', 'USER',NOW());
INSERT INTO account_info VALUES(2,'admin','$2a$10$M7tqdf8GC3cqQUGdeMg.b.sF4GLjrqNd1C4iOfTpSiQHDYQRJlSLm', 'ADMIN',NOW());

Sử dụng BCrypt để mã hóa trực tiếp:

public static void main(String[] args) {
      BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

      // 1. Mã hóa (kết quả khác nhau mỗi lần chạy vì muối được tạo ngẫu nhiên)
      String plainText = "123456";
      String hashedText = encoder.encode(plainText);
      System.out.println("Văn bản đã mã hóa lưu vào cơ sở dữ liệu: " + hashedText);

      // 2. Xác minh khớp
      boolean matches = encoder.matches(plainText, hashedText);
      System.out.println("Mật khẩu có đúng không: " + matches);
  }

Quá trình đăng ký:

  1. Người dùng nhập mật khẩu dạng văn bản.
  2. Bên máy chủ tạo muối (Salt) ngẫu nhiên.
  3. Tính toán băm mật khẩu + muối.
  4. Chỉ lưu kết quả băm (BCrypt sẽ mã hóa muối và tham số thuật toán vào chuỗi).

Quá trình đăng nhập:

  1. Lấy văn bản băm của người dùng từ cơ sở dữ liệu.
  2. Bên máy chủ sử dụng cùng thuật toán, cố gắng khớp văn bản đầu vào và văn bản đã mã hóa.
  3. Xác thực nếu khớp thành công.

2. Cấu hình bảo mật

Sử dụng trực tiếp cấu hình SpringSecurity để thực hiện đăng nhập, đăng xuất, chặn truy cập:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.session.FlushMode;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Bật kiểm soát quyền bằng chú thích như @PreAuthorize
@EnableRedisHttpSession(
        flushMode = FlushMode.ON_SAVE,
        redisNamespace = "${spring.session.redis.namespace:ssoredis:session}"
)
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable) // Dự án phân tách phía sau thường tắt CSRF
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/public/**", "/authenticate").permitAll() // Danh sách trắng
                        .requestMatchers("/api/management/**").hasRole("ADMIN")     // Quyền vai trò
                        .anyRequest().authenticated()                           // Tất cả yêu cầu khác cần đăng nhập
                )
                .formLogin(form -> form
                        .loginProcessingUrl("/authenticate")
                        .successHandler((req, resp, auth) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            resp.setCharacterEncoding("UTF-8");
                            resp.getWriter().write("{\"status\":200,\"msg\":\"Đăng nhập thành công\"}");
                        })
                        .failureHandler((req, resp, ex) -> {
                            resp.setStatus(401);
                            resp.setContentType("application/json;charset=utf-8");
                            resp.setCharacterEncoding("UTF-8");
                            resp.getWriter().write("{\"status\":401,\"msg\":\"Tên tài khoản hoặc mật khẩu sai\"}");
                        })
                )
                .logout(logout -> logout
                        .logoutUrl("/sign-out")
                        .logoutSuccessHandler((req, resp, auth) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            resp.setCharacterEncoding("UTF-8");
                            resp.getWriter().write("{\"status\":200,\"msg\":\"Đăng xuất thành công\"}");
                        })
                )
                .exceptionHandling(ex -> ex
                        // Xử lý khi chưa đăng nhập
                        .authenticationEntryPoint((req, resp, authEx) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            resp.setCharacterEncoding("UTF-8");
                            resp.setStatus(401);
                            resp.getWriter().write("Chưa đăng nhập, vui lòng đăng nhập trước");
                        })
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Sử dụng mã hóa băm mạnh
        return new BCryptPasswordEncoder();
    }
}
  • EnableWebSecurity: Bật Spring Security
  • EnableRedisHttpSession: Bật cấu hình lưu phiên làm việc trên Redis và truyền tham số trong chú thích
  • EnableMethodSecurity: Bật kiểm soát quyền bằng chú thích, hỗ trợ vai trò
  • Spring Security nội bộ đã triển khai giao diện /authenticate, chúng ta chỉ cần triển khai UserDetailsService

3. Truy vấn người dùng

Triển khai UserDetailsService:

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.ssoredis.dao.entity.AccountInfo;
import com.example.ssoredis.dao.mapper.AccountInfoMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class AccountServiceImpl implements UserDetailsService {

    @Resource
    private AccountInfoMapper accountInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<AccountInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AccountInfo::getUserName, username);
        AccountInfo account = accountInfoMapper.selectOne(queryWrapper);
        if (account == null) {
            throw new UsernameNotFoundException("Người dùng không tồn tại");
        }
        return org.springframework.security.core.userdetails.User
                .withUsername(account.getUserName())
                .password(account.getEncryptedPassword()) // Cơ sở dữ liệu chứa văn bản đã mã hóa
                .roles(account.getAccessRole())        // Ví dụ "ADMIN" hoặc "USER"
                .build();
    }
}
  • AccountInfoMapper truy vấn bảng account_info để lấy người dùng

4. Giao diện mẫu

Giao diện thử nghiệm:

import com.example.ssoredis.api.response.PagedResponse;
import com.example.ssoredis.api.response.SingleResponse;
import com.example.ssoredis.api.dto.TestDataDTO;
import com.example.ssoredis.api.dto.PageQueryDTO;
import com.example.ssoredis.api.vo.TestDataVO;
import com.example.ssoredis.api.vo.PageQueryVO;
import com.example.ssoredis.service.TestDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/api/test")
public class TestController {

    @Autowired
    private TestDataService testDataService;

    @PostMapping("/fetch-page")
    public PagedResponse<TestDataVO> fetchPage(@RequestBody PageQueryVO req) {
        PageQueryDTO reqDTO = new PageQueryDTO();
        BeanUtils.copyProperties(req, reqDTO);
        PagedResponse<TestDataDTO> pageVO = testDataService.fetchPage(reqDTO);
        List<TestDataVO> testDataVOS = new ArrayList<>();
        for (TestDataDTO testDataDTO : pageVO.getData()) {
            TestDataVO testDataVO = new TestDataVO();
            BeanUtils.copyProperties(testDataDTO, testDataVO);
            testDataVOS.add(testDataVO);
        }
        return PagedResponse.success(pageVO, testDataVOS);
    }
    
    @GetMapping("/fetch-single")
    @PreAuthorize("hasRole('ADMIN')") // Chỉ vai trò ADMIN mới có thể truy cập
    public SingleResponse<TestDataVO> fetchSingle(@RequestParam Long id) {
        TestDataDTO testDataDTO = testDataService.getById(id);
        TestDataVO testDataVO = new TestDataVO();
        BeanUtils.copyProperties(testDataDTO, testDataVO);
        return SingleResponse.success(testDataVO);
    }
}
  • Giao diện fetch-page người dùng thường có thể truy cập
  • Giao diện fetch-single chỉ vai trò ADMIN mới có thể truy cập

Thử nghiệm chạy

1. Thử nghiệm truy cập giao diện mẫu

1) Truy cập trực tiếp giao diện /fetch-page khi chưa đăng nhập

Kết quả: Truy cập thất bại, chưa đăng nhập

2) Đăng nhập người dùng thường

Kết quả: Đăng nhập thành công

3) Truy cập lại /fetch-page

Kết quả: Truy cập thành công

2. Thử nghiệm truy cập giao diện vai trò ADMIN

1) Người dùng thường truy cập trực tiếp giao diện /fetch-single

Kết quả: Không đủ quyền hạn

2) Đăng nhập ADMIN

Kết quả: Đăng nhập ADMIN thành công

3) Truy cập lại giao diện ADMIN /fetch-single

Kết quả: Truy cập thành công!

Thẻ: SpringBoot springsecurity Redis sso JWT

Đăng vào ngày 26 tháng 7 lúc 02:00