🧩 Bài 2: Spring Security + JPA – Xác thực từ cơ sở dữ liệu
🎯 Mục tiêu
- Tạo bảng
users
trong MySQL. - Sử dụng Spring Data JPA để truy xuất thông tin người dùng.
- Tích hợp Spring Security để xác thực người dùng từ database.
1️⃣ Chuẩn bị cơ sở dữ liệu
Tạo CSDL:
CREATE DATABASE demo_security;
Tạo bảng users
:
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
role VARCHAR(50) NOT NULL
);
Thêm người dùng thử nghiệm:
INSERT INTO users (username, password, role)
VALUES ('admin', '{noop}123456', 'ROLE_ADMIN');
⚠️
{noop}
dùng để chỉ mật khẩu chưa mã hóa (chỉ dùng demo).
2️⃣ Cấu hình application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/demo_security
spring.datasource.username=root
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
3️⃣ Tạo Entity User
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role;
// Getters & setters
}
4️⃣ Repository truy xuất người dùng
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
5️⃣ Custom UserDetails và UserDetailsService
🔸 UserDetailsImpl:
public class UserDetailsImpl implements UserDetails {
private final User user;
public UserDetailsImpl(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(user.getRole()));
}
@Override
public String getPassword() { return user.getPassword(); }
@Override
public String getUsername() { return user.getUsername(); }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
🔸 UserDetailsServiceImpl:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepo.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new UserDetailsImpl(user);
}
}
6️⃣ Cấu hình bảo mật
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public").permitAll()
.anyRequest().authenticated())
.httpBasic();
return http.build();
}
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance()) // chỉ dùng demo
.and().build();
}
}
7️⃣ Test thử endpoint
@RestController
public class DemoController {
@GetMapping("/public")
public String publicAPI() {
return "Trang công khai!";
}
@GetMapping("/private")
public String privateAPI() {
return "Bạn đã đăng nhập thành công!";
}
}
✅ Kết luận
Bạn vừa xây dựng hệ thống xác thực người dùng bằng Spring Security và dữ liệu trong MySQL thông qua Spring Data JPA. Từ đây, bạn có thể:
- Bổ sung mã hóa password bằng BCrypt
- Xây dựng trang đăng nhập tùy chỉnh (login form)
- Phân quyền nâng cao theo ROLE
🧠 Vì sao cần UserDetails
trong Spring Security?
Khi Spring Security thực hiện xác thực người dùng, framework cần 1 mô hình chuẩn để:
- Lấy được username và password,
- Biết được người dùng có quyền hạn (roles/authorities) gì,
- Kiểm tra trạng thái của tài khoản (có bị khóa, hết hạn, vô hiệu hóa… không).
➡️ Vì vậy, Spring Security định nghĩa interface UserDetails
như là khuôn mẫu chuẩn.
public interface UserDetails {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
🛠 Vai trò trong hệ thống
- Khi người dùng đăng nhập (
POST /login
hoặc gọi API bảo vệ), - Spring Security sẽ gọi
loadUserByUsername(username)
trongUserDetailsService
. UserDetailsService
sẽ trả về một đối tượngUserDetails
chứa toàn bộ thông tin cần thiết về người dùng.
Spring sẽ dùng các thông tin đó để:
- Kiểm tra mật khẩu.
- Kiểm tra tài khoản có bị khóa / vô hiệu hóa / hết hạn không.
- Gán các quyền cho người dùng (authorities).
💡 Vì sao không trả thẳng Entity User
?
User
của bạn (Entity JPA) có thể chứa rất nhiều thông tin riêng, ví dụ như email, số điện thoại, ngày sinh, ảnh đại diện,…- Trong khi đó, xác thực chỉ cần một số thông tin nhỏ (username, password, roles, trạng thái tài khoản).
- Để tách biệt trách nhiệm và giữ cho quy trình xác thực bảo mật và rõ ràng, Spring yêu cầu một đối tượng chuyên biệt là
UserDetails
.
👉 Vậy nên, bạn tạo thêm lớp UserDetailsImpl
để chuyển đổi từ Entity User
thành mô hình chuẩn UserDetails
.
🏆 Tóm tắt ngắn gọn
Entity User | UserDetails |
---|---|
Chứa toàn bộ thông tin người dùng trong database | Chứa thông tin tối thiểu cần thiết để xác thực |
Quản lý bởi JPA | Quản lý bởi Spring Security |
Dùng cho lưu trữ, xử lý dữ liệu nghiệp vụ | Dùng cho quy trình xác thực (Authentication) |
🛡️ Quy trình Xác thực trong Spring Security với UserDetails
[User gửi thông tin đăng nhập (username, password)]
|
v
[Spring Security nhận yêu cầu login]
|
v
[Gọi UserDetailsService.loadUserByUsername(username)]
|
v
[Tải UserDetails từ database]
- getUsername()
- getPassword()
- getAuthorities()
- isAccountNonLocked()
- isEnabled()
|
v
[Spring Security kiểm tra thông tin]
- So khớp password
- Kiểm tra trạng thái tài khoản
|
v
[Xác thực thành công ✅] ---> [Gán quyền vào SecurityContextHolder]
|
v
[Tiếp tục truy cập các API được bảo vệ]
🎯 Các thành phần chính tham gia:
Thành phần | Vai trò |
---|---|
AuthenticationManager | Quản lý xác thực |
UsernamePasswordAuthenticationToken | Gói username + password |
UserDetailsService | Load thông tin người dùng từ database |
UserDetails | Đối tượng chứa thông tin xác thực chuẩn hóa |
SecurityContextHolder | Lưu trữ thông tin người dùng sau xác thực thành công |
📚 Cách kết nối các thành phần
UsernamePasswordAuthenticationFilter
sẽ lấy username, password từ HTTP request.- Gửi đến
AuthenticationManager
. AuthenticationManager
gọiUserDetailsService.loadUserByUsername()
.UserDetailsService
tìm trong CSDL → trả vềUserDetails
.- So sánh password, kiểm tra account status.
- Nếu đúng, lưu thông tin xác thực vào
SecurityContextHolder
.
🧠 Một câu dễ nhớ
UserDetails là “hộ chiếu” giúp người dùng vượt qua cổng kiểm tra an ninh (Spring Security).