You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

Django DRF中Axios带有效Token请求/api/posts/返回401未授权问题

问题描述

React前端向Django Rest Framework(DRF)后端的/api/posts/发送POST请求时,收到401 Unauthorized响应。React的Axios拦截器会将localStorage中的Bearer Token添加到Authorization请求头,但请求仍失败。

DRF视图使用generics.ListCreateAPIView,权限配置为permissions.IsAuthenticatedOrReadOnly。请求头中存在Token,但POST请求始终无法通过权限验证。此外,当PostListCreateView的路由配置在项目urls.py中时,能成功创建帖子;但移至api应用的urls.py时,会出现405 METHOD "POST" not allowed错误。所有前端访问的API URL需以/api/urlpattern/形式访问。


Django DRF后端代码

项目urls.py(此配置下创建帖子成功)

from django.contrib import admin
from django.urls import path, include
from api.views import CreateUserView, ProfileView, PostListCreateView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from django.conf import settings
from django.conf.urls.static import static


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/user/register/', CreateUserView.as_view(), name='register'),
    path('api/token/', TokenObtainPairView.as_view(), name='get_token'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='refresh'),
    path('api/posts/', PostListCreateView.as_view(), name='post-list'),
    path('api_auth/', include('rest_framework.urls')),
    path('api/', include('api.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

api应用urls.py(此配置下出现405错误)

from django.urls import path
from . import views


urlpatterns = [
    path('', views.WelcomeView.as_view(), name='welcome'),
    path('home/', views.HomeView.as_view(), name='home'),
    path('<str:username>/update-profile/', views.UpdateProfileView.as_view(), name='update-profile'),
    path('<str:username>/', views.ProfileView.as_view(), name="profile"),
    path('posts/', views.PostListCreateView.as_view(), name='post-list'),
    path('posts/<int:pk>/', views.PostDetailView.as_view(), name='post-detail'), 
    path('posts/<int:post_pk>/likes/', views.LikesListCreateView.as_view(), name='like-list'),
    path('likes/<int:pk>/', views.LikesDetailView.as_view(), name='like-detail'),
    path('posts/<int:post_pk>/comments/', views.CommentListCreateView.as_view(), name='comment-list'),
    path('comments/<int:pk>/', views.CommentDetailView.as_view(), name='comment-detail'),
]

模型、序列化器及视图代码

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    body = models.TextField(max_length=240)
    media = models.ImageField(null=True, blank=True)
    created_at = models.DateTimeField(default=timezone.now)

    def number_of_likes(self):
        return self.likes.count()

    def number_of_comments(self):
        return self.comments.count()

    def get_created_at_in_user_timezone(self, author):
        user_timezone = pytz.timezone(author.profile.timezone)
        return self.created_at.astimezone(user_timezone)

class PostSerializer(serializers.ModelSerializer):
    author = serializers.StringRelatedField(source='author.username', read_only=True)
    number_of_likes = serializers.SerializerMethodField()
    number_of_comments = serializers.SerializerMethodField()

    def get_number_of_likes(self, obj):
        return obj.likes.count()

    def get_number_of_comments(self, obj):
        return obj.comments.count()

    class Meta:
        model = Post
        fields = ['id', 'author', 'body', 'media', 'created_at', 'number_of_likes', 'number_of_comments']
        read_only_fields = ['author', 'created_at', 'number_of_likes', 'number_of_comments']

class PostListCreateView(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        user = self.request.user
        return Post.objects.filter(author=user)
    
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

React前端代码

api.js

import axios from 'axios'
import { ACCESS_TOKEN } from './constants'

const api = axios.create({
    baseURL: import.meta.env.VITE_API_URL
})

api.interceptors.request.use(
    (config) => {
        if (config.url && !config.url.includes('/api/user/register/')) {
            const token = localStorage.getItem(ACCESS_TOKEN);
            if (token) {
                config.headers.Authorization = `Bearer ${token}`
            }
        }
        return config
    },
    (error) => {
        return Promise.reject(error)
    }
)

export default api;

HomeView.jsx(提交逻辑部分)

const username = localStorage.getItem('username');
const { profile, loading } = useProfile(username);
const [postData, setPostData] = useState({
    body: "",
    media: null,
});

// 处理文件上传
const handleFileChange = (e) => {
    const { name, files } = e.target;
    setPostData((prevData) => ({
        ...prevData,
        [name]: files[0],
    }));
};

// 处理文本输入
const handleChange = (e) => {
    const { name, value } = e.target;
    setPostData((prevData) => ({
        ...prevData,
        [name]: value,
    }));
};

// 提交帖子
const makePost = async (e) => {
    e.preventDefault();

    const formData = new FormData();

    Object.keys(postData).forEach((key) => {
        if (postData[key] !== "" && postData[key] !== null) {
            formData.append(key, postData[key]);
        }
    });

    try {
        const response = await api.post("/api/posts/", formData, {
            headers: {
                "Content-Type": "multipart/form-data",
            },
        });

        if (response.status === 201) {
            setPostData({ body: "", media: null });
            alert("帖子创建成功");
        } else {
            alert("帖子创建失败");
        }
    } catch (err) {
        console.error("创建帖子出错:", err.response?.data || err.message);
        alert(`错误: ${err.response?.data?.detail || err.message}`);
    }
};

解决方案

1. 修复路由优先级(解决405错误)

api应用的urls.py中,<str:username>/路由在posts/之前,会将/api/posts/匹配为用户名posts的请求,实际访问的是不支持POST方法的ProfileView,因此返回405。调整路由顺序,将posts/移至<str:username>/前面:

urlpatterns = [
    path('', views.WelcomeView.as_view(), name='welcome'),
    path('home/', views.HomeView.as_view(), name='home'),
    path('posts/', views.PostListCreateView.as_view(), name='post-list'),
    path('posts/<int:pk>/', views.PostDetailView.as_view(), name='post-detail'), 
    path('posts/<int:post_pk>/likes/', views.LikesListCreateView.as_view(), name='like-list'),
    path('likes/<int:pk>/', views.LikesDetailView.as_view(), name='like-detail'),
    path('posts/<int:post_pk>/comments/', views.CommentListCreateView.as_view(), name='comment-list'),
    path('comments/<int:pk>/', views.CommentDetailView.as_view(), name='comment-detail'),
    path('<str:username>/update-profile/', views.UpdateProfileView.as_view(), name='update-profile'),
    path('<str:username>/', views.ProfileView.as_view(), name="profile"),
]

2. 确保请求头正确携带Token

  • 检查浏览器Network面板,确认POST请求的Authorization头存在且格式为Bearer <有效Token>
  • 手动设置Content-Type: multipart/form-data时,可能覆盖拦截器添加的头,修改请求配置合并默认头:
const response = await api.post("/api/posts/", formData, {
    headers: {
        ...api.defaults.headers,
        "Content-Type": "multipart/form-data",
    },
});

3. 验证DRF的JWT配置

确保项目settings.py中正确配置Simple JWT:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}

4. 配置跨域支持(前后端域名不同时)

安装django-cors-headers并添加到项目配置:

INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
]

CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",  # React开发服务器地址
]

CORS_ALLOW_CREDENTIALS = True

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

火山引擎 最新活动