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

添加CORS_TRUSTED_ORIGINS后Django仍出现CSRF错误求助

密码修改PUT请求出现403 Forbidden错误,其他PUT/POST请求正常

问题描述

实现密码修改功能时,React前端向Django后端发送PUT请求触发403错误,Django控制台提示“Origin checking failed - http://localhost:3000 does not match any trusted origins”,但其他PUT/POST请求无此问题。

代码详情

settings.py

REST_FRAMEWORK = {
    # 'DEFAULT_PERMISSION_CLASSES': [
        # 'rest_framework.permissions.IsAuthenticated',
    # ],
    # 'DEFAULT_AUTHENTICATION_CLASSES': [
    #     'rest_framework_simplejwt.authentication.JWTAuthentication',
    # ],
    'DEFAULT_PAGINATION_CLASS': 'main.paginations.CustomPagination',
    'PAGE_SIZE': 12
}

CORS_ORIGIN_ALLOW_ALL = False

CORS_ALLOWED_ORIGINS = [
    "http://127.0.0.1:8000",
    "http://127.0.0.1:3000",
    "http://localhost:3000",
]

CORS_TRUSTED_ORIGINS = [
    'http://localhost:3000',
]

views.py

class ChangePasswordView(generics.UpdateAPIView):
    queryset = User.objects.all()
    serializer_class = serializers.ChangePasswordSerializer

serializers.py

class ChangePasswordSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=True)
    password2 = serializers.CharField(write_only=True, required=True)
    old_password = serializers.CharField(write_only=True, required=True)

    class Meta:
        model = models.User
        fields = [ 'old_password', 'password', 'password2' ]

    def validate(self, attrs):
        if attrs['password'] != attrs['password2']:
            raise serializers.ValidationError({'password': 'Password fields did not match!'})
        return attrs

    def validate_old_password(self, value):
        user = self.context['request'].user
        if not user.check_password(value):
            raise serializers.ValidationError({'old_password': 'Old password provided is incorrect!'})
        return value

    def update(self, instance, validated_data):
        user = self.context['request'].user
        print(user)
        if user.pk != instance.pk:
            raise serializers.ValidationError({"authorize": "You don't have permission to change password!",})
        instance.set_password(validated_data['password'])
        instance.save()
        return instance

urls.py

...
    path('change_password/<int:pk>/', views.ChangePasswordView.as_view(), name='auth_change_password'),
...

前端代码

const link = `${BASE_URL}${CHANGE_PASSWORD_URL}${customer_id}`;
        const formData = new FormData();
        formData.append('old_password', oldPwdRef.current.value);
        formData.append('password', newPwdRef.current.value);
        formData.append('password2', confirmNewPwdRef.current.value);
        axios.put(link, formData, {
            headers: {
                'content-type': 'application/json',
            }}).then((response) => {
            console.log(`${TAG} response is ${JSON.stringify(response)}`);
        }).catch(error => {
            console.log(`${TAG} error: ${error}`);
        })

错误日志

Django控制台

Forbidden (Origin checking failed - http://localhost:3000 does not match any trusted origins.): /api/change_password/2
[26/Oct/2023 06:09:11] "PUT /api/change_password/2 HTTP/1.1" 403 2569

Chrome控制台

ChangePassword:  error: AxiosError: Request failed with status code 403

已尝试的方法

设置CORS_ORIGIN_ALLOW_ALL = True,问题依旧。

解决方案

1. 修正前端请求格式冲突

当前代码使用FormData却手动设置content-type: application/json,导致请求格式不匹配,后端无法正确解析数据,同时干扰CORS/CSRF验证逻辑。两种修正方式:

方式一:改用JSON格式发送数据

const link = `${BASE_URL}${CHANGE_PASSWORD_URL}${customer_id}`;
const data = {
  old_password: oldPwdRef.current.value,
  password: newPwdRef.current.value,
  password2: confirmNewPwdRef.current.value
};
axios.put(link, data, {
  headers: {
    'content-type': 'application/json',
  }
}).then((response) => {
  console.log(`${TAG} response is ${JSON.stringify(response)}`);
}).catch(error => {
  console.log(`${TAG} error: ${error}`);
})

方式二:保留FormData,移除手动content-type设置

让浏览器自动生成multipart/form-data类型的请求头:

const link = `${BASE_URL}${CHANGE_PASSWORD_URL}${customer_id}`;
const formData = new FormData();
formData.append('old_password', oldPwdRef.current.value);
formData.append('password', newPwdRef.current.value);
formData.append('password2', confirmNewPwdRef.current.value);
axios.put(link, formData).then((response) => {
  console.log(`${TAG} response is ${JSON.stringify(response)}`);
}).catch(error => {
  console.log(`${TAG} error: ${error}`);
})

2. 完善Django CORS配置

开启凭证支持,并补充完整的可信源地址:

# settings.py
CORS_ALLOW_CREDENTIALS = True

CORS_TRUSTED_ORIGINS = [
    'http://localhost:3000',
    'http://127.0.0.1:3000',
]

3. 启用视图认证与权限

当前视图未配置认证类,导致request.user为匿名用户,触发序列化器中的权限验证异常,同时影响CSRF验证。取消settings.py中相关注释,启用JWT认证和登录权限:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PAGINATION_CLASS': 'main.paginations.CustomPagination',
    'PAGE_SIZE': 12
}

同时前端请求需携带JWT Token:

axios.put(link, data, {
  headers: {
    'content-type': 'application/json',
    'Authorization': `Bearer ${your_jwt_token}`
  }
})

4. 适配CSRF验证(若使用Session认证)

如果采用Session认证而非JWT,需确保前端携带CSRF Token:

// 从cookie中获取CSRF Token
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

const csrftoken = getCookie('csrftoken');

// 发送请求时携带CSRF Token
axios.put(link, data, {
  headers: {
    'X-CSRFToken': csrftoken,
    'content-type': 'application/json',
  }
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 23:35:56