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




