feat(auth): 实现用户认证与授权功能
- 添加用户登录、登出及令牌管理功能 - 实现基于JWT和Redis的认证系统 - 完善用户和角色管理API - 添加密码加密与验证功能 - 配置数据库连接和Redis客户端 - 实现中间件进行权限验证 - 初始化管理员和测试用户数据 - 更新模型和接口文档 - 添加测试API和配置文件
This commit is contained in:
parent
98f53cbaaa
commit
f1da855793
BIN
backend/auth/__pycache__/config.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/__pycache__/init_data.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/init_data.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/__pycache__/main.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/__pycache__/middleware.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/middleware.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/__pycache__/schemas.cpython-313.pyc
Normal file
BIN
backend/auth/__pycache__/schemas.cpython-313.pyc
Normal file
Binary file not shown.
0
backend/auth/apps/__init__.py
Normal file
0
backend/auth/apps/__init__.py
Normal file
BIN
backend/auth/apps/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/auth/apps/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/apps/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/auth/apps/__pycache__/urls.cpython-313.pyc
Normal file
Binary file not shown.
0
backend/auth/apps/roles/__init__.py
Normal file
0
backend/auth/apps/roles/__init__.py
Normal file
BIN
backend/auth/apps/roles/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/auth/apps/roles/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/apps/roles/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/auth/apps/roles/__pycache__/urls.cpython-313.pyc
Normal file
Binary file not shown.
65
backend/auth/apps/roles/urls.py
Normal file
65
backend/auth/apps/roles/urls.py
Normal file
@ -0,0 +1,65 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastcrud import FastCRUD
|
||||
|
||||
from database import get_db
|
||||
from models import Role, User
|
||||
from schemas import RoleCreate, RoleResponse
|
||||
from middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/roles", tags=["roles"])
|
||||
|
||||
# 创建FastCRUD实例
|
||||
role_crud = FastCRUD(Role, RoleResponse)
|
||||
|
||||
|
||||
@router.post("/", response_model=RoleResponse)
|
||||
async def create_role(role_data: RoleCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""创建角色"""
|
||||
# 创建角色实例
|
||||
db_role = Role(
|
||||
name=role_data.name,
|
||||
creator=role_data.creator
|
||||
)
|
||||
|
||||
# 使用FastCRUD创建角色
|
||||
created_role = await role_crud.create(db, db_role)
|
||||
return created_role
|
||||
|
||||
|
||||
@router.get("/", response_model=list[RoleResponse])
|
||||
async def get_roles(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""获取所有角色"""
|
||||
roles = await role_crud.get_multi(db)
|
||||
return roles
|
||||
|
||||
|
||||
@router.get("/{role_id}", response_model=RoleResponse)
|
||||
async def get_role(role_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""获取单个角色"""
|
||||
role = await role_crud.get(db, role_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
return role
|
||||
|
||||
|
||||
@router.put("/{role_id}", response_model=RoleResponse)
|
||||
async def update_role(role_id: int, role_data: RoleCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""更新角色"""
|
||||
# 准备更新数据
|
||||
update_data = role_data.dict()
|
||||
|
||||
# 使用FastCRUD更新角色
|
||||
updated_role = await role_crud.update(db, role_id, update_data)
|
||||
if not updated_role:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
return updated_role
|
||||
|
||||
|
||||
@router.delete("/{role_id}")
|
||||
async def delete_role(role_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""删除角色"""
|
||||
deleted = await role_crud.delete(db, role_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="角色不存在")
|
||||
return {"message": "角色删除成功"}
|
||||
10
backend/auth/apps/urls.py
Normal file
10
backend/auth/apps/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
from .users.urls import router as users_router
|
||||
from .roles.urls import router as roles_router
|
||||
|
||||
# 创建主路由
|
||||
api_router = APIRouter()
|
||||
|
||||
# 包含子路由
|
||||
api_router.include_router(users_router)
|
||||
api_router.include_router(roles_router)
|
||||
0
backend/auth/apps/users/__init__.py
Normal file
0
backend/auth/apps/users/__init__.py
Normal file
BIN
backend/auth/apps/users/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/auth/apps/users/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/apps/users/__pycache__/urls.cpython-313.pyc
Normal file
BIN
backend/auth/apps/users/__pycache__/urls.cpython-313.pyc
Normal file
Binary file not shown.
127
backend/auth/apps/users/urls.py
Normal file
127
backend/auth/apps/users/urls.py
Normal file
@ -0,0 +1,127 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from fastcrud import FastCRUD
|
||||
from datetime import timedelta
|
||||
import redis.asyncio as redis
|
||||
|
||||
from database import get_db
|
||||
from models import User
|
||||
from schemas import UserCreate, UserResponse, UserLogin, Token
|
||||
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 middleware import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
# 创建FastCRUD实例
|
||||
user_crud = FastCRUD(User, UserResponse)
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(user_data: UserLogin, db: AsyncSession = Depends(get_db), redis_conn: redis.Redis = Depends(get_redis)):
|
||||
"""用户登录"""
|
||||
# 查找用户
|
||||
user = await db.query(User).filter(User.username == user_data.username).first()
|
||||
|
||||
# 验证用户是否存在且密码正确
|
||||
if not user or not verify_password(user_data.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=app_settings.access_token_expire_minutes)
|
||||
access_token = create_access_token(
|
||||
data={"sub": str(user.id), "username": user.username},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
# 将令牌存储到Redis
|
||||
expire_seconds = int(access_token_expires.total_seconds())
|
||||
await set_token_in_redis(redis_conn, user.id, access_token, expire_seconds)
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse)
|
||||
async def create_user(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建用户"""
|
||||
# 生成密码哈希值
|
||||
hashed_password = get_password_hash(user_data.password)
|
||||
|
||||
# 创建用户实例,使用密码哈希值
|
||||
db_user = User(
|
||||
username=user_data.username,
|
||||
nickname=user_data.nickname,
|
||||
email=user_data.email,
|
||||
phone=user_data.phone,
|
||||
wx_openid=user_data.wx_openid,
|
||||
avatar=user_data.avatar,
|
||||
password_hash=hashed_password,
|
||||
role_id=user_data.role_id
|
||||
)
|
||||
|
||||
# 使用FastCRUD创建用户
|
||||
created_user = await user_crud.create(db, db_user)
|
||||
return created_user
|
||||
|
||||
|
||||
@router.get("/", response_model=list[UserResponse])
|
||||
async def get_users(db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""获取所有用户"""
|
||||
users = await user_crud.get_multi(db)
|
||||
return users
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""获取单个用户"""
|
||||
user = await user_crud.get(db, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse)
|
||||
async def update_user(user_id: int, user_data: UserCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""更新用户"""
|
||||
# 生成密码哈希值
|
||||
hashed_password = get_password_hash(user_data.password)
|
||||
|
||||
# 准备更新数据
|
||||
update_data = user_data.dict()
|
||||
update_data["password_hash"] = hashed_password
|
||||
del update_data["password"] # 删除明文密码
|
||||
|
||||
# 使用FastCRUD更新用户
|
||||
updated_user = await user_crud.update(db, user_id, update_data)
|
||||
if not updated_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return updated_user
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
"""删除用户"""
|
||||
deleted = await user_crud.delete(db, user_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return {"message": "用户删除成功"}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(current_user: User = Depends(get_current_user), redis_conn: redis.Redis = Depends(get_redis)):
|
||||
"""用户登出"""
|
||||
from utils.redis_client import delete_token_from_redis
|
||||
await delete_token_from_redis(redis_conn, current_user.id)
|
||||
return {"message": "登出成功"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_current_user_info(current_user: User = Depends(get_current_user)):
|
||||
"""获取当前用户信息"""
|
||||
return current_user
|
||||
@ -1,21 +1,23 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
|
||||
class PostgreSQLSettings(BaseSettings):
|
||||
"""PostgreSQL数据库配置"""
|
||||
host: str = "localhost"
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 5432
|
||||
user: str = "postgres"
|
||||
password: str = "postgres"
|
||||
database: str = "book_system"
|
||||
user: str = "admin"
|
||||
password: str = "PostgresAdmin@123"
|
||||
database: str = "booksystem"
|
||||
min_size: int = 5
|
||||
max_size: int = 20
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""生成数据库连接URL"""
|
||||
return f"postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
|
||||
encoded_password = quote_plus(self.password)
|
||||
return f"postgresql+asyncpg://{self.user}:{encoded_password}@{self.host}:{self.port}/{self.database}"
|
||||
|
||||
class Config:
|
||||
env_prefix = "POSTGRES_"
|
||||
@ -23,9 +25,9 @@ class PostgreSQLSettings(BaseSettings):
|
||||
|
||||
class RedisSettings(BaseSettings):
|
||||
"""Redis配置"""
|
||||
host: str = "localhost"
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 6379
|
||||
password: Optional[str] = None
|
||||
password: Optional[str] = "RedisAdmin@123"
|
||||
db: int = 0
|
||||
|
||||
@property
|
||||
@ -44,7 +46,7 @@ class AppSettings(BaseSettings):
|
||||
name: str = "Book System API"
|
||||
version: str = "1.0.0"
|
||||
debug: bool = False
|
||||
secret_key: str = "your-secret-key"
|
||||
secret_key: str = "BookSystemAPIQRAdmin123"
|
||||
algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 30
|
||||
|
||||
|
||||
26
backend/auth/database.py
Normal file
26
backend/auth/database.py
Normal file
@ -0,0 +1,26 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from config import postgres_settings
|
||||
|
||||
# 创建异步数据库引擎
|
||||
engine = create_async_engine(
|
||||
postgres_settings.url,
|
||||
echo=False,
|
||||
future=True
|
||||
)
|
||||
|
||||
# 创建异步会话工厂
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
|
||||
async def get_db():
|
||||
"""获取数据库会话"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
102
backend/auth/init_data.py
Normal file
102
backend/auth/init_data.py
Normal file
@ -0,0 +1,102 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from models import Role, User
|
||||
from utils.password import get_password_hash
|
||||
|
||||
|
||||
async def init_roles(db: AsyncSession):
|
||||
"""初始化角色数据"""
|
||||
# 检查是否已经存在角色数据
|
||||
result = await db.execute(select(Role))
|
||||
existing_roles = result.scalars().all()
|
||||
if existing_roles:
|
||||
print("角色数据已存在,跳过初始化")
|
||||
return
|
||||
|
||||
# 创建初始角色
|
||||
roles = [
|
||||
Role(name="管理员", creator="system"),
|
||||
Role(name="普通用户", creator="system"),
|
||||
Role(name="访客", creator="system"),
|
||||
]
|
||||
|
||||
for role in roles:
|
||||
db.add(role)
|
||||
|
||||
await db.commit()
|
||||
print("角色数据初始化完成")
|
||||
|
||||
|
||||
async def init_admin_user(db: AsyncSession):
|
||||
"""初始化管理员用户"""
|
||||
# 检查是否已经存在管理员用户
|
||||
result = await db.execute(select(User).filter(User.username == "admin"))
|
||||
existing_admin = result.scalar_one_or_none()
|
||||
if existing_admin:
|
||||
print("管理员用户已存在,跳过初始化")
|
||||
return
|
||||
|
||||
# 获取管理员角色
|
||||
result = await db.execute(select(Role).filter(Role.name == "管理员"))
|
||||
admin_role = result.scalar_one_or_none()
|
||||
if not admin_role:
|
||||
print("管理员角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 创建管理员用户
|
||||
admin_user = User(
|
||||
username="admin",
|
||||
nickname="系统管理员",
|
||||
email="admin@example.com",
|
||||
phone="13800138000",
|
||||
password_hash=get_password_hash("admin"),
|
||||
role_id=admin_role.id,
|
||||
avatar="",
|
||||
wx_openid=""
|
||||
)
|
||||
|
||||
db.add(admin_user)
|
||||
await db.commit()
|
||||
print("管理员用户初始化完成")
|
||||
|
||||
|
||||
async def init_test_user(db: AsyncSession):
|
||||
"""初始化测试用户"""
|
||||
# 检查是否已经存在测试用户
|
||||
result = await db.execute(select(User).filter(User.username == "test"))
|
||||
existing_test_user = result.scalar_one_or_none()
|
||||
if existing_test_user:
|
||||
print("测试用户已存在,跳过初始化")
|
||||
return
|
||||
|
||||
# 获取普通用户角色
|
||||
result = await db.execute(select(Role).filter(Role.name == "普通用户"))
|
||||
user_role = result.scalar_one_or_none()
|
||||
if not user_role:
|
||||
print("普通用户角色不存在,请先初始化角色数据")
|
||||
return
|
||||
|
||||
# 创建测试用户
|
||||
test_user = User(
|
||||
username="test",
|
||||
nickname="测试用户",
|
||||
email="test@example.com",
|
||||
phone="13800138001",
|
||||
password_hash=get_password_hash("test"),
|
||||
role_id=user_role.id,
|
||||
avatar="",
|
||||
wx_openid=""
|
||||
)
|
||||
|
||||
db.add(test_user)
|
||||
await db.commit()
|
||||
print("测试用户初始化完成")
|
||||
|
||||
|
||||
async def init_all_data(db: AsyncSession):
|
||||
"""初始化所有数据"""
|
||||
print("开始初始化数据库数据...")
|
||||
await init_roles(db)
|
||||
await init_admin_user(db)
|
||||
await init_test_user(db)
|
||||
print("数据库数据初始化完成")
|
||||
@ -1,7 +1,39 @@
|
||||
from fastapi import FastAPI
|
||||
from contextlib import asynccontextmanager
|
||||
from apps.urls import api_router
|
||||
from database import engine, get_db
|
||||
from models import Base
|
||||
from init_data import init_all_data
|
||||
|
||||
#实例化FastAPI
|
||||
app = FastAPI()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 启动时初始化数据库
|
||||
async with engine.begin() as conn:
|
||||
# 创建所有表
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# 初始化数据
|
||||
async for db in get_db():
|
||||
await init_all_data(db)
|
||||
break
|
||||
|
||||
yield
|
||||
# 关闭时的清理工作
|
||||
await engine.dispose()
|
||||
|
||||
# 实例化FastAPI
|
||||
app = FastAPI(
|
||||
title="Book System API",
|
||||
description="图书系统API",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
|
||||
|
||||
# 包含路由
|
||||
app.include_router(api_router)
|
||||
|
||||
# 声明装饰器方法和路径
|
||||
@app.get("/")
|
||||
@ -15,4 +47,4 @@ if __name__ == "__main__":
|
||||
import uvicorn
|
||||
import os
|
||||
name = f"{os.path.splitext(os.path.basename(os.path.abspath(__file__)))[0]}:app"
|
||||
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["."])
|
||||
uvicorn.run(name, host="0.0.0.0", port=8000, reload=True, reload_dirs=["_"])
|
||||
|
||||
46
backend/auth/middleware.py
Normal file
46
backend/auth/middleware.py
Normal file
@ -0,0 +1,46 @@
|
||||
from fastapi import Request, HTTPException, status, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import redis.asyncio as redis
|
||||
|
||||
from database import get_db
|
||||
from utils.jwt import decode_token
|
||||
from utils.redis_client import get_redis, check_token_in_redis
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def get_current_user(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncSession = Depends(get_db), redis_conn: redis.Redis = Depends(get_redis)):
|
||||
"""获取当前用户"""
|
||||
token = credentials.credentials
|
||||
|
||||
# 解码令牌
|
||||
payload = decode_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
user_id = int(payload.get("sub"))
|
||||
|
||||
# 检查令牌是否在Redis中
|
||||
is_valid = await check_token_in_redis(redis_conn, user_id, token)
|
||||
if not is_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="令牌已过期或已登出",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 查找用户
|
||||
from models import User
|
||||
user = await db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
|
||||
return user
|
||||
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
@ -12,6 +12,7 @@ class Role(Base):
|
||||
id = Column(Integer, primary_key=True, index=True, comment="角色ID")
|
||||
name = Column(String(50), nullable=False, unique=True, comment="角色名称")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
|
||||
creator = Column(String(50), nullable=True, comment="创建人")
|
||||
|
||||
# 关联到User
|
||||
@ -27,9 +28,11 @@ class User(Base):
|
||||
email = Column(String(50), nullable=True, comment="电子邮箱")
|
||||
phone = Column(String(11), nullable=True, comment="手机号码")
|
||||
wx_openid = Column(String(100), nullable=True, comment="微信OpenID")
|
||||
password = Column(String(100), nullable=False, comment="密码")
|
||||
password_hash = Column(String(128), nullable=False, comment="密码哈希值")
|
||||
avatar = Column(String(255), nullable=True, comment="头像路径")
|
||||
role_id = Column(Integer, ForeignKey("role.id"), default=2, comment="角色ID")
|
||||
createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
updatetime = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
|
||||
lastlogintime = Column(DateTime, nullable=True, comment="最后登录时间")
|
||||
|
||||
# 关联到Role
|
||||
|
||||
@ -1,11 +1,21 @@
|
||||
# FastAPI核心
|
||||
fastapi>=0.133.0
|
||||
uvicorn[standard]>=0.41.0
|
||||
fastapi_cdn_host==0.10.0
|
||||
|
||||
# Pydantic数据验证
|
||||
pydantic>=2.5.0
|
||||
pydantic-settings>=2.1.0
|
||||
|
||||
# 密码加密
|
||||
passlib[bcrypt]>=1.7.4
|
||||
|
||||
# FastCRUD
|
||||
fastcrud==0.21.0
|
||||
|
||||
# JWT
|
||||
python-jose[cryptography]>=3.3.0
|
||||
|
||||
# PostgreSQL + SQLAlchemy ORM(包含异步支持)
|
||||
sqlalchemy>=2.0.46
|
||||
asyncpg>=0.31.0
|
||||
|
||||
@ -10,6 +10,7 @@ class UserBase(BaseModel):
|
||||
email: Optional[EmailStr] = Field(None, max_length=50, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, max_length=11, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, max_length=100, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, max_length=255, description="头像路径")
|
||||
role_id: int = Field(2, description="角色ID")
|
||||
|
||||
|
||||
@ -24,10 +25,18 @@ class UserLogin(BaseModel):
|
||||
password: str = Field(..., description="密码")
|
||||
|
||||
|
||||
class UserResponse(UserBase):
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应模型"""
|
||||
id: int = Field(..., description="用户ID")
|
||||
username: str = Field(..., description="登录名")
|
||||
nickname: str = Field(..., description="昵称")
|
||||
email: Optional[EmailStr] = Field(None, description="电子邮箱")
|
||||
phone: Optional[str] = Field(None, description="手机号码")
|
||||
wx_openid: Optional[str] = Field(None, description="微信OpenID")
|
||||
avatar: Optional[str] = Field(None, description="头像路径")
|
||||
role_id: int = Field(..., description="角色ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
lastlogintime: datetime = Field(..., description="最后登录时间")
|
||||
|
||||
class Config:
|
||||
@ -60,6 +69,7 @@ class RoleResponse(RoleBase):
|
||||
"""角色响应模型"""
|
||||
id: int = Field(..., description="角色ID")
|
||||
createtime: datetime = Field(..., description="创建时间")
|
||||
updatetime: datetime = Field(..., description="更新时间")
|
||||
creator: Optional[str] = Field(None, description="创建人")
|
||||
|
||||
class Config:
|
||||
|
||||
19
backend/auth/test_api.py
Normal file
19
backend/auth/test_api.py
Normal file
@ -0,0 +1,19 @@
|
||||
import urllib.request
|
||||
|
||||
# 测试根路径
|
||||
print("Testing root path...")
|
||||
try:
|
||||
response = urllib.request.urlopen('http://127.0.0.1:8000/')
|
||||
print(f"Status code: {response.status}")
|
||||
print(f"Response: {response.read().decode('utf-8')}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# 测试/docs路径
|
||||
print("\nTesting /docs path...")
|
||||
try:
|
||||
response = urllib.request.urlopen('http://127.0.0.1:8000/docs')
|
||||
print(f"Status code: {response.status}")
|
||||
print(f"Response length: {len(response.read().decode('utf-8'))} characters")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
0
backend/auth/utils/__init__.py
Normal file
0
backend/auth/utils/__init__.py
Normal file
BIN
backend/auth/utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/auth/utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/utils/__pycache__/jwt.cpython-313.pyc
Normal file
BIN
backend/auth/utils/__pycache__/jwt.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/utils/__pycache__/password.cpython-313.pyc
Normal file
BIN
backend/auth/utils/__pycache__/password.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/auth/utils/__pycache__/redis_client.cpython-313.pyc
Normal file
BIN
backend/auth/utils/__pycache__/redis_client.cpython-313.pyc
Normal file
Binary file not shown.
25
backend/auth/utils/jwt.py
Normal file
25
backend/auth/utils/jwt.py
Normal file
@ -0,0 +1,25 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from config import app_settings
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
"""创建访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=app_settings.access_token_expire_minutes)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, app_settings.secret_key, algorithm=app_settings.algorithm)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def decode_token(token: str):
|
||||
"""解码令牌"""
|
||||
try:
|
||||
payload = jwt.decode(token, app_settings.secret_key, algorithms=[app_settings.algorithm])
|
||||
return payload
|
||||
except JWTError:
|
||||
return None
|
||||
14
backend/auth/utils/password.py
Normal file
14
backend/auth/utils/password.py
Normal file
@ -0,0 +1,14 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
# 创建密码加密上下文
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""获取密码哈希值"""
|
||||
return pwd_context.hash(password)
|
||||
41
backend/auth/utils/redis_client.py
Normal file
41
backend/auth/utils/redis_client.py
Normal file
@ -0,0 +1,41 @@
|
||||
import redis.asyncio as redis
|
||||
from config import redis_settings
|
||||
|
||||
# 创建Redis连接池
|
||||
redis_pool = redis.ConnectionPool.from_url(
|
||||
redis_settings.url,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
|
||||
async def get_redis():
|
||||
"""获取Redis连接"""
|
||||
async with redis.Redis(connection_pool=redis_pool) as conn:
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def set_token_in_redis(redis_conn, user_id: int, token: str, expire_seconds: int):
|
||||
"""将令牌存储到Redis"""
|
||||
key = f"user:token:{user_id}"
|
||||
await redis_conn.set(key, token, ex=expire_seconds)
|
||||
|
||||
|
||||
async def get_token_from_redis(redis_conn, user_id: int):
|
||||
"""从Redis获取令牌"""
|
||||
key = f"user:token:{user_id}"
|
||||
return await redis_conn.get(key)
|
||||
|
||||
|
||||
async def delete_token_from_redis(redis_conn, user_id: int):
|
||||
"""从Redis删除令牌"""
|
||||
key = f"user:token:{user_id}"
|
||||
await redis_conn.delete(key)
|
||||
|
||||
|
||||
async def check_token_in_redis(redis_conn, user_id: int, token: str):
|
||||
"""检查令牌是否在Redis中且有效"""
|
||||
stored_token = await get_token_from_redis(redis_conn, user_id)
|
||||
return stored_token == token
|
||||
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