You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何结合FastAPI安全认证与Bootstrap5实现登录跳转功能

FastAPI OAuth2-JWT + Bootstrap5 集成方案(Cookie存储+登录跳转)

一、FastAPI 后端配置(JWT存入Cookie)

1. 安装核心依赖

pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart

2. 完整后端代码实现

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from pydantic import BaseModel

# 核心配置(生产环境用环境变量加载)
SECRET_KEY = "your-random-secret-key-here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

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

# 模拟用户数据库
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "hashed_password": pwd_context.hash("secret123"),
        "email": "johndoe@example.com",
    }
}

class User(BaseModel):
    username: str
    email: str | None = None

class UserInDB(User):
    hashed_password: str

# 密码验证与用户认证
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_user(db, username: str):
    if username in db:
        return UserInDB(**db[username])

def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user or not verify_password(password, user.hashed_password):
        return False
    return user

# JWT令牌生成
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

# 登录接口:将JWT存入HttpOnly Cookie
@app.post("/login")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token = create_access_token(
        data={"sub": user.username},
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    # 设置安全Cookie
    response = JSONResponse(content={"msg": "Login successful"})
    response.set_cookie(
        key="access_token",
        value=f"Bearer {access_token}",
        httponly=True,
        max_age=ACCESS_TOKEN_EXPIRE_MINUTES*60,
        samesite="Lax",
        # secure=True,  # 生产环境HTTPS下启用
    )
    return response

# 认证依赖:从Cookie读取并验证JWT
async def get_current_user():
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        # 从Cookie获取令牌
        from fastapi import Request
        request: Request = Depends()
        token = request.cookies.get("access_token")
        if not token:
            raise credentials_exception
        payload = jwt.decode(token.split(" ")[1], SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if not username or not get_user(fake_users_db, username):
            raise credentials_exception
        return get_user(fake_users_db, username)
    except JWTError:
        raise credentials_exception

# 需要认证的About接口
@app.get("/api/about")
async def read_about(current_user: User = Depends(get_current_user)):
    return {"message": f"Welcome to About page, {current_user.username}!"}

二、Bootstrap5 前端实现(登录跳转+认证检查)

1. 登录页面(login.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <div class="row justify-content-center">
            <div class="col-md-6">
                <div class="card">
                    <div class="card-header">
                        <h3 class="text-center">Login</h3>
                    </div>
                    <div class="card-body">
                        <form id="loginForm">
                            <div class="mb-3">
                                <label for="username" class="form-label">Username</label>
                                <input type="text" class="form-control" id="username" required>
                            </div>
                            <div class="mb-3">
                                <label for="password" class="form-label">Password</label>
                                <input type="password" class="form-control" id="password" required>
                            </div>
                            <button type="submit" class="btn btn-primary w-100">Login</button>
                        </form>
                        <div id="errorMsg" class="mt-3 text-danger text-center d-none"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
        document.getElementById('loginForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;

            const formData = new FormData();
            formData.append('username', username);
            formData.append('password', password);

            try {
                const response = await fetch('/login', {
                    method: 'POST',
                    body: formData
                });

                if (response.ok) {
                    // 登录成功自动跳转About页面
                    window.location.href = '/about.html';
                } else {
                    const res = await response.json();
                    document.getElementById('errorMsg').textContent = res.detail;
                    document.getElementById('errorMsg').classList.remove('d-none');
                }
            } catch (err) {
                document.getElementById('errorMsg').textContent = 'Login failed, please try again';
                document.getElementById('errorMsg').classList.remove('d-none');
            }
        });
    </script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

2. About页面(about.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>About</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <h1 class="text-center">About Page</h1>
        <div id="welcomeMsg" class="mt-4 text-center fs-5"></div>
        <button id="logoutBtn" class="btn btn-danger mt-4 d-block mx-auto">Logout</button>
    </div>

    <script>
        // 页面加载时检查认证状态
        async function checkAuth() {
            try {
                const response = await fetch('/api/about', {
                    method: 'GET',
                    credentials: 'include'  // 自动携带Cookie
                });

                if (response.ok) {
                    const data = await response.json();
                    document.getElementById('welcomeMsg').textContent = data.message;
                } else {
                    // 认证失败跳回登录页
                    window.location.href = '/login.html';
                }
            } catch (err) {
                window.location.href = '/login.html';
            }
        }

        // 登出:清除Cookie并跳转登录页
        document.getElementById('logoutBtn').addEventListener('click', () => {
            document.cookie = "access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
            window.location.href = '/login.html';
        });

        window.onload = checkAuth;
    </script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

三、关键注意事项

  • Cookie安全:生产环境必须启用secure=True,确保Cookie仅在HTTPS传输;httponly=True防止前端JS读取Cookie,规避XSS风险;samesite="Lax"降低CSRF攻击概率。
  • 密钥管理:不要硬编码SECRET_KEY,用环境变量加载,可通过openssl rand -hex 32生成随机安全密钥。
  • 前端请求:所有需要认证的接口请求必须添加credentials: 'include',确保浏览器自动携带Cookie。
  • 过期处理:Cookie的max_age与JWT过期时间保持一致,过期后前端会自动跳回登录页。

内容的提问来源于stack exchange,提问作者brad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 21:37:38