The Complete Guide to FastAPIpassword-hashing-security
📂 Stage: Stage 4 - Security and Authentication (Security)
🔗 Related chapters: FastAPI oauth2-jwt-auth · FastAPIdependency-injection
No matter how cool your web application features are, if user passwords are leaked in clear text, all your efforts will be instantly wasted. Password security is a required course for back-end development, and it is also a link that must be taken seriously in the FastAPI project. In this guide, we will start from basic principles, combined withPasslibandbcrypt, step by step to build a secure and maintainable password processing solution. There are no abstract mathematical formulas in the whole text, only directly executable code and ready-to-use best practices.
Table of contents
Password Security Basics
There is only one iron rule when it comes to storing passwords: Never store clear text passwords. This sentence sounds simple, but in actual projects, many developers often get into trouble because of "saving trouble". The code below demonstrates three common mistakes:
# ❌ 明文存储(绝对禁止)
def bad_register(email: str, password: str):
query = f"INSERT INTO users (email, password) VALUES ('{email}', '{password}')"
# ❌ 使用弱哈希算法(MD5、SHA1)
import hashlib
def weak_hash(password: str) -> str:
return hashlib.md5(password.encode()).hexdigest()
# ❌ 固定盐值(加盐也没用)
SALT = "myapp_salt"
def fixed_salt_hash(password: str) -> str:
return hashlib.sha256((password + SALT).encode()).hexdigest()
These practices not only fail to resist rainbow table attacks, but also allow attackers to easily crack them in batches. The correct approach is to use specialized password hashing libraries that automatically handle security details such as salt value, number of iterations, etc.
# ✅ 使用 passlib + bcrypt 安全哈希
from passlib.context import CryptContext
pwd_context = CryptContext(
schemes=["bcrypt"],
deprecated="auto",
bcrypt__rounds=12 # 成本因子,越大越安全,但性能开销也越大
)
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
The core principles can be summarized in four:
- Never store clear text passwords.
- Use strong hashing algorithm (such as bcrypt, Argon2).
- Use a unique random salt for each user (the library will automatically do this for you).
- Regularly evaluate and update hashing algorithms (e.g. migrating from bcrypt to Argon2).
Detailed explanation of Passlib password library
PasslibIt is the most mature password hashing library in the Python ecosystem. It provides a unified interface, supports more than 30 hashing algorithms, and can automatically handle the process of "abandoning old algorithms and upgrading to new algorithms". Ideal for use in FastAPI projects.
Installation and basic configuration
pip install passlib[bcrypt] bcrypt
After installation, we can package aPasswordManagerClasses gather commonly used operations together to facilitate maintenance and testing.
from passlib.context import CryptContext
from passlib.exc import UnknownHashError
import logging
class PasswordManager:
def __init__(self):
# 使用 bcrypt,自动弃用过期算法
self.pwd_context = CryptContext(
schemes=["bcrypt"],
deprecated="auto",
bcrypt__default_rounds=12
)
self.logger = logging.getLogger(__name__)
def hash(self, password: str) -> str:
"""将密码哈希并返回安全的哈希字符串"""
try:
return self.pwd_context.hash(password)
except Exception as e:
self.logger.error(f"密码哈希失败: {e}")
raise
def verify(self, plain_password: str, hashed_password: str) -> bool:
"""验证明文密码与哈希是否匹配"""
try:
return self.pwd_context.verify(plain_password, hashed_password)
except UnknownHashError:
# 如果哈希格式完全不认识,直接拒绝
return False
except Exception as e:
self.logger.error(f"密码验证异常: {e}")
return False
def needs_rehash(self, hashed: str) -> bool:
"""检查是否需要重新哈希(例如迭代轮数不足时)"""
return self.pwd_context.needs_update(hashed)
In this way, our FastAPI service can be simply called in the following way:
manager = PasswordManager()
# 哈希密码
hashed = manager.hash("MySecurePassword123!")
print(f"哈希结果: {hashed}")
# 输出示例: $2b$12$LJ3m4ys3Lk0mPVJWQYiTfu6a3R2KcHh0O7...
# 验证密码
print(manager.verify("MySecurePassword123!", hashed)) # True
print(manager.verify("WrongPassword", hashed)) # False
# 检查是否需要更新哈希
print(manager.needs_rehash(hashed)) # False(当前配置下不需要)
bcrypt algorithm in-depth analysis
bcrypt is one of the most widely used password hashing algorithms today. It is based on the Blowfish block encryption algorithm, has a built-in salt value and an adjustable number of rounds (cost factor), and can effectively resist brute force cracking and rainbow table attacks. The biggest difference from ordinary hash functions (such as SHA256) is that bcrypt is deliberately slow, and we can make it even slower by increasing the cost factor, thereby greatly increasing the attacker's computational cost.
How to choose cost factors
The cost factor (rounds) determines the number of iterations of bcrypt: a factor of 12 means2^12iterations. The higher the factor, the more secure the hash is, but the longer it takes to verify. The following tool class can help you find a maximum cost factor that "completes hashing in an acceptable time" on your own server:
from passlib.context import CryptContext
import time
class CostOptimizer:
def __init__(self, target_time: float = 0.1):
"""
target_time: 你希望哈希操作最多花费的时间(秒),
通常建议 0.1~0.5 秒。
"""
self.target_time = target_time
def find_optimal_cost(self) -> int:
# 从高到低尝试,直到找到耗时在目标范围内的最大因子
for cost in range(16, 4, -1):
ctx = CryptContext(schemes=["bcrypt"], bcrypt__default_rounds=cost)
start = time.time()
ctx.hash("testpassword")
duration = time.time() - start
if duration <= self.target_time:
return cost
return 12 # 默认兜底值
# 示例:希望哈希在 0.2 秒内完成
optimizer = CostOptimizer(target_time=0.2)
optimal_cost = optimizer.find_optimal_cost()
print(f"推荐成本因子: {optimal_cost}")
Avoid blocking: asynchronous password processing
FastAPI is based on asynchronous, and bcrypt hashing is a CPU-intensive operation, if directlyasyncCalling it in the route will block the entire event loop. The correct approach is to throw the calculation task to the thread pool for execution:
import asyncio
from concurrent.futures import ThreadPoolExecutor
class AsyncPasswordManager(PasswordManager):
def __init__(self, max_workers: int = 4):
super().__init__()
self.executor = ThreadPoolExecutor(max_workers=max_workers)
async def hash_async(self, password: str) -> str:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(self.executor, self.hash, password)
async def verify_async(self, plain: str, hashed: str) -> bool:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(self.executor, self.verify, plain, hashed)
Just use it like this in FastAPI routing:
pwd_manager = AsyncPasswordManager()
@app.post("/register")
async def register(email: str, password: str):
hashed_pwd = await pwd_manager.hash_async(password)
# ... 保存到数据库
Password strength verification and strategy
Even with strong hashes, weak passwords (e.g.12345678) still puts users at risk. When users set passwords, we should perform strength verification on both the front end and the back end.
Custom password strength validator
belowPasswordValidatorAbility to check length, case, numbers and special characters and return ratings and recommendations:
import re
from typing import List
from dataclasses import dataclass
@dataclass
class StrengthResult:
is_strong: bool
score: int # 0~100
feedback: List[str]
class PasswordValidator:
def __init__(self):
self.min_len = 8
self.max_len = 128
self.require_upper = True
self.require_lower = True
self.require_digit = True
self.require_special = True
self.special_chars = r"!@#$%^&*(),.?\":{}|<>"
def validate(self, password: str) -> StrengthResult:
feedback = []
score = 0
# 长度得分
if self.min_len <= len(password) <= self.max_len:
score += 25
else:
feedback.append(f"密码长度应在{self.min_len}-{self.max_len}字符之间")
# 大写字母
if self.require_upper and re.search(r'[A-Z]', password):
score += 25
elif self.require_upper:
feedback.append("需要至少一个大写字母")
# 小写字母
if self.require_lower and re.search(r'[a-z]', password):
score += 25
elif self.require_lower:
feedback.append("需要至少一个小写字母")
# 数字
if self.require_digit and re.search(r'\d', password):
score += 25
elif self.require_digit:
feedback.append("需要至少一个数字")
# 特殊字符
if self.require_special:
if any(c in self.special_chars for c in password):
score += 25
else:
feedback.append("需要至少一个特殊字符")
score = min(100, score) # 防止溢出
return StrengthResult(
is_strong=score >= 80,
score=score,
feedback=feedback
)
Combined with Pydantic models
In FastAPI, we can put the validation logic into Pydantic’s@field_validator, realize automatic verification:
from pydantic import BaseModel, field_validator, EmailStr
class RegistrationRequest(BaseModel):
email: EmailStr
password: str
confirm_password: str
first_name: str
last_name: str
@field_validator("password")
@classmethod
def validate_password(cls, v: str) -> str:
validator = PasswordValidator()
result = validator.validate(v)
if not result.is_strong:
raise ValueError(f"密码强度不足: {', '.join(result.feedback)}")
return v
@field_validator("confirm_password")
@classmethod
def passwords_match(cls, v: str, info) -> str:
if "password" in info.data and v != info.data["password"]:
raise ValueError("两次密码不一致")
return v
Now, when you submit a weak password to the registration interface, FastAPI will automatically return a 422 error with a clear message.
Secure user registration process
With reliable password hashing tools and strong verification, we can build a complete user registration and login flow. SQLAlchemy is used here as the ORM, and an asynchronous engine is used to adapt to FastAPI.
User model definition
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True, nullable=False)
username = Column(String(255), unique=True, index=True, nullable=True)
hashed_password = Column(String(255), nullable=False)
first_name = Column(String(100), nullable=False)
last_name = Column(String(100), nullable=False)
is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False)
is_locked = Column(Boolean, default=False)
failed_login_attempts = Column(Integer, default=0)
created_at = Column(DateTime, default=func.now())
User service layer
The user service encapsulates registration, login and account locking logic, and all password operations passPasswordManagerFinish:
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional
class UserService:
def __init__(self, db: AsyncSession, password_manager: PasswordManager):
self.db = db
self.pwd_manager = password_manager
self.lockout_threshold = 5 # 失败 5 次锁定账户
async def register(
self, email: str, password: str, first_name: str, last_name: str
) -> User:
# 检查邮箱唯一性
existing = await self.get_by_email(email)
if existing:
raise ValueError("邮箱已被注册")
# 创建用户(密码已哈希)
user = User(
email=email,
hashed_password=self.pwd_manager.hash(password),
first_name=first_name,
last_name=last_name
)
self.db.add(user)
await self.db.commit()
await self.db.refresh(user)
return user
async def login(self, email: str, password: str) -> Optional[User]:
user = await self.get_by_email(email)
if not user:
return None
if user.is_locked:
raise ValueError("账户已被锁定,请尝试找回密码")
if self.pwd_manager.verify(password, user.hashed_password):
# 登录成功:重置失败计数器
user.failed_login_attempts = 0
await self.db.commit()
return user
else:
# 登录失败:累加计数器,超过阈值锁定
user.failed_login_attempts += 1
if user.failed_login_attempts >= self.lockout_threshold:
user.is_locked = True
await self.db.commit()
return None
async def get_by_email(self, email: str) -> Optional[User]:
stmt = select(User).where(User.email == email)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
FastAPI interface layer can be injected directlyUserService, keeping the code simple and testable.
Password Security Best Practices
Security is a systematic project, and hashing alone is not enough. Below is a production-tested "checklist" that you can check one by one against your own projects.
🔐 Technical level
- ✅ Use
bcryptorArgon2To do password hashing, choose a reasonable cost factor.
- ✅ Each user's hash is automatically included with a unique random salt (Passlib has it built in).
- ✅ Implement password strength verification and prohibit common weak passwords.
- ✅ Use
needs_updateCheck and support smooth upgrade algorithm.
- ✅ Implement account locking policy to prevent brute force cracking.
- ✅ Use HTTPS throughout the process to avoid passwords being transmitted in clear text on the network.
- ✅ Never log passwords into logs or bug reports.
📊 Business level
- ✅ Provides secure password reset functionality (via email token instead of directly displaying the old password).
- ✅ Mandatory email verification after registration to reduce fake accounts.
- ✅ Introducing two-factor authentication (2FA) as an optional enhancement.
- ✅ Record security-critical events (login, password changes, etc.) for easy auditing.
- ✅ Conduct regular security audits and dependency library upgrades.
Detect whether the password is leaked
Even if your password is very strong, you may still be caught due to data breaches on other websites. We can leverage the “Have I Been Pwned” API to detect if a user’s password has ever appeared in a leaked database. This method uses k-anonymity technology, which only sends the hash prefix and does not reveal the full password:
import hashlib
import requests
class PasswordBreachDetector:
def __init__(self):
self.api_url = "https://api.pwnedpasswords.com/range/"
def check(self, password: str) -> tuple[bool, int]:
"""返回(是否泄露, 泄露次数)"""
try:
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
response = requests.get(f"{self.api_url}{prefix}")
response.raise_for_status()
for line in response.text.splitlines():
hash_suffix, count = line.split(':')
if hash_suffix == suffix:
return True, int(count)
return False, 0
except Exception:
# 网络问题或 API 不可用时,不应阻断注册
return False, 0
When registering or changing a password, if it is detected that the password has been leaked, the user can be advised to change the password.
Defense against brute force cracking: Simple rate limiting middleware
Multiple password attempts may cause the account to be locked, but we still need to do a layer of rate limiting at the network level to prevent attackers from consuming resources through a large number of requests:
from fastapi import Request, HTTPException, status
import time
from collections import defaultdict
class RateLimiter:
def __init__(self):
self.storage = defaultdict(list) # {ip: [timestamp, ...]}
self.window = 60 # 时间窗口(秒)
self.max_requests = 100 # 窗口内最大请求数
async def check(self, request: Request) -> bool:
client_ip = request.client.host
now = time.time()
# 移除过期的记录
self.storage[client_ip] = [
t for t in self.storage[client_ip]
if now - t < self.window
]
if len(self.storage[client_ip]) >= self.max_requests:
return False
self.storage[client_ip].append(now)
return True
# 在 FastAPI 依赖或中间件中使用
rate_limiter = RateLimiter()
Integrating the above logic into dependency injection can add effective protection to the login and registration interfaces.
Summary
This article starts from scratch and explains how to handle passwords securely in the FastAPI project. Starting from the most basic principle of "not storing plaintext", we gradually built aPasslib+bcryptHash management, password strength verification, user registration and login services, until the final security reinforcement measures (leakage detection, rate limiting). The entire link covers every key link from password submission to storage.
Three core points to remember:
- Always use a dedicated password hashing library (such as Passlib), don't invent your own algorithm.
- Password strength verification + account locking + leak detection, the trinity can effectively reduce risks.
- Security is a continuous process: Regularly upgrade hash configuration, audit logs, and pay attention to the latest security trends.
Password security is the cornerstone of web applications. Only by laying this foundation can your FastAPI project grow with confidence.
🔗 Extended reading