Phiên bản các thành phần
Các thành phần chính:
| Thành phần | Phiên bản | Mô tả |
|---|---|---|
| JDK | 25 | Hỗ trợ SpringBoot4 |
| SpringBoot | 4.x | Phiên bản mới |
| SpringSecurity | 6.2 | Hỗ trợ SpringBoot4 |
| Redis | 8.4 | Lưu trữ session |
| MySQL | 8.0 | Cơ 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ý:
- Người dùng nhập mật khẩu dạng văn bản.
- Bên máy chủ tạo muối (Salt) ngẫu nhiên.
- Tính toán băm mật khẩu + muối.
- 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:
- Lấy văn bản băm của người dùng từ cơ sở dữ liệu.
- 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.
- 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!