← Назад к вопросам

Как хранятся пароли в БД?

2.0 Middle🔥 251 комментариев
#Базы данных (SQL)#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Хранение паролей в БД

Пароли должны храниться максимально защищённо. Правильное хранение паролей — одна из критических задач безопасности, требующая понимания хеширования и современных алгоритмов.

1. Почему нельзя хранить пароли в открытом виде

Если БД скомпрометирована, злоумышленник получает доступ ко всем паролям. Это приводит к массовому взлому аккаунтов.

# НИКОГДА не делай так!
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True)
    password = Column(String)  # ❌ УЯЗВИМОСТЬ!

2. Хеширование vs Шифрование

Хеширование — одностороннее преобразование (необратимо):

пароль "admin123" → хеш "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918"

Шифрование — двусторонний процесс (обратимо):

пароль "admin123" + ключ → зашифрованный текст

Для паролей используй хеширование, не шифрование.

3. PBKDF2 — старый стандарт

Легаци-подход, но всё ещё используется:

from hashlib import pbkdf2_hmac
import secrets

def hash_password_pbkdf2(password: str) -> str:
    # Генерируем случайную соль
    salt = secrets.token_hex(32)
    
    # Хешируем пароль с солью
    hash_obj = pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt.encode('utf-8'),
        iterations=100000  # Количество итераций
    )
    
    # Возвращаем соль + хеш
    return f"{salt}${hash_obj.hex()}"

def verify_password_pbkdf2(password: str, hashed: str) -> bool:
    salt, hash_hex = hashed.split('$')
    computed_hash = pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt.encode('utf-8'),
        iterations=100000
    )
    return computed_hash.hex() == hash_hex

4. bcrypt — современный стандарт

Оптимален для большинства приложений. Имеет встроенную соль и автоматически замедляется со временем:

import bcrypt

def hash_password_bcrypt(password: str) -> str:
    # Генерируем соль и хешируем в одном вызове
    salt = bcrypt.gensalt(rounds=12)  # rounds — стоимость (CPU-инженсивность)
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    return hashed.decode('utf-8')

def verify_password_bcrypt(password: str, hashed: str) -> bool:
    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))

# Использование в Django
from django.contrib.auth.hashers import make_password, check_password

user.password = make_password(password)
if check_password(password, user.password):
    # Пароль верный
    pass

5. Argon2 — самый безопасный

Победенная функция конкурса PHC (Password Hashing Competition). Резистентна к GPU и ASIC атакам:

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()

def hash_password_argon2(password: str) -> str:
    return ph.hash(password)

def verify_password_argon2(password: str, hashed: str) -> bool:
    try:
        ph.verify(hashed, password)
        return True
    except VerifyMismatchError:
        return False

# Параметры Argon2
ph = PasswordHasher(
    time_cost=2,        # Количество итераций
    memory_cost=65536,  # Память в КБ
    parallelism=4,      # Количество потоков
    hash_len=16,        # Длина хеша
    salt_len=16         # Длина соли
)

6. Сравнение алгоритмов

АлгоритмБезопасностьСкоростьСовременностьРекомендация
PBKDF2СредняяСредняяУстаревшийИзбегать для новых проектов
bcryptХорошаяМедленнаяХорошийБазовый выбор
scryptОтличнаяМедленнаяХорошийАльтернатива bcrypt
Argon2ОтличнаяОчень медленнаяНовейшийЛучший выбор

7. Практический пример с FastAPI

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from sqlalchemy import Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base
from argon2 import PasswordHasher

Base = declarative_base()
ph = PasswordHasher()
app = FastAPI()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, index=True)
    hashed_password = Column(String)  # Хранится хеш

class UserCreate(BaseModel):
    email: str
    password: str

class UserLogin(BaseModel):
    email: str
    password: str

@app.post("/register")
async def register(user: UserCreate):
    # Хешируем пароль перед сохранением
    hashed = ph.hash(user.password)
    
    db_user = User(
        email=user.email,
        hashed_password=hashed  # Сохраняем только хеш
    )
    # Сохраняем в БД
    return {"email": user.email, "message": "Registered successfully"}

@app.post("/login")
async def login(user: UserLogin):
    # Ищем пользователя
    db_user = db.query(User).filter(User.email == user.email).first()
    if not db_user:
        raise HTTPException(status_code=401, detail="Invalid credentials")
    
    # Проверяем пароль
    try:
        ph.verify(db_user.hashed_password, user.password)
    except:
        raise HTTPException(status_code=401, detail="Invalid credentials")
    
    return {"message": "Logged in successfully", "token": generate_token(db_user)}

8. Дополнительные меры безопасности

Затвердение паролей (Password Stretching):

# Используй высокую стоимость в bcrypt
bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=14))
# rounds=14 → ~1 секунда на проверку, что усложняет brute-force

Проверка слабых паролей:

from pydantic import validator
import re

class UserCreate(BaseModel):
    password: str
    
    @validator('password')
    def validate_password(cls, v):
        if len(v) < 8:
            raise ValueError('Минимум 8 символов')
        if not re.search(r'[A-Z]', v):
            raise ValueError('Нужна заглавная буква')
        if not re.search(r'[0-9]', v):
            raise ValueError('Нужна цифра')
        return v

Ограничение попыток входа:

from datetime import datetime, timedelta

class LoginAttempt(Base):
    __tablename__ = "login_attempts"
    id = Column(Integer, primary_key=True)
    email = Column(String, index=True)
    timestamp = Column(DateTime)
    success = Column(Boolean)

async def check_brute_force(email: str):
    # Проверяем неудачные попытки за последние 15 минут
    failed = db.query(LoginAttempt).filter(
        LoginAttempt.email == email,
        LoginAttempt.success == False,
        LoginAttempt.timestamp > datetime.now() - timedelta(minutes=15)
    ).count()
    
    if failed >= 5:
        raise HTTPException(status_code=429, detail="Too many login attempts")

9. Миграция хешей

Если меняешь алгоритм, не rehash старые пароли сразу. Делай ленивую миграцию:

class User(Base):
    __tablename__ = "users"
    hashed_password = Column(String)
    hash_algorithm = Column(String, default='argon2')

async def verify_login(email: str, password: str):
    user = db.query(User).filter(User.email == email).first()
    
    # Проверяем старым алгоритмом
    if user.hash_algorithm == 'bcrypt':
        verified = verify_password_bcrypt(password, user.hashed_password)
    else:
        verified = verify_password_argon2(password, user.hashed_password)
    
    if not verified:
        return False
    
    # Если старый алгоритм — перехешируем к новому
    if user.hash_algorithm != 'argon2':
        user.hashed_password = ph.hash(password)
        user.hash_algorithm = 'argon2'
        db.commit()
    
    return True

Итог: Используй Argon2 для новых проектов, bcrypt как минимум если Argon2 недоступен. НИКОГДА не храни пароли в открытом виде. Добавь проверку слабых паролей и ограничение брute-force атак. Если меняешь алгоритм — делай ленивую миграцию.