Как хранятся пароли в БД?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Хранение паролей в БД
Пароли должны храниться максимально защищённо. Правильное хранение паролей — одна из критических задач безопасности, требующая понимания хеширования и современных алгоритмов.
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 атак. Если меняешь алгоритм — делай ленивую миграцию.