Skip to content

第六章 6.3: 基础认证

学习时间: 1 小时


1. API 认证: 进入飞行控制中心的通行证

认证 是验证用户身份的过程。与使用会话和 Cookie 的网站不同,无状态 (stateless) API 通常使用 令牌

流程如下:

  1. 用户将其登录名和密码发送到特定的端点(例如,/login)。
  2. 服务器验证它们。如果正确,它会生成一个唯一的、加密的 令牌(一个长字符串)并将其发送回。
  3. 每次后续请求访问受保护资源(例如,POST /planets)时,用户必须在 Authorization 头部中附加此令牌。
  4. 服务器验证令牌的有效性,如果令牌正确,则执行请求。

💡 太空类比:

  • 登录名/密码 = 您的生物识别扫描,用于获取通行证。
  • 令牌 = 您在进入飞行控制中心时获得的电子通行证(ID 卡)。
  • Authorization: Bearer <令牌> 头部 = 您将通行证贴在每个受保护门旁的读卡器上。
  • 受保护的端点 (POST, PUT, DELETE) = 服务器机房或启动控制台的门。

2. Laravel 中的认证: Sanctum

Laravel 为 API 认证提供了一个优雅的解决方案——Laravel Sanctum。它非常适合 SPA(单页应用程序)、移动应用程序和简单的基于令牌的 API。

步骤 1: 安装和配置 Sanctum

Sanctum 已在标准的 Laravel 应用程序中安装,但我们将检查配置。

  1. 发布配置 (如果尚未完成):
    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
    
  2. 运行迁移 (将创建 personal_access_tokens 表):
    php artisan migrate
    
  3. User 模型中添加 Trait: 打开 app/Models/User.php 并确保它使用了 HasApiTokens trait。
    // app/Models/User.php
    use Laravel\Sanctum\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens, HasFactory, Notifiable;
        // ...
    }
    

步骤 2: 创建令牌颁发端点 我们需要一个用户将发送登录名/密码的路由。

添加到 routes/api.php

// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
use Illuminate\Validation\ValidationException;

Route::post('/login', function (Request $request) {
    $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    $user = User::where('email', $request->email)->first();

    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['凭据无效。'],
        ]);
    }

    // 返回令牌
    return response()->json([
        'token' => $user->createToken('api-token')->plainTextToken
    ]);
});

您可以通过 Seeder 或 Tinker 创建一个测试用户。

步骤 3: 保护路由 现在,我们将保护我们的 CRUD 操作。修改 routes/api.php

// routes/api.php
use App\Http\Controllers\PlanetController;

// 用于查看行星的公共路由
Route::get('/planets', [PlanetController::class, 'index']);
Route::get('/planets/{planet}', [PlanetController::class, 'show']);

// 受保护的路由组
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/planets', [PlanetController::class, 'store']);
    Route::put('/planets/{planet}', [PlanetController::class, 'update']);
    Route::delete('/planets/{planet}', [PlanetController::class, 'destroy']);

    // 退出路由 (删除令牌)
    Route::post('/logout', function (Request $request) {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => '您已成功退出'], 200);
    });
});

中间件 auth:sanctum 将检查 Authorization 头部中是否存在有效的令牌。


3. FastAPI 中的认证: OAuth2 和 JWT

FastAPI 没有内置的认证系统,但有强大的工具来实现在此。事实标准是 OAuth2 与 JWT 令牌

步骤 1: 安装依赖

pip install "python-jose[cryptography]" "passlib[bcrypt]" "python-multipart"

  • python-jose: 用于创建和验证 JWT 令牌。
  • passlib: 用于哈希和验证密码。
  • python-multipart: 用于处理表单数据 (usernamepassword)。

步骤 2: 创建安全模块 (security.py) 这是一个很好的实践——将所有认证逻辑提取到一个单独的文件中。

创建文件 security.py

# security.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta, timezone

# --- 设置 ---
SECRET_KEY = "your-super-secret-key-that-is-long-and-random" # ⚠️ 请替换为您的密钥!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# --- 工具 ---
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login")

# --- 函数 ---
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# --- 令牌验证依赖函数 ---
def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="无法验证凭据",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    # 在这里可以从数据库返回用户,目前只是返回用户名
    return {"username": username}

步骤 3: 集成到 main.py 现在,我们将其连接到我们的应用程序。

  1. 创建 /login 端点:

    # main.py
    from fastapi.security import OAuth2PasswordRequestForm
    from fastapi import Depends, APIRouter
    from . import security # 导入我们的模块
    
    # ... 您的 FastAPI 代码 ...
    router = APIRouter(prefix="/api/v1")
    
    @router.post("/login")
    def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
        # 这里应该有数据库中的用户验证
        # 例如,我们有一个测试用户
        is_user_valid = (form_data.username == "testuser" and
                         security.verify_password("testpass", security.get_password_hash("testpass")))
    
        if not is_user_valid:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="用户名或密码错误",
            )
        access_token = security.create_access_token(data={"sub": form_data.username})
        return {"access_token": access_token, "token_type": "bearer"}
    
    # ...
    app.include_router(router)
    

  2. 保护端点:

    我们使用 get_current_user 依赖。

    # main.py 或在您的行星路由中
    
    @router.post("/planets", status_code=status.HTTP_201_CREATED)
    def create_planet(
        planet: PlanetCreate,
        current_user: dict = Depends(security.get_current_user) # <-- 保护!
    ):
        # 创建行星的逻辑...
        print(f"用户 {current_user['username']} 正在创建行星。")
        # ...
        return new_planet
    
    # 同样保护 PUT 和 DELETE
    


4. 前端使用令牌

我们的前端现在应该首先获取令牌,保存它(例如,在 localStorage 中),并将其附加到每个受保护的请求中。

JavaScript 示例 (fetch):

// 1. 登录
async function login(email, password) {
    const response = await fetch('http://localhost:8001/api/login', { // Laravel API 地址
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({email, password})
    });
    const data = await response.json();

    if (data.token) {
        localStorage.setItem('api_token', data.token); // 保存令牌
    }
}

// 2. 发送受保护的请求
async function createPlanet(planetData) {
    const token = localStorage.getItem('api_token');

    const response = await fetch('http://localhost:8001/api/planets', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}` // <--- 附加令牌!
        },
        body: JSON.stringify(planetData)
    });
    // ...
}


巩固练习

1. 无状态 API 最常用于认证:

2. 在 Laravel 中,使用令牌保护路由所使用的中间件是:

3. 在 FastAPI 中,用于从登录表单获取数据的依赖项是:

4. 令牌如何在受保护的请求中从客户端传输到服务器?


🚀 章节总结:

您已为您的 API 设置了“访问控制系统”。现在,不是每个想要的人都能修改您的“银河数据库”了。

  • ✅ 理解了基于令牌的身份验证原则。
  • 🔐 在 Laravel Sanctum 中实现了令牌颁发和路由保护。
  • ⚙️ 在 FastAPI 中配置了基于 OAuth2 和 JWT 的身份验证。
  • 🛰️ 了解了前端如何保存和使用令牌。

您的 API 不仅功能齐全,而且安全可靠。 然而,为了让其他开发人员能够使用它们,他们需要“操作说明”。

📌 检查:

  • 尝试使用 Postman 或 Insomnia 对 /api/planets (在 Laravel 中) 或 /api/v1/planets (在 FastAPI 中) 发送不带令牌的 POST 请求。您应该会收到 401 Unauthorized 错误。
  • 请求 /login,获取令牌,将其添加到 Authorization 标头中,然后重复 POST 请求。它应该会成功执行。