+3

Series pragmarx/google2fa Nâng cao #3: Chống Brute-force OTP – Hàng rào Rate Limiting (Trạm cuối)

1. Tư duy phòng vệ: Giới hạn theo lũy tiến

Cách làm đơn giản nhất là chặn cứng: Sai quá 5 lần thì khóa 15 phút. Tuy nhiên, các hệ thống Enterprise thường áp dụng chiến lược tinh tế hơn: Khóa lũy tiến (Exponential Backoff).

  • Sai lần 3: Khóa 30 giây.
  • Sai lần 5: Khóa 5 phút.
  • Sai lần 10: Khóa 24 giờ và gửi cảnh báo về hệ thống giám sát.

Để đơn giản và hiệu quả cao, chúng ta sẽ ứng dụng RateLimiter Facade có sẵn của Laravel chạy trên nền Redis nhằm đảm bảo tốc độ tối ưu.

2. Nâng cấp API phòng chờ với Throttling

Hãy mở lại hàm verify2FA trong AuthController.php và bọc nó lại bằng một lớp kiểm tra tần suất request. Chúng ta sẽ định danh kẻ tấn công bằng sự kết hợp giữa challenge_id (hoặc IP của request) để tránh việc chúng fake IP.

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use PragmaRX\Google2FALaravel\Facade as Google2FA;

class AuthController extends Controller
{
    public function verify2FA(Request $request)
    {
        $request->validate([
            'challenge_id' => 'required|string',
            'otp_code'     => 'required|digits:6'
        ]);

        // 1. Định nghĩa cái "Khóa" để tính lượt truy cập (kết hợp IP và vé chờ)
        $limiterKey = '2fa_brute_block_' . $request->challenge_id . '_' . $request->ip();

        // 2. CHỐT CHẶN BRUTE-FORCE: Kiểm tra xem vượt giới hạn chưa?
        // Cho phép thử tối đa 5 lần. Nếu vượt quá, chặn đứng ngay tại đây.
        if (RateLimiter::tooManyAttempts($limiterKey, 5)) {
            $seconds = RateLimiter::availableIn($limiterKey);
            
            return response()->json([
                'success' => false,
                'message' => "Bạn đã nhập sai quá nhiều lần. API đã bị khóa tạm thời. Vui lòng thử lại sau {$seconds} giây."
            ], 429); // Mã lỗi 429 Too Many Requests
        }

        // Tìm user từ cache như các bài trước
        $cacheKey = '2fa_challenge_' . $request->challenge_id;
        $userId = Cache::get($cacheKey);
        if (!$userId) {
            return response()->json(['message' => 'Phiên đăng nhập hết hạn.'], 400);
        }
        $user = User::find($userId);

        // Chống Replay Attack (Từ Bài 2 nâng cao)
        $blacklistKey = 'used_otp_' . $user->id . '_' . $request->otp_code;
        if (Cache::has($blacklistKey)) {
            // Tính là 1 lần thử sai để phạt nếu cố tình spam mã cũ
            RateLimiter::hit($limiterKey, 300); // Lưu vết phạt trong 5 phút
            return response()->json(['message' => 'Mã này đã được sử dụng.'], 403);
        }

        // 3. THỰC HIỆN KIỂM TRA MÃ OTP
        $secretKey = decrypt($user->google2fa_secret);
        $isValid = Google2FA::verifyKey($secretKey, $request->otp_code);

        if ($isValid) {
            // NẾU ĐÚNG: Xóa sạch vết phạt trước đó để reset bộ đếm về 0
            RateLimiter::clear($limiterKey);

            // Ghi Blacklist OTP (Bài 2)
            $windowSeconds = (config('google2fa.window') + 1) * 30;
            Cache::put($blacklistKey, true, now()->addSeconds($windowSeconds));

            // Cấp Access Token như bình thường
            $token = $user->createToken('AFC-Metro-Token')->plainTextToken;
            return response()->json(['success' => true, 'access_token' => $token]);
        }

        // 4. NẾU SAI: Tăng bộ đếm vết phạt lên 1 đơn vị
        // Bộ đếm này sẽ tự động biến mất sau 5 phút (300 giây) nếu không bị đập tiếp
        RateLimiter::hit($limiterKey, 300);

        $attemptsLeft = RateLimiter::remaining($limiterKey, 5);

        return response()->json([
            'success' => false,
            'message' => "Mã OTP không chính xác. Bạn còn {$attemptsLeft} lần thử lại trước khi bị khóa."
        ], 401);
    }
}

Lời kết cho Mini-Series Nâng Cao

Chúc mừng bạn! Việc bổ sung Rate Limiting kết hợp với Chống Replay Attack ở Bài 2 đã biến module Google 2FA của bạn từ một giải pháp tích hợp thư viện thông thường trở thành một pháo đài bảo mật cấp độ Enterprise.

Hệ thống của bạn giờ đây không chỉ kiểm tra tính đúng đắn của dữ liệu, mà còn có khả năng tự nhận biết hành vi bất thường, chống Man-in-the-middle, chống spam bot, bảo vệ toàn vẹn tài nguyên máy chủ và đem lại sự an tâm tuyệt đối cho bài toán vận hành.

Hành trình chinh phục bảo mật và tối ưu kiến trúc Backend của chúng ta tạm thời khép lại một chương rất đẹp tại đây. Chúc bạn luôn giữ vững phong độ và luôn tạo ra những dòng mã nguồn sạch sẽ, kiên cố nhất!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí