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

使用GitHub Actions自动化测试时FastAPI认证失败求助

GitHub Actions中JWT认证测试401失败(本地正常)

8个测试里6个因401未授权失败,仅登录端点@router.post("/token")和验证JWT_SECRET_KEY加载的测试通过,本地测试全部正常。密钥用于签名JWT令牌,认证流程与FastAPI官方OAuth2 JWT实现一致,添加认证前本地和GitHub Actions测试均能通过。


GitHub Actions配置

name: Tests

on:
  push:
    branches: [ "dev", "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python 3.11
      uses: actions/setup-python@v3
      with:
        python-version: "3.11"
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        pip install -r backend/requirements.txt
    - name: Lint with flake8
      run: |
        # stop the build if there are Python syntax errors or undefined names
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    - name: Test backend tests
      env:
        JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
      run: |
        # run backend tests
        pytest backend/tests/tests.py
    - name: Check test status
      if: ${{ failure() }}
      run: exit 1

测试代码关键片段

仅展示三个核心测试用例:test_secret_keys(通过)、test_authentication(通过)、test_authenticated_user_access(失败):

import pytest
import os

from fastapi.testclient import TestClient

from .test_utils import FakeUser
from settings import ROOT, PROJECT_DIR
from api import app
from db.database import SessionLocal
from db.models import DbBlog

automatic_fields = ['timestamp', 'id', 'image_url']
files_dir = os.path.join(ROOT, 'files')
client = TestClient(app)
user = FakeUser("a", "a")


def get_header():
    response = client.post("http://localhost:8000/token",
                           data={"grant_type": "password",
                                 "username": user.username,
                                 "password": user.password})

    if response.status_code == 200:
        access_token = response.json()["access_token"]
        return {"Authorization": f"Bearer {access_token}"}


@pytest.fixture
def delete_all_data():
    """
    This fixture runs on all tests apart from delete. This allows delete to be run on its own as well as with all tests.
    """
    # clean db
    session = SessionLocal()
    session.query(DbBlog).delete()
    session.commit()
    session.close()

    # clean dir with images
    if os.path.exists(files_dir):
        all_image_files = os.listdir(files_dir)
        for file in all_image_files:
            if file != '.gitkeep':
                os.remove(os.path.join(files_dir, file))


def test_secret_keys():
    assert os.environ.get("JWT_SECRET_KEY") is not None


def test_authentication(delete_all_data):
    """
    Authenticate fake user and check they can access a restricted endpoint
    """
    response = client.post("http://localhost:8000/token",
                             data={"grant_type": "password",
                                   "username": user.username,
                                   "password": user.password})
    assert response.status_code == 200


def test_authenticated_user_access(delete_all_data):
    response = client.get("http://localhost:8000/users/me", headers=get_header())
    assert response.status_code == 200

认证相关端点代码

@router.post("/token")
async def login(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_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
    return {"access_token": access_token, "token_type": "bearer"}

@router.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user

可能的解决方案

1. 检查JWT密钥加载时机

应用实例可能在GitHub Actions设置环境变量前就已初始化,导致令牌签名与验证使用的密钥不一致。可以:

  • 在测试文件中延迟导入应用,确保环境变量加载后再初始化TestClient:
    def test_authenticated_user_access(delete_all_data):
        from api import app
        client = TestClient(app)
        # 重新获取令牌并发起请求
        response = client.post("http://localhost:8000/token",
                               data={"grant_type": "password",
                                     "username": user.username,
                                     "password": user.password})
        access_token = response.json()["access_token"]
        headers = {"Authorization": f"Bearer {access_token}"}
        response = client.get("http://localhost:8000/users/me", headers=headers)
        assert response.status_code == 200
    
  • 或在api.py中改为动态读取环境变量,而非模块加载时一次性读取。

2. 添加调试信息定位问题

在GitHub Actions测试步骤中添加调试输出,确认密钥状态和令牌解码情况:

- name: Test backend tests
  env:
    JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
  run: |
    echo "JWT_SECRET_KEY loaded: ${{ env.JWT_SECRET_KEY != '' }}"
    pytest backend/tests/tests.py -s

同时在get_current_active_user函数中添加异常打印,查看令牌验证失败的具体原因。

3. 确认测试用户与令牌有效期

  • 检查fake_users_db在GitHub Actions环境中是否正确初始化了测试用户(登录测试通过说明用户存在,但仍需确认用户状态是否为活跃)。
  • 确保ACCESS_TOKEN_EXPIRE_MINUTES在本地和CI环境中配置一致,避免测试过程中令牌过期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 21:37:44