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

Django项目Token认证失败:登出接口测试异常求助

Django基于Session Cookie的登出功能测试失败问题

我在开发Django项目的用户逻辑时,发现基于Session Cookie的登出功能在浏览器中正常运行,但测试时无法工作。登录响应中的Cookie似乎没被后续请求正确保存,测试返回“Authentication credentials were not provided”错误。


项目相关代码

views.py

from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.contrib.auth.base_user import AbstractBaseUser
from django.db.models.manager import BaseManager
from rest_framework import generics, status, permissions, serializers
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.authtoken.models import Token

# from rest_framework.authentication import SessionAuthentication
# from rest_framework.exceptions import PermissionDenied

from apps.users.models import User
from apps.users.serializers import (
    UserSerializer,
    UserLoginSerializer,
)


class UserLoginView(generics.GenericAPIView):
    serializer_class: type = UserLoginSerializer

    def get_object(self) -> AbstractBaseUser:
        """Overwrite method to get the authenticated user object"""
        try:
            session_key: str = self.request.COOKIES.get("session")
            user: AbstractBaseUser = Token.objects.get(key=session_key).user
        except ObjectDoesNotExist:
            user = None
        return user

    def post(self, request: Request) -> Response:
        response: Response
        user: AbstractBaseUser

        try:
            # parse data
            data: dict = {
                k: v[0] if type(v) is list and len(v) == 1 else v
                for k, v in dict(request.data).items()
            }
            # Verify the absence of session token
            serializer: serializers.Serializer = self.get_serializer(data=data)
            user = self.get_object()
            if user:
                raise ValidationError("Already logged in.")

            # Run serializer validations
            user = serializer.is_valid()
            if not user:
                raise ValidationError("Unable to log in with provided credentials.")

            # Create response content (in this case, just a confirmation message)
            response_content: dict[str, str] = {
                "detail": "Login successful",
            }
            # Create response object
            response = Response(data=response_content, status=status.HTTP_200_OK)
            # Aggregate cookie to the response
            token: Token
            token, _ = Token.objects.get_or_create(user=serializer.validated_data)
            response.set_cookie(
                key="session",
                value=token.key,
                secure=True,
                httponly=True,
                samesite="lax",
            )

        except ValidationError as error:
            response = Response(
                {"detail": error.message},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        return response


class UserLogoutView(generics.GenericAPIView):

    def get_serializer_class(self) -> serializers.Serializer:
        return serializers.Serializer  # Return basic Serializer class

    def delete(self, request: Request) -> Response:
        response: Response
        try:
            session_key: str = request.COOKIES.get("session")
            token: Token = Token.objects.get(key=session_key)
            response = Response(
                status=status.HTTP_200_OK,
                data={"detail": "Logout successful"},
            )
            response.delete_cookie(key="session")
            token.delete()

        except ObjectDoesNotExist:
            response = Response(
                {"error": "Session cookie not found"},
                status=status.HTTP_401_UNAUTHORIZED,
            )
        except Exception as error:
            print(error)
            response = Response(
                {"detail": str(error)},
                status=status.HTTP_400_BAD_REQUEST,
            )

        return response

tests_views.py

class TestLogoutViews(TestCase):
    def setUp(self) -> None:
        self.client = Client()
        self.data: dict[str, str] = {
            "username": "testuser",
            "email": "test@test.com",
            "password": "Password1",
        }
        self.logout_url: str = reverse("user_logout")
        self.login_url: str = reverse("user_login")

        self.client.post(
            reverse("user_register"),
            json.dumps(self.data),
            content_type="application/json",
        )
        self.data.pop("email")

    def test_logout_view_correct_DELETE(self) -> None:

        # Log in first
        response: HttpResponse = self.client.post(
            self.login_url,
            json.dumps(self.data),
            content_type="application/json",
        )

        print()
        print(response.status_code, self.client.cookies)
        print(self.client.get("").wsgi_request.user)
        print()

        # Send the DELETE request
        response: HttpResponse = self.client.delete(self.logout_url)

        # Check if the response is 200
        self.assertEqual(
            response.status_code,
            200,
            msg=(
                f"Response is {response.status_code}, expected 200. ",
                f"Response content: {response.content}",
            ),
        )

        # Check if the cookie is deleted
        self.assertFalse(
            "session" in self.client.cookies, msg="Cookie should be deleted"
        )

settings.py

REST_FRAMEWORK: dict[str, str] = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework.authentication.TokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ),
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

测试输出

200 Set-Cookie: session=5875a98d252d48a019226a342fb09cb785271a32; HttpOnly; Path=/; SameSite=lax; Secure
AnonymousUser
======================================================================
FAIL: test_logout_view_correct_DELETE (users.test_views.TestLogoutViews)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\user\repositories\flicks-and-picks\src\backend\apps\users\test_views.py", line 518, in test_logout_view_correct_DELETE
    self.assertEqual(
AssertionError: 401 != 200 : ('Response is 401, expected 200. ', 'Response content: b\'{"detail":"Authentication credentials were not provided."}\''))

更新:测试请求头的尝试

通过独立测试程序发现添加包含Authorization字段的请求头可避免“Authentication credentials were not provided”提示,但仍不清楚该字段的正确值设置。测试代码及输出如下:

测试代码

import os
import json
import requests

client = requests.session()
login_url = "http://127.0.0.1:8000/users/login/"
logout_url = "http://127.0.0.1:8000/users/logout/"
login_data: dict[str, str] = {"username": "test1", "password": "Password1"}

print()
response: requests.Response = client.post(login_url, json=login_data)
print(response.status_code, response.json()["detail"])
print()
print(client.cookies)

# Save the session cookie
cookies = response.cookies
session_cookie = response.cookies['session']
headers = {'Authorization': f'Token {session_cookie}'}

print()
response: requests.Response = client.delete(logout_url, cookies=cookies)
print(response.status_code, response.json()["detail"])

print()
response: requests.Response = client.delete(logout_url, headers=headers, cookies=cookies)
print(response.status_code, response.json()["detail"])

输出

200 Login successful

<RequestsCookieJar[<Cookie session=734df1e09726dd888a90ef25fc5b6f15151d8125 for 127.0.0.1/>]>

401 Authentication credentials were not provided.

401 Token matching query does not exist.

问题原因及解决方案

  1. 核心问题:settings.py中配置了DRF的TokenAuthentication和SessionAuthentication作为默认认证类,登出视图未跳过认证检查,DRF会优先尝试用默认认证方式验证,但你的自定义逻辑是从Cookie中取Token,DRF默认认证不识别这个自定义Cookie,因此抛出认证错误。

  2. 具体修复步骤:

    • 给UserLogoutView添加权限配置,允许未认证访问(自行处理Cookie Token验证):
      class UserLogoutView(generics.GenericAPIView):
          permission_classes = [permissions.AllowAny]  # 添加此行
          
          # 原有代码保持不变
      
    • 修复独立测试中的Authorization头:session_cookie是Cookie对象,需取其value属性,正确写法:
      headers = {'Authorization': f'Token {session_cookie.value}'}
      
    • 可选优化:自定义认证类让DRF识别Cookie中的Token,替代默认TokenAuthentication:
      # apps/users/authentication.py
      from rest_framework.authentication import BaseAuthentication
      from rest_framework.exceptions import AuthenticationFailed
      from rest_framework.authtoken.models import Token
      
      class CookieTokenAuthentication(BaseAuthentication):
          def authenticate(self, request):
              session_key = request.COOKIES.get('session')
              if not session_key:
                  return None
              try:
                  token = Token.objects.get(key=session_key)
              except Token.DoesNotExist:
                  raise AuthenticationFailed('Invalid token')
              return (token.user, token)
      
      然后更新settings.py:
      REST_FRAMEWORK: dict[str, str] = {
          "DEFAULT_AUTHENTICATION_CLASSES": (
              "apps.users.authentication.CookieTokenAuthentication",
              "rest_framework.authentication.SessionAuthentication",
          ),
          "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
      }
      

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 22:49:54