Chặng bảo mật khép lại. Bài này gom mười bài thành thứ dùng được.
Sáu kết quả đáng nhớ
Người dùng vai USER gọi được phương thức @PreAuthorize("hasRole('ADMIN')") (bài 41) — chỉ cần gọi vòng qua chính lớp đó. Không lỗi, không log, không cảnh báo.
BCrypt cắt ở byte thứ 72 (bài 40). Hai mật khẩu khác hẳn nhau nhưng trùng 72 byte đầu được coi là một. Với tiếng Việt có dấu, 72 byte chỉ khoảng 30–40 ký tự.
Gửi đúng token CSRF vẫn 403 (bài 42). Spring Security 6 đổi ba thứ cùng lúc: xoay token lúc đăng nhập, hoãn sinh token, và bộ xử lý XOR.
403 hiện ra thành 401 (bài 42). CSRF hỏng trả 403, rồi dispatch /error biến nó thành 401. Mã bạn thấy không phải mã ban đầu.
Cấu hình HSTS mà header không xuất hiện (bài 45). Spring chỉ gửi nó cho request HTTPS, và sau nginx thì ứng dụng chỉ thấy HTTP nếu thiếu X-Forwarded-Proto.
MockMvc nói 403, máy chủ thật trả 405 (bài 47). Cùng một đoạn mã, cùng một request.
Bốn chỗ proxy làm chú thích biến mất
Đây là mạch xuyên suốt cả sê-ri, không riêng chặng này:
| Chú thích | Tự gọi thì | Bài |
|---|---|---|
@Transactional |
không có giao dịch | 26 |
@Cacheable |
cache không trúng | 35 |
@PreAuthorize |
phân quyền bị bỏ qua | 41 |
trường public trên bean |
đọc ra null |
35 |
Cùng một nguyên nhân: Spring cài đặt bằng proxy, và lời gọi từ bên trong cùng lớp không đi qua proxy.
Ba cái đầu đều im lặng. Cái thứ ba là lỗ hổng bảo mật.
Quy tắc rút ra: đặt chú thích ở ranh giới, không ở bên trong. Và khi cần kiểm ở tầng sâu, tách sang bean khác — việc đó thường lộ ra rằng lớp đang làm hai việc.
Danh sách kiểm trước khi lên sản xuất
Xác thực
- Mật khẩu băm bằng BCrypt cost ≥ 10 hoặc Argon2, qua
DelegatingPasswordEncoder - Giới hạn số lần thử đăng nhập, đếm theo cửa sổ chứ không chặn cứng theo IP
- Cùng một thông báo cho "sai mật khẩu" và "không có tài khoản", và cùng thời gian phản hồi
- Không mật khẩu nào trong log
Phân quyền
- Mọi endpoint có luật, kết bằng
anyRequest().authenticated() - Quyền trên dữ liệu, không chỉ trên endpoint — tốt nhất là đưa vào truy vấn
- Không có
@PreAuthorizenào bị bỏ qua vì tự gọi
Phiên và token
- Cookie có
HttpOnly,Secure,SameSite=Lax - CSRF bật cho mọi thứ dùng cookie; tắt có chọn lọc cho API dùng header
- JWT có
expngắn, kiểmissvàaud, thuật toán được ghim
Truyền tải
- HTTPS bắt buộc, HSTS thật sự được gửi (kiểm bằng
curl -sItrên URL https) X-Forwarded-Proto,X-Forwarded-Host,X-Real-IPđược nginx chuyển tiếp- CSP không có
unsafe-inlinetrongscript-src
Lộ thông tin
- Actuator không ra Internet,
includeliệt kê tường minh - Dấu vết ngăn xếp tắt trong phản hồi
- Thông báo lỗi không chứa SQL, tên ràng buộc, hay đường dẫn tệp
Đầu vào
- Mọi
@RequestBodycó@Valid - Mọi đầu vào sinh nhiều bản ghi có trần, kiểm trước khi ghi
- Tệp tải lên: giới hạn kích thước, không tin tên tệp, không tin
Content-Type, làm sạch SVG - Chuyển hướng chỉ lấy phần đường dẫn, bắt đầu bằng đúng một dấu gạch chéo
Phụ thuộc
- Quét trong CI với ngưỡng làm đỏ build
- Cập nhật tự động bật sẵn
Ba nguyên tắc
Nhiều lớp, không một lớp. Phân quyền theo URL cộng phân quyền mức phương thức cộng điều kiện trong truy vấn. Bỏ sót một tầng vẫn còn hai tầng. Bài 41 cho thấy vì sao: một tầng có thể biến mất mà không ai biết.
Mặc định an toàn. Spring Security khoá mọi thứ khi bạn thêm phụ thuộc (bài 39), và đó là mặc định đúng — bạn phải khai tường minh cái gì công khai. Áp dụng cùng nguyên tắc cho mã của mình: danh sách cho phép, không phải danh sách chặn.
Đo, đừng tin cấu hình. Sáu kết quả ở đầu bài đều là chỗ cấu hình trông đúng mà hành vi thật khác. Một lệnh curl kiểm được nhiều hơn một giờ đọc lại mã.
Điều tôi rút ra từ chặng này
Ba trong sáu bất ngờ ở trên không phải lỗ hổng — chúng là thứ trông như hỏng mà thật ra đang hoạt động đúng (HSTS vắng mặt, 403 thành 401, CSRF từ chối token cũ). Và một cái trông như hoạt động bình thường lại là lỗ hổng thật (tự gọi bỏ qua @PreAuthorize).
Đó là điều làm bảo mật khó: triệu chứng không tương ứng với mức nghiêm trọng. Thứ ồn ào thường vô hại, thứ nguy hiểm thường im lặng.
Cách duy nhất tôi biết để đối phó là không dựa vào triệu chứng: có danh sách kiểm, chạy nó định kỳ, và thử bằng tài khoản của người khác.
Thử ba mươi giây
curl -sI https://ung-dung-cua-ban.com/ | grep -icE "content-security|strict-transport|x-frame|x-content-type|referrer"
Con số dưới 5 nghĩa là bạn thiếu header bảo mật cơ bản — và đó là phần rẻ nhất của toàn bộ danh sách kiểm ở trên.
Ngày mai bắt đầu chặng cuối: kiểm thử và đưa vào sản xuất. Mở đầu bằng kim tự tháp test và các lát cắt của Spring Boot.