FastAPI-passlib密码加密

一、为什么需要密码加密

安全风险

  • 明文存储密码极易泄露
  • 数据库被攻击时用户信息全部暴露
  • 彩虹表攻击可快速破解简单密码

加密原则

  • 单向加密(不可逆)
  • 加盐(Salt)防止彩虹表攻击
  • 慢速算法(增加暴力破解成本)

二、Passlib 配置

复制代码
pip install "passlib[bcrypt]"

from passlib.context import CryptContext

# 创建密码上下文
pwd_context = CryptContext(
    schemes=["bcrypt"],  # 使用 bcrypt 算法
    deprecated="auto"    # 自动标记过时的哈希
)

# 也可以配置多个方案
pwd_context = CryptContext(
    schemes=["bcrypt", "argon2"],
    deprecated="auto",
    bcrypt__rounds=12,  # bcrypt 迭代次数
)

三、密码加密与验证

复制代码
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 加密密码
def hash_password(password: str) -> str:
    """将明文密码加密"""
    return pwd_context.hash(password)

# 验证密码
def verify_password(plain_password: str, hashed_password: str) -> bool:
    """验证密码是否正确"""
    return pwd_context.verify(plain_password, hashed_password)

# 示例
password = "my_secret_password123"
hashed = hash_password(password)
print(hashed)  
# $2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW

# 验证
is_valid = verify_password(password, hashed)
print(is_valid)  # True

is_valid = verify_password("wrong_password", hashed)
print(is_valid)  # False

四、在用户注册中使用

复制代码
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas import UserCreate, User
from app import crud

router = APIRouter(prefix="/auth", tags=["authentication"])

@router.post("/register", response_model=User, status_code=status.HTTP_201_CREATED)
async def register(
    user: UserCreate,
    db: AsyncSession = Depends(get_db)
):
    """用户注册"""
    # 检查邮箱是否已存在
    existing_user = await crud.get_user_by_email(db, user.email)
    if existing_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Email already registered"
        )
    
    # 创建用户(密码会在 crud.create_user 中加密)
    new_user = await crud.create_user(db, user)
    return new_user

五、用户登录验证

复制代码
from datetime import datetime, timedelta
from jose import JWTError, jwt
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

# JWT 配置
SECRET_KEY = "your-secret-key-keep-it-secret"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")

def create_access_token(data: dict, expires_delta: timedelta = None):
    """创建 JWT token"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

@router.post("/login")
async def login(
    form_data: OAuth2PasswordRequestForm = Depends(),
    db: AsyncSession = Depends(get_db)
):
    """用户登录"""
    # 查询用户
    user = await crud.get_user_by_email(db, form_data.username)
    
    # 验证用户存在且密码正确
    if not user or not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # 创建 token
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.email},
        expires_delta=access_token_expires
    )
    
    return {
        "access_token": access_token,
        "token_type": "bearer"
    }

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
):
    """从 token 获取当前用户"""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email: str = payload.get("sub")
        if email is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = await crud.get_user_by_email(db, email)
    if user is None:
        raise credentials_exception
    
    return user

# 受保护的路由
@router.get("/me", response_model=User)
async def read_users_me(current_user = Depends(get_current_user)):
    """获取当前用户信息"""
    return current_user

六、密码重置流程

复制代码
import secrets
from datetime import datetime, timedelta

# 生成重置令牌
def generate_reset_token():
    """生成密码重置令牌"""
    return secrets.token_urlsafe(32)

# 存储令牌(需要在数据库中添加字段)
class User(Base):
    # ... 其他字段
    reset_token = Column(String(100), nullable=True)
    reset_token_expires = Column(DateTime(timezone=True), nullable=True)

@router.post("/forgot-password")
async def forgot_password(
    email: str,
    db: AsyncSession = Depends(get_db)
):
    """请求密码重置"""
    user = await crud.get_user_by_email(db, email)
    if not user:
        # 安全起见,不透露用户是否存在
        return {"message": "If email exists, reset link will be sent"}
    
    # 生成令牌
    reset_token = generate_reset_token()
    expires = datetime.utcnow() + timedelta(hours=1)
    
    # 更新用户
    await crud.update_user(db, user.id, {
        "reset_token": reset_token,
        "reset_token_expires": expires
    })
    
    # 发送邮件(此处省略)
    # send_reset_email(user.email, reset_token)
    
    return {"message": "Password reset email sent"}

@router.post("/reset-password")
async def reset_password(
    token: str,
    new_password: str,
    db: AsyncSession = Depends(get_db)
):
    """重置密码"""
    # 查找令牌对应的用户
    result = await db.execute(
        select(User).where(
            and_(
                User.reset_token == token,
                User.reset_token_expires > datetime.utcnow()
            )
        )
    )
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired reset token"
        )
    
    # 更新密码并清除令牌
    await crud.update_user(db, user.id, {
        "password": new_password,  # crud 会自动加密
        "reset_token": None,
        "reset_token_expires": None
    })
    
    return {"message": "Password reset successful"}
相关推荐
lay_liu2 小时前
Spring 简介
java·后端·spring
yuanlaile2 小时前
Golang实现在线教育直播、农场监控直播 幼儿园监控直播
开发语言·后端·golang·go直播实战
y = xⁿ2 小时前
重生之我创作出了小红书:计数模块 SDS 位图分片与偏移 异步发送
后端·kafka·intellij-idea
fengxin_rou3 小时前
详解深浅拷贝:从原理到实现的完整指南
java·后端·浅拷贝·深拷贝
tsyjjOvO3 小时前
【SpringMVC 进阶】拦截器、文件上传、异常处理与 SSM 整合全解析
java·后端·spring
计算机学姐3 小时前
基于SpringBoot+Vue的智能民宿预定游玩系统【AI智能客服+数据可视化】
java·vue.js·spring boot·后端·mysql·spring·信息可视化
小江的记录本3 小时前
【泛型】泛型:泛型擦除、通配符、上下界限定
java·windows·spring boot·后端·spring·maven·mybatis
pupudawang3 小时前
springboot下使用druid-spring-boot-starter
java·spring boot·后端
lierenvip3 小时前
Spring Boot 自动配置
java·spring boot·后端