feat(auth): 添加验证码功能以增强登录安全性
实现验证码生成、存储和验证逻辑 - 新增验证码生成工具函数 - 在登录接口中添加验证码校验 - 添加获取验证码图片的API端点 - 更新用户登录schema包含验证码字段 - 添加captcha依赖到requirements.txt
This commit is contained in:
parent
1427cf4884
commit
c98adab6ce
6
backend/.trae/rules/git-commit-message.md
Normal file
6
backend/.trae/rules/git-commit-message.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
scene: git_message
|
||||
---
|
||||
|
||||
在此处编写规则,自定义 AI 生成提交信息的风格。
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from fastcrud import FastCRUD
|
||||
@ -19,6 +20,7 @@ from utils.password import verify_password, get_password_hash
|
||||
from utils.jwt import create_access_token
|
||||
from config import app_settings
|
||||
from utils.redis_client import get_redis, set_token_in_redis
|
||||
from utils.captcha import create_captcha, validate_captcha
|
||||
from middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
@ -27,6 +29,19 @@ router = APIRouter(prefix="/users", tags=["users"])
|
||||
user_crud = FastCRUD(User, UserResponse)
|
||||
|
||||
|
||||
@router.get("/captcha")
|
||||
async def get_captcha(redis_conn: redis.Redis = Depends(get_redis)):
|
||||
"""获取验证码图片"""
|
||||
# 创建验证码
|
||||
captcha_id, image_bytes = await create_captcha(redis_conn)
|
||||
|
||||
# 返回验证码图片,同时在响应头中返回验证码ID
|
||||
response = Response(content=image_bytes, media_type="image/png")
|
||||
response.headers["X-Captcha-ID"] = captcha_id
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
user_data: UserLogin,
|
||||
@ -34,6 +49,17 @@ async def login(
|
||||
redis_conn: redis.Redis = Depends(get_redis),
|
||||
):
|
||||
"""用户登录"""
|
||||
# 验证验证码
|
||||
is_captcha_valid = await validate_captcha(
|
||||
redis_conn, user_data.captcha_id, user_data.captcha_code
|
||||
)
|
||||
if not is_captcha_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="验证码错误或已过期",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 查找用户
|
||||
result = await db.execute(select(User).filter(User.username == user_data.username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
@ -25,4 +25,7 @@ alembic>=1.18.4
|
||||
asyncpg>=0.31.0
|
||||
|
||||
# Redis异步客户端
|
||||
redis>=7.2.0
|
||||
redis>=7.2.0
|
||||
|
||||
# 验证码生成
|
||||
captcha>=0.7.1
|
||||
@ -47,6 +47,8 @@ class UserLogin(BaseModel):
|
||||
|
||||
username: str = Field(..., description="登录名")
|
||||
password: str = Field(..., description="密码")
|
||||
captcha_id: str = Field(..., description="验证码ID")
|
||||
captcha_code: str = Field(..., description="验证码")
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
|
||||
Binary file not shown.
BIN
backend/auth/utils/__pycache__/captcha.cpython-313.pyc
Normal file
BIN
backend/auth/utils/__pycache__/captcha.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
69
backend/auth/utils/captcha.py
Normal file
69
backend/auth/utils/captcha.py
Normal file
@ -0,0 +1,69 @@
|
||||
from io import BytesIO
|
||||
from captcha.image import ImageCaptcha
|
||||
from uuid import uuid4
|
||||
from utils.redis_client import get_redis, set_token_in_redis
|
||||
import redis.asyncio as redis
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def generate_captcha(length: int = 4) -> tuple[str, bytes]:
|
||||
"""生成验证码图片和验证码文本"""
|
||||
# 生成随机验证码
|
||||
characters = string.digits + string.ascii_uppercase
|
||||
captcha_text = ''.join(random.choices(characters, k=length))
|
||||
|
||||
# 生成验证码图片
|
||||
image = ImageCaptcha(width=150, height=50)
|
||||
image_bytes = image.generate(captcha_text)
|
||||
|
||||
# 将图片转换为字节流
|
||||
buffer = BytesIO()
|
||||
buffer.write(image_bytes.read())
|
||||
buffer.seek(0)
|
||||
|
||||
return captcha_text, buffer.getvalue()
|
||||
|
||||
|
||||
async def save_captcha_to_redis(redis_conn: redis.Redis, captcha_id: str, captcha_text: str, expire_seconds: int = 300):
|
||||
"""将验证码保存到Redis"""
|
||||
await redis_conn.set(f"captcha:{captcha_id}", captcha_text, ex=expire_seconds)
|
||||
|
||||
|
||||
async def get_captcha_from_redis(redis_conn: redis.Redis, captcha_id: str) -> str | None:
|
||||
"""从Redis获取验证码"""
|
||||
return await redis_conn.get(f"captcha:{captcha_id}")
|
||||
|
||||
|
||||
async def delete_captcha_from_redis(redis_conn: redis.Redis, captcha_id: str):
|
||||
"""从Redis删除验证码"""
|
||||
await redis_conn.delete(f"captcha:{captcha_id}")
|
||||
|
||||
|
||||
async def validate_captcha(redis_conn: redis.Redis, captcha_id: str, user_input: str) -> bool:
|
||||
"""验证验证码是否正确"""
|
||||
stored_captcha = await get_captcha_from_redis(redis_conn, captcha_id)
|
||||
if not stored_captcha:
|
||||
return False
|
||||
|
||||
# 忽略大小写比较
|
||||
is_valid = stored_captcha.upper() == user_input.upper()
|
||||
|
||||
# 无论验证成功与否,都删除验证码(防止重复使用)
|
||||
await delete_captcha_from_redis(redis_conn, captcha_id)
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
async def create_captcha(redis_conn: redis.Redis) -> tuple[str, bytes]:
|
||||
"""创建验证码并保存到Redis"""
|
||||
# 生成验证码
|
||||
captcha_text, image_bytes = generate_captcha()
|
||||
|
||||
# 生成唯一ID
|
||||
captcha_id = str(uuid4())
|
||||
|
||||
# 保存到Redis,有效期5分钟
|
||||
await save_captcha_to_redis(redis_conn, captcha_id, captcha_text, 300)
|
||||
|
||||
return captcha_id, image_bytes
|
||||
BIN
backend/swagger-ui/favicon-32x32.png
Normal file
BIN
backend/swagger-ui/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
2
backend/swagger-ui/swagger-ui-bundle.js
Normal file
2
backend/swagger-ui/swagger-ui-bundle.js
Normal file
File diff suppressed because one or more lines are too long
3
backend/swagger-ui/swagger-ui.css
Normal file
3
backend/swagger-ui/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user