Vấn đề thực tế: Khi API “chạy được” là chưa đủ
Hồi mới đi làm, mình từng hí hửng bàn giao một con API Spring Boot cho đội Frontend. Lúc đó, mình chỉ tập trung vào việc trả về đúng JSON là xong. Kết quả thật thảm hại. Chỉ sau một tuần, hệ thống rò rỉ hơn 1.000 bản ghi user vì mình để lộ thông tin nhạy cảm trong response. Tệ hơn, bất kỳ ai có link cũng xóa được bài viết của người khác do thiếu phân quyền (Authorization).
Nhiều bạn Junior hiện nay vẫn đang lặp lại sai lầm này. Code có thể chạy ngon trên máy cá nhân nhưng sẽ sớm “gục ngã” trước các đợt audit bảo mật hoặc khi lượng user tăng cao. Một hệ thống thực tế đòi hỏi sự khắt khe về tính đóng gói và bảo mật hơn nhiều so với các bài tập lớn.
3 “tử huyệt” khiến API của bạn bị từ chối khi lên Production
Sau khi tham gia refactor một codebase hơn 50.000 dòng cho dự án Fintech, mình nhận ra 3 lỗi phổ biến nhất:
- Quản lý bảo mật hời hợt: Nhiều bạn vẫn dùng Basic Auth hoặc lưu mật khẩu dạng plain text. Đây là lời mời chào hấp dẫn cho các hacker.
- Lạm dụng Entity: Trả nguyên đối tượng Database ra ngoài API là lỗi cực nặng. Nó vô tình làm lộ password hash hoặc các ID nội bộ không cần thiết.
- Hardcoded cấu hình: Việc để Secret Key của JWT ngay trong code khiến bạn phải build lại toàn bộ dự án chỉ để đổi một cái key.
Lựa chọn kiến trúc: Tại sao lại là JWT?
Để xử lý xác thực, chúng ta thường cân nhắc giữa Session và Token. Session-based truyền thống rất khó scale khi bạn cần chạy Load Balancing trên 3-4 server khác nhau. Basic Auth thì lại quá kém an toàn vì gửi kèm mật khẩu trong mọi request.
JWT (JSON Web Token) nổi lên như một tiêu chuẩn vàng cho REST API. Nó giúp hệ thống stateless (không lưu trạng thái), dễ dàng tích hợp với cả Web lẫn Mobile App. Dưới đây là cách mình triển khai một dự án chuẩn chỉnh, kết hợp PostgreSQL và bảo mật đa lớp.
Hướng dẫn triển khai chi tiết
1. Khởi tạo và cấu hình PostgreSQL bài bản
Đầu tiên, hãy truy cập Spring Initializr. Bạn cần chọn các thư viện: Spring Web, Spring Data JPA, PostgreSQL Driver, Spring Security, và Lombok.
Thay vì dùng file .properties cũ kỹ, mình ưu tiên dùng application.yml vì cấu trúc phân cấp rõ ràng, dễ đọc. Hãy cấu hình kết nối DB như sau:
spring:
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:mydb}
username: ${DB_USER:postgres}
password: ${DB_PASS:password}
jpa:
hibernate:
ddl-auto: update
show-sql: false
Lưu ý quan trọng: Đừng bao giờ ghi đè password thật vào file này. Cú pháp ${VAR:default} giúp bạn linh hoạt truyền biến môi trường khi deploy bằng Docker hoặc Kubernetes.
2. Thiết lập Model và Phân quyền (RBAC)
Mô hình phân quyền dựa trên vai trò (Role-based Access Control) là bắt buộc. Bạn nên tách biệt User và Role thành mối quan hệ Many-to-Many. Cách tiếp cận này giúp bạn dễ dàng nâng cấp một User lên ADMIN mà không cần sửa code.
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private Set<Role> roles = new HashSet<>();
}
3. Cấu hình Spring Security 6: Điểm chạm khó nhất
Spring Security 6 đã bỏ cách kế thừa WebSecurityConfigurerAdapter cũ. Hiện tại, chúng ta chuyển sang hướng functional configuration. Đây là nơi cấu hình các “chốt chặn” cho API.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
4. Quản lý JWT Service
Sử dụng thư viện jjwt để tạo token. Một mẹo nhỏ: hãy đặt thời gian hết hạn (Expiration) ngắn, ví dụ 15 phút cho Access Token. Sau đó, kết hợp thêm Refresh Token để đảm bảo trải nghiệm người dùng không bị gián đoạn mà vẫn an toàn.
Đóng gói Docker: Sẵn sàng cho Production
Để tránh lỗi “chạy trên máy em ngon mà lên server thì tịt”, mình luôn đóng gói ứng dụng vào Docker. Điều này đồng nhất môi trường phát triển và môi trường thật.
File Dockerfile tối giản cho dự án:
FROM eclipse-temurin:17-jdk-alpine
WORKDIR /app
COPY target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Kết hợp với docker-compose.yml, bạn có thể khởi chạy cả App và Database chỉ với một câu lệnh duy nhất. Việc quản lý hạ tầng lúc này sẽ nhàn nhã hơn rất nhiều.
Lời kết từ thực tế
Mình từng mất cả đêm để fix bug chỉ vì một file cấu hình XML sai sót trong hệ thống cũ. Bài học lớn nhất là: Hãy viết code cho người sau đọc, đừng chỉ viết cho máy. Việc áp dụng Spring Boot 3 với cấu hình minh bạch giúp đội ngũ bảo trì tiết kiệm được hàng chục giờ làm việc mỗi tháng.
Đừng quên viết Integration Test cho các luồng Login và Register. Một bộ test tốt giúp bạn tự tin bấm nút Deploy vào chiều thứ 6 mà không lo bị sếp gọi điện lúc nửa đêm. Nếu đã nắm vững các bước này, bạn đã thực sự sẵn sàng để xây dựng những hệ thống Backend chuyên nghiệp.

