Giải phẫu luồng POST /login: Tư duy Thiết kế đằng sau một Nút Bấm
Khi nhìn vào giao diện đăng nhập của hệ thống quản trị, mọi thứ có vẻ rất đơn giản: nhân viên mở trang GET /login/admin, nhập Email, Mật khẩu cùng mã OTP rồi bấm Đăng nhập.
Tuy nhiên, đằng sau cú click chuột đó là một chuỗi kiểm soát chặt chẽ đi qua 5 lớp bảo vệ và 2 hệ thống độc lập. Bài viết này không chỉ liệt kê code chạy từ dòng nào đến dòng nào, mà tập trung vào cách tư duy (mindset) để hiểu tại sao luồng đăng nhập lại được thiết kế như vậy, giúp việc debug và bảo trì sau này trở nên dễ dàng hơn.
Phần 1: Bức tranh toàn cảnh – Request đi qua những đâu?
Luồng xử lý của POST /login hoạt động theo nguyên tắc Fail-Fast (Dừng ngay khi lỗi): Request lần lượt đi qua từng lớp từ ngoài vào trong; bất kỳ lớp nào từ chối, luồng sẽ dừng lại ngay lập tức và trả lỗi về cho người dùng.
1. Vòng ngoài: Các lớp Middleware & Validation
Trước khi chạm được vào logic nghiệp vụ chính, request phải vượt qua 4 trạm kiểm soát:
| Lớp xử lý | File / Vị trí | Câu hỏi lớp này trả lời | Hành vi khi phát hiện bất thường |
|---|---|---|---|
#1 Middleware web |
Core Laravel | Request có khởi tạo session và kèm CSRF token hợp lệ không? | Chặn ngay (419 Page Expired). |
#2 Middleware guest |
LoginController.php:15 |
Người dùng này chưa đăng nhập phải không? | Nếu đã có session đăng nhập, chuyển hướng thẳng vào trang trong, không xử lý lại. |
#3 log.action.warning |
webLogWarningActionMiddleware |
Có ai đang cố đăng nhập trong khung giờ nhạy cảm (0h – 7h sáng) không? | Không chặn request. Chỉ âm thầm bắn cảnh báo lên kênh Slack (warning_actions) để giám sát. |
#4 LoginRequest |
Http/Requests/LoginRequest.php |
Dữ liệu đầu vào có sạch không? (Email đúng chuẩn, password 6 ký tự, OTP đủ 6 số trừ nhóm bypass 2FA, và email phải tồn tại trong bảng users local). | Trả về lỗi Validation, không tốn tài nguyên gọi vào Service hay API ngoài. |
2. Vòng trong: Trái tim nghiệp vụ tại LoginService::execute()
Khi dữ liệu đầu vào đã hợp lệ và chắc chắn email có tồn tại trong hệ thống local, LoginService.php sẽ tiếp quản và thực thi 4 bước:
Bước A: Kiểm tra đặc quyền Bypass 2FA
-
Câu hỏi: Tài khoản này có nằm trong danh sách được miễn xác thực 2 lớp (2FA) không?
-
Xử lý: Các email nằm trong hàm cấu hình bypass (cùng với tài khoản
manager@superadmin.comở môi trường non-production) sẽ bỏ qua toàn bộ Bước B (không gọi ra hệ thống Auth trung tâm) và nhảy thẳng xuống Bước C.
Bước B: Xác thực và Phân quyền qua Auth API trung tâm
Với các tài khoản thông thường, hệ thống không tự kiểm tra mật khẩu ngay mà hỏi Hệ thống Auth trung tâm — nền tảng đứng sau ứng dụng Hasaki Work).
B1. Gọi API Xác thực :
-
Kiểm tra trạng thái khóa (Lockout): Kiểm tra xem tài khoản có đang bị tạm khóa không (dựa trên cả Cache lẫn cột
login_locked_...trong DB). Nếu đang bị khóa, từ chối ngay mà không gọi API ngoài. -
Gọi API: Bắn request
POST api/auth/user/login-otpkèm theoemail,password,app=nowvàotp. -
Nếu thất bại: Tăng biến đếm số lần sai. Nếu sai đủ 3 lần, hệ thống khóa tài khoản trong 5 phút (ghi nhận vào cả Cache và DB), đồng thời bắn cảnh báo lên Slack.
-
Đang bị khóa: Báo
"Tài khoản tạm thời bị khóa..." -
Sai OTP: Báo
"Mã OTP không đúng..." -
Sai mật khẩu / lỗi khác: Báo
"Thông tin đăng nhập không đúng, vui lòng đổi mật khẩu trên ứng dụng Hasaki Work..."
-
-
Nếu thành công: Reset bộ đếm lỗi về
0, nhậntokenvà gọi tiếp APIget-user-roleđể lấy danh sách quyền (roles) của nhân viên.
B2. Kiểm tra phân quyền vào App :
-
Role Whitelist: User phải sở hữu ít nhất một trong các role được cấu hình trong
setting():admin,operation,tech,test,shipper leader,CS,HR. -
Ràng buộc riêng cho Tech: Riêng với role
tech, có role thôi chưa đủ — email của nhân viên đó bắt buộc phải nằm trong danh sách trắnggetAllowEmailLogin().
B3. Đồng bộ mật khẩu về Database Local
-
Lưu
tokenvừa nhận từ Auth API vào bản ghi của user. -
Kiểm tra mật khẩu người dùng vừa nhập với mã băm (hash) đang lưu trong DB local. Nếu khác nhau (do người dùng vừa đổi mật khẩu trên app Hasaki Work), hệ thống sẽ ghi đè (cập nhật) hash mật khẩu mới vào DB local.
Bước C: Kiểm tra trạng thái tài khoản tại Local
-
Bản ghi user tại DB local bắt buộc phải có trạng thái
ACTIVE. -
Nếu user không phải là
admin, tài khoản đó phải hoàn tất xác thực email (verified).
Bước D: Khởi tạo phiên đăng nhập Laravel
-
Gọi hàm chuẩn của Laravel
Auth::attempt(email, password). Nhờ Bước B3 vừa đồng bộ hash mật khẩu về local, hàm này sẽ đối chiếu thành công một cách mượt mà. -
Lưu lại
current_session_idđể phục vụ cơ chế Single Session (mỗi tài khoản chỉ được phép duy trì một phiên đăng nhập tại một thời điểm). -
Chuyển hướng người dùng về URL họ đang muốn truy cập trước đó (intended URL) hoặc về trang chủ
/.
Xử lý dọn dẹp khi có lỗi : Nếu bất kỳ bước nào trong
LoginServiceném ra ngoại lệ hoặc thất bại, hệ thống sẽ xóa sạch cache thông tin user (auth-inshasaki-user...), quay trở lại trang login và hiển thị lý do cụ thể qua biến flash session.
Phần 2: 6 Tư duy Cốt lõi (Mindset) để Làm chủ Luồng Đăng nhập
Đọc hiểu từng dòng code giúp bạn biết hệ thống đang làm gì, nhưng nắm được 6 nguyên lý thiết kế dưới đây sẽ giúp bạn biết tại sao không nên sửa bừa và cách khoanh vùng lỗi trong 30 giây.
1. Hai nguồn sự thật (Two Sources of Truth), mỗi nơi giữ một việc
Trong kiến trúc này, dữ liệu người dùng không nằm ở một chỗ mà được chia làm hai:
-
Auth API trung tâm - giữ "Danh tính" (Identity): Quản lý mật khẩu gốc, mã OTP và danh sách Role toàn cục. Nó trả lời câu hỏi: "Người này có đúng là nhân viên công ty và thuộc bộ phận được phép vào hệ thống không?"
-
Database Local giữ "Trạng thái ứng dụng" (App State): Quản lý việc tài khoản có đang
ACTIVEở hệ thống này không, đã verify chưa, vàsession_idhiện tại là gì.
Điểm mấu chốt cần nhớ: Mật khẩu lưu trong bảng users ở DB local chỉ là một bản sao (shadow copy) của mật khẩu trên Auth API. Nó tồn tại chỉ để phục vụ cho hàm Auth::attempt() mặc định của Laravel hoạt động. Hiểu được điều này, bạn sẽ thấy hoàn toàn hợp lý khi người dùng nhập sai mật khẩu trên web nhưng hệ thống lại báo: "Vui lòng đổi mật khẩu trên ứng dụng Hasaki Work" — vì ứng dụng này không phải là nơi làm chủ mật khẩu.
2. Nguyên lý "Mỗi lớp một câu hỏi" – Bí quyết Debug nhanh
Thứ tự kiểm tra được sắp xếp theo hình phễu, đi từ nhẹ đến nặng, từ ngoài vào trong:
-
Tại sao lại xếp như vậy? Để tiết kiệm tài nguyên. Nếu email còn chưa tồn tại trong DB local hoặc nhập thiếu chữ số OTP (kiểm tra cực nhanh ở
LoginRequest), hệ thống sẽ chặn ngay mà không tốn một request mạng (HTTP call) nào gọi sang Auth API. -
Ứng dụng khi Debug: Khi có nhân viên báo không đăng nhập được, bạn chỉ cần nhìn câu thông báo lỗi hoặc tự hỏi "Request đang dừng ở câu hỏi nào?". Mỗi thông báo lỗi trên màn hình đều ánh xạ 1-1 với đúng một bước trong phễu.
3. Đừng nhầm lẫn giữa Xác thực (Authentication) và Phân quyền (Authorization)
-
Xác thực (Bước B1): Nhập đúng mật khẩu và đúng mã OTP chỉ mới chứng minh được bạn là ai.
-
Phân quyền (Bước B2): Role whitelist và danh sách email cho phép của team
techmới quyết định bạn có được bước qua cửa hay không.
Đây là lý do vì sao có những nhân viên vẫn đăng nhập vào app Hasaki Work hàng ngày bình thường, nhập đúng mật khẩu và OTP, nhưng vẫn bị hệ thống này từ chối thẳng thừng. Khi gặp trường hợp đó, đừng kiểm tra mật khẩu — hãy kiểm tra cấu hình setting() và getAllowEmailLogin().
4. Thiết kế để Phòng thủ (Defense in Depth), không chỉ để Chạy đúng
Luồng đăng nhập này được cài cắm rất nhiều chốt chặn an ninh chủ động:
-
Chống Brute-force thông minh: Khi sai quá 3 lần, trạng thái khóa 5 phút được ghi vào cả Cache lẫn Database. Tại sao phải ghi cả hai nơi? Vì nếu một lập trình viên hoặc tiến trình deploy vô tình chạy lệnh
php artisan cache:clear, kẻ tấn công cũng không thể lợi dụng lúc cache trống để tiếp tục dò mật khẩu — chốt chặn trong Database vẫn còn nguyên tác dụng. -
Cảnh báo sớm (Early Warning): Việc đăng nhập lúc 2h sáng hay việc nhập sai mật khẩu 3 lần liên tiếp đều lập tức kích hoạt thông báo bắn về Slack. Hệ thống giả định rằng mọi hành vi bất thường đều cần có người biết ngay lập tức.
-
Câu hỏi tự vấn khi bảo trì: Bất cứ khi nào bạn định sửa code trong
LoginService, hãy luôn tự hỏi: "Thay đổi này của mình có vô tình tạo ra lối tắt (shortcut) nào làm vô hiệu hóa các chốt chặn trên không?"
5. Danh sách Bypass 2FA là "Cửa sau có chủ đích" – Cần kiểm soát gắt gao
Nhìn lại Bước A, những email nằm trong danh sách bypass sẽ nhảy cóc qua toàn bộ Bước B. Điều này đồng nghĩa với việc:
-
Không cần mã OTP.
-
Không có cơ chế khóa tài khoản 5 phút khi nhập sai mật khẩu nhiều lần.
-
Không kiểm tra biến động Role từ hệ thống trung tâm.
Những tài khoản này chỉ còn được bảo vệ bởi một lớp duy nhất là mật khẩu lưu ở DB local. Do đó, hãy coi danh sách bypass này như các tài khoản kỹ thuật đặc biệt (Service/Break-glass accounts): hạn chế tối đa việc thêm mới, đặt mật khẩu cực mạnh và phải rà soát định kỳ.
Tóm lại
Khi nhìn vào LoginService, đừng nhìn nó như một hàm dài 60 dòng code. Hãy nhìn nó như một trạm kiểm soát biên giới: kiểm tra giấy tờ nội bộ trước (LoginRequest), gọi điện xác minh nhân thân với tổng đài trung ương (Auth API), đối chiếu danh sách được phép nhập cảnh (ChecksAuthRoles), cập nhật lại hồ sơ lưu trú (Sync Password & Status), và cuối cùng mới đóng dấu cấp visa (Session). Nắm vững mô hình này, bạn đã hoàn toàn làm chủ luồng xác thực của hệ thống.
All Rights Reserved