跳转至

FastAPI 处理登录页面

概要: 利用fastapi-login包实现FastAPI前端页面的鉴权保护

创建时间: 2023.07.30 14:42:54

更新时间: 2023.07.30 15:22:08

安装依赖

提示

截止到2023.07.30,fastapi-login最新包依然依赖的是pydantic V1版本,与fastapi最新版0.100.x使用的V2版本不兼容,此处使用0.80.0来确保兼容性

Bash
1
2
3
pip install fastapi==0.80.0
pip install fastapi-login
pip install python-multipart

实例代码

Python
import os
from datetime import timedelta

import uvicorn
from fastapi import FastAPI, Depends, Request, status
from fastapi.responses import HTMLResponse, Response, RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_login import LoginManager
from fastapi_login.exceptions import InvalidCredentialsException
from loguru import logger as log

app = FastAPI()


# 处理登录错误的异常
class NotAuthenticatedException(Exception):
    pass


# noinspection PyUnusedLocal
def exc_handler(request, exc):
    """未授权时重定向到登录页面,上面的两个参数为必需项"""
    return RedirectResponse(url='/login')


# You also have to add an exception handler to your app instance
app.add_exception_handler(NotAuthenticatedException, exc_handler)

SECRET = os.urandom(24).hex()
manager = LoginManager(
    secret=SECRET,
    token_url='/auth/token',
    use_cookie=True,
    custom_exception=NotAuthenticatedException
)

fake_db = {'lzwang': {'password': '123456'}}


@manager.user_loader()
async def load_user(user_id: str):
    user = fake_db.get(user_id)
    return user


# noinspection PyUnusedLocal
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
    """简易登录页面"""
    return """
        <html>
            <head>
                <title>登录页面</title>
                <link href="/static/css/style.css" rel="stylesheet">
            </head>
            <body>
                <h1>用户登录</h1>
                <form method="post" action="/auth/token">
                    <label for="username">用户名:</label><br>
                    <input type="text" id="username" name="username"><br>
                    <label for="password">密码:</label><br>
                    <input type="password" id="password" name="password"><br><br>
                    <input type="submit" value="登录">
                </form>
            </body>
        </html>
    """


# noinspection PyUnusedLocal
@app.post('/auth/token')
async def login(response: Response, input_data: OAuth2PasswordRequestForm = Depends()):
    # 获取用户输入凭据
    log.info(f'Login: User Input = {input_data.username}, {input_data.password}')

    # 获取数据库正确的凭据
    db_data = await load_user(input_data.username)
    log.info(f'Login: Database user_data = {db_data}')

    # 凭据校验
    if not db_data:
        raise InvalidCredentialsException
    elif input_data.password != db_data['password']:
        raise InvalidCredentialsException

    # 创建 token ,配置自动过期时间
    access_token = manager.create_access_token(
        data={"sub": input_data.username}, expires=timedelta(seconds=15)
    )
    log.success(f"Login SUCCESS, token: {access_token}")

    # 将此 token 设置到 response 上,重定向到首页
    response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
    manager.set_cookie(response, access_token)

    return response


# noinspection PyUnusedLocal
@app.get('/')
async def must_auth_page(user=Depends(manager)):
    """测试必须登录的页面"""
    return f"Visit Page with AUTH OK, Login SUCCESS"


@app.get('/{item}')
async def no_need_auth_page(item: int):
    """测试无需登录的页面"""
    return f"Visit Page without AUTH OK, item={item}"


if __name__ == "__main__":
    uvicorn.run(
        app=app,
        host='127.0.0.1',
        port=7777,
        log_level='debug'
    )
现在可以访问http://127.0.0.1:7777/页面,就会被重定向到登录页面如下
image.png
在输入我们测试的用户名lzwang和密码123456登录成功后,将被重定向需要鉴权的原始页面
image.png
此外,在log中我们可以看到此次获取到的token信息
Bash
INFO:     Started server process [73176]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:7777 (Press CTRL+C to quit)
INFO:     127.0.0.1:49199 - "GET / HTTP/1.1" 307 Temporary Redirect
INFO:     127.0.0.1:49199 - "GET /login HTTP/1.1" 200 OK
INFO:     127.0.0.1:49199 - "GET /static/css/style.css HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:49199 - "GET /favicon.ico HTTP/1.1" 422 Unprocessable Entity
INFO:     127.0.0.1:49200 - "POST /auth/token HTTP/1.1" 303 See Other
INFO:     127.0.0.1:49200 - "GET / HTTP/1.1" 200 OK
2023-07-30 14:57:06.011 | INFO     | __main__:login:85 - Login: User Input = lzwang, 123456
2023-07-30 14:57:06.011 | INFO     | __main__:login:89 - Login: Database user_data = {'password': '123456'}
2023-07-30 14:57:06.011 | SUCCESS  | __main__:login:101 - Login SUCCESS, token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsendhbmciLCJleHAiOjE2OTA3MDAyNDF9.Cr1SXrC5LVKZk_6v4miMBXuYD0rtj11pf9OA-5RpbQo
上面获取的token为eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsendhbmciLCJleHAiOjE2OTA3MDAyNDF9.Cr1SXrC5LVKZk_6v4miMBXuYD0rtj11pf9OA-5RpbQo

实现原理

  1. 实例化一种处理未鉴权的异常类NotAuthenticatedException,然后写一个exc_handler函数,用来处理当app捕获到此异常类后,自动重定向到/login页面
  2. 实例化一个fastapi_login.LoginManager,用来对登录的用户信息进行校验(鉴权操作)
  3. 编写一个简单的/login前端HTML网页
  4. 实现鉴权,在/auth/token中,我们进行凭据校验,token创建和配置,以及设置token给response,重定向到首页
  5. 在app的endpoints中,需要鉴权的页面增加入参user=Depends(manager),无需鉴权的页面则不需要任何变更

参考