关于CSRF的后端问题(FastAPI、AuthX、SQLAlchemy、Pydantic等)
我在尝试创建一个带有登录系统和角色的简单API。我的代码有个问题:
from fastapi import FastAPI, Response, HTTPException, Request, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from typing import Annotated
from authx import AuthX, AuthXConfig
app = FastAPI()
config = AuthXConfig()
config.JWT_SECRET_KEY = "secret_key"
config.JWT_ACCESS_COOKIE_NAME = "cookie"
config.JWT_TOKEN_LOCATION = ["cookies"]
config.JWT_COOKIE_CSRF_PROTECT = True
security = AuthX(config=config)
engine = create_async_engine("sqlite+aiosqlite:///basedata.db")
Session = async_sessionmaker(engine, expire_on_commit=False)
async def get_session():
async with Session() as session:
yield session
SessionDep = Annotated[AsyncSession, Depends(get_session)]
class Base(DeclarativeBase):
pass
class LogModel(Base):
__tablename__ = "Log_Model"
Login: Mapped[str] = mapped_column(primary_key=True)
Pass: Mapped[str]
Role: Mapped[str] = mapped_column(default="user")
class LogAddSchema(BaseModel):
Login: str
Pass: str
@app.post("/run_engine")
async def run_engine():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
return {"Data": "Runned"}
@app.post("/add_new_user")
async def add_user(user: LogAddSchema, session: SessionDep):
stmt = select(LogModel).where(LogModel.Login == user.Login)
result = await session.execute(stmt)
if result.scalar_one_or_none():
return {"error": "Користувач уже існує"}
new_user = LogModel(Login=user.Login, Pass=user.Pass)
session.add(new_user)
await session.commit()
return {"message": "Користувач створений"}
@app.post("/add_admin")
async def add_admin(user: LogAddSchema, session: SessionDep):
stmt = select(LogModel).where(LogModel.Login == user.Login)
result = await session.execute(stmt)
if result.scalar_one_or_none():
return {"error": "Користувач уже існує"}
new_user = LogModel(Login=user.Login, Pass=user.Pass, Role="admin")
session.add(new_user)
await session.commit()
return {"message": "Адміністратор створений"}
@app.post("/user_login")
async def user_login(user: LogAddSchema, session: SessionDep, response: Response):
stmt = select(LogModel).where(LogModel.Login == user.Login)
result = await session.execute(stmt)
db_user = result.scalar_one_or_none()
if db_user is None or db_user.Pass != user.Pass:
raise HTTPException(status_code=400, detail="Incorrect login or password")
token = security.create_access_token(uid=db_user.Login, data={"role": db_user.Role})
response = JSONResponse(content={"message": "Вхід успішний"})
security.set_access_cookies(response, token)
return response
@app.get("/profile")
async def profile(request: Request):
try:
payload = await security.access_token_required(request)
return {"user": payload.sub, "role": getattr(payload, "role", "user")}
except HTTPException:
return {"Please": "Login"}
@app.get("/admin")
async def admin_panel(request: Request):
try:
payload = await security.access_token_required(request)
role = getattr(payload, "role", "user")
if role != "admin":
raise HTTPException(status_code=403, detail="Access denied")
return {"message": "Admin panel"}
except HTTPException:
return {"Please": "Login"}
@app.delete("/delete_user/{login}")
async def delete_user(login: str, request: Request, session: SessionDep):
payload = await security.access_token_required(request)
role = getattr(payload, "role", "user")
if role != "admin":
raise HTTPException(status_code=403, detail="Access denied")
stmt = select(LogModel).where(LogModel.Login == login)
result = await session.execute(stmt)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
await session.delete(user)
await session.commit()
return {"message": f"User '{login}' deleted successfully"}
当我想使用user_login端点时,出现如下错误:
Traceback (most recent call last):
File "D:\Codes\Python\env\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 416, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "D:\Codes\Python\env\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\fastapi\applications.py", line 1134, in __call__
await super().__call__(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\applications.py", line 107, in __call__
await self.middleware_stack(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__
raise exc
File "D:\Codes\Python\env\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "D:\Codes\Python\env\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\routing.py", line 716, in __call__
await self.middleware_stack(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\routing.py", line 736, in app
await route.handle(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\routing.py", line 290, in handle
await self.app(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 119, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 105, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 424, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 312, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\backend.py", line 78, in user_login
security.set_access_cookies(response, token)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\authx\main.py", line 482, in set_access_cookies
self._set_cookies(token=token, type="access", response=response, max_age=max_age)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\authx\main.py", line 194, in _set_cookies
response.set_cookie(
^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'set_cookie'
ERROR:uvicorn.error:Exception in ASGI application
Traceback (most recent call last):
File "D:\Codes\Python\env\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 416, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "D:\Codes\Python\env\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\fastapi\applications.py", line 1134, in __call__
await super().__call__(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\applications.py", line 107, in __call__
await self.middleware_stack(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__
raise exc
File "D:\Codes\Python\env\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "D:\Codes\Python\env\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
await self.app(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\routing.py", line 716, in __call__
await self.middleware_stack(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\routing.py", line 736, in app
await route.handle(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\routing.py", line 290, in handle
await self.app(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 119, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
raise exc
File "D:\Codes\Python\env\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 105, in app
response = await f(request)
^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 424, in app
raw_response = await run_endpoint_function(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "D:\Codes\Python\env\Lib\site-packages\fastapi\routing.py", line 312, in run_endpoint_function
return await dependant.call(**values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\backend.py", line 78, in user_login
security.set_access_cookies(response, token)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\authx\main.py", line 482, in set_access_cookies
self._set_cookies(token=token, type="access", response=response, max_age=max_age)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Codes\Python\env\Lib\site-packages\authx\main.py", line 194, in _set_cookies
response.set_cookie(
^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'set_cookie'
有些AI说这是AuthX和 CSRF的问题,但我不确定。
解决方案
这并非主要的CSRF问题。导致直接崩溃的原因是set_access_cookies() 的参数顺序颠倒了。AuthX的文档指出该方法的签名是set_access_cookies(token, response, max_age=None),因此token必须在前,Response对象在后。在你的代码中你调用security.set_access_cookies(response, token),这会让AuthX将 JWT字符串当作响应对象。随后它尝试在该字符串上调用set_cookie(),从而引发AttributeError: 'str' 对象没有属性 'set_cookie'。
所以这行 security.set_access_cookies(response, token) 应该改为 security.set_access_cookies(token, response)。
你的user_login端点应该像这样:
@app.post("/user_login")
async def user_login(user: LogAddSchema, session: SessionDep):
stmt = select(LogModel).where(LogModel.Login == user.Login)
result = await session.execute(stmt)
db_user = result.scalar_one_or_none()
if db_user is None or db_user.Pass != user.Pass:
raise HTTPException(status_code=400, detail="Incorrect login or password")
token = security.create_access_token(
uid=db_user.Login,
data={"role": db_user.Role}
)
response = JSONResponse(content={"message": "Вхід успішний"})
security.set_access_cookies(token, response)
return response
在 /profile和 /admin这两个端点中也存在一个逻辑问题。它会捕获所有HTTPExceptions,包括你自己的403 Access denied,并把它们变成 "Please": "Login"。因此经过身份验证但非管理员的用户可能会看到错误的提示。
except HTTPException:
return {"Please": "Login"}
可以像下面这样写一个更精炼的版本:
@app.get("/profile")
async def profile(request: Request):
payload = await security.access_token_required(request)
return {"user": payload.sub, "role": getattr(payload, "role", "user")}
@app.get("/admin")
async def admin_panel(request: Request):
payload = await security.access_token_required(request)
role = getattr(payload, "role", "user")
if role != "admin":
raise HTTPException(status_code=403, detail="Access denied")
return {"message": "Admin panel"}
另外,为了安全起见,请不要以明文存储密码。在保存用户信息之前,使用密码哈希(如passlib/bcrypt)进行存储。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。