1
0
forked from erp-dev/erp
Files
erpnew/docs/TROUBLESHOOT_401_PATCH.md
2025-11-19 14:08:05 +08:00

13 KiB
Raw Permalink Blame History

PlateOrder PATCH 请求返回 401 错误排查指南

📋 问题概述

前端反馈 PlateOrder 接口的 PATCH 请求返回 401 未授权错误。

🔍 可能的原因与解决方案

原因 1: JWT Token 未发送或格式错误

最常见原因! 前端请求时缺少 Authorization 头或格式不正确。

检查方法:

在浏览器开发者工具中:

  1. 打开 Network 标签页
  2. 找到失败的 PATCH 请求
  3. 查看 Request Headers

正确的格式:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

常见错误:

# 错误1: 缺少 Authorization 头
Content-Type: application/json

# 错误2: 使用了错误的前缀
Authorization: Token eyJhbGciOiJIUzI1NiIs...  ❌ 应该是 Bearer

# 错误3: Token 值错误
Authorization: Bearer undefined  ❌ 前端未正确读取 token

前端修复代码:

// Axios 示例
import axios from 'axios';

// 方法1: 每次请求都带 token
const token = localStorage.getItem('access_token');
axios.patch('/api/v1/plate-orders/123/', {
  urgency_level: '加急'
}, {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

// 方法2: 配置全局拦截器(推荐)
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

// 之后所有请求自动带 token
axios.patch('/api/v1/plate-orders/123/', {
  urgency_level: '加急'
});
// Fetch 示例
const token = localStorage.getItem('access_token');
fetch('/api/v1/plate-orders/123/', {
  method: 'PATCH',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    urgency_level: '加急'
  })
});

原因 2: JWT Token 已过期

Token 有效期为 7 天(根据 settings.py 配置),过期后需要刷新。

检查方法:

# 在后端查看 Token 是否有效
curl -X GET http://localhost:8000/api/v1/plate-orders/ \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

# 如果返回 401说明 Token 已过期

前端修复代码:

// 实现 Token 自动刷新
import axios from 'axios';

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 如果正在刷新,将请求加入队列
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers['Authorization'] = 'Bearer ' + token;
          return axios(originalRequest);
        }).catch(err => {
          return Promise.reject(err);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      const refreshToken = localStorage.getItem('refresh_token');

      try {
        const response = await axios.post('/api/token/refresh/', {
          refresh: refreshToken
        });

        const { access } = response.data;
        localStorage.setItem('access_token', access);

        originalRequest.headers['Authorization'] = 'Bearer ' + access;
        processQueue(null, access);

        return axios(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        // Token 刷新失败,跳转到登录页
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

原因 3: 用户缺少 change_plateorder 权限

用户可能有 view_plateorder 权限(能 GET但缺少 change_plateorder 权限(不能 PATCH/PUT

检查方法:

# 使用诊断脚本检查
cd /home/f/coding/flower
uv run python diagnose_permissions.py <username>

# 或在 Django shell 中检查
python manage.py shell
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> user = User.objects.get(username='前端用户名')
>>> user.has_perm('printing.change_plateorder')
False  # ❌ 如果返回 False说明缺少权限

后端修复方法:

方法1: 在 Django Admin 中添加

  1. 登录 Admin: http://localhost:8000/admin/
  2. 进入 用户 管理
  3. 找到目标用户/组
  4. 权限 区域勾选:
    • printing | plate order | Can change plate order
  5. 保存

方法2: 使用 Django Shell

python manage.py shell

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission

User = get_user_model()
user = User.objects.get(username='前端用户名')

# 添加单个权限
change_perm = Permission.objects.get(codename='change_plateorder')
user.user_permissions.add(change_perm)

# 或批量添加所有 PlateOrder 权限
perms = Permission.objects.filter(
    content_type__app_label='printing',
    content_type__model='plateorder'
)
user.user_permissions.add(*perms)

print("✅ 权限已添加!")

方法3: 通过用户组管理(推荐)

from django.contrib.auth.models import Group, Permission

# 创建或获取用户组
group, created = Group.objects.get_or_create(name='业务员')

# 为组添加权限
perms = Permission.objects.filter(
    content_type__app_label='printing',
    content_type__model='plateorder'
)
group.permissions.add(*perms)

# 将用户加入组
user.groups.add(group)

原因 4: CORS 问题导致预检请求失败

如果前端和后端在不同域名PATCH 请求会先发送 OPTIONS 预检请求。

检查方法:

在 Network 标签页中查看是否有:

  1. 一个 OPTIONS 请求(预检)
  2. 紧随其后的 PATCH 请求

如果 OPTIONS 请求失败PATCH 不会发送。

后端修复方法:

检查 settings.py 中的 CORS 配置:

# flower/settings.py

# 确保已安装 django-cors-headers
INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 放在最前面
    'django.middleware.common.CommonMiddleware',
    ...
]

# 开发环境可以允许所有域名
CORS_ALLOW_ALL_ORIGINS = True

# 生产环境应该指定具体域名
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://localhost:8080",
    "https://your-frontend-domain.com",
]

# 允许携带认证信息
CORS_ALLOW_CREDENTIALS = True

# 允许的 HTTP 方法
CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
]

# 允许的请求头
CORS_ALLOW_HEADERS = [
    'accept',
    'accept-encoding',
    'authorization',
    'content-type',
    'dnt',
    'origin',
    'user-agent',
    'x-csrftoken',
    'x-requested-with',
]

原因 5: 前端发送了错误的 Content-Type

某些情况下Content-Type 不匹配会导致认证失败。

检查方法:

Request Headers 中应该有:

Content-Type: application/json

常见错误:

Content-Type: text/plain
Content-Type: application/x-www-form-urlencoded

前端修复:

// Axios 会自动设置,但可以强制指定
axios.patch('/api/v1/plate-orders/123/', data, {
  headers: {
    'Content-Type': 'application/json'
  }
});

// Fetch 必须手动设置
fetch('/api/v1/plate-orders/123/', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify(data)
});

🛠️ 完整的前端解决方案

推荐的 Axios 配置React/Vue

// api.js
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:8000/api/v1',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器:自动添加 Token
api.interceptors.request.use(
  config => {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

// 响应拦截器:处理 401 自动刷新 Token
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return api(originalRequest);
        }).catch(err => Promise.reject(err));
      }

      originalRequest._retry = true;
      isRefreshing = true;

      const refreshToken = localStorage.getItem('refresh_token');
      if (!refreshToken) {
        window.location.href = '/login';
        return Promise.reject(error);
      }

      try {
        const response = await axios.post(
          'http://localhost:8000/api/token/refresh/',
          { refresh: refreshToken }
        );

        const { access } = response.data;
        localStorage.setItem('access_token', access);
        processQueue(null, access);

        originalRequest.headers.Authorization = `Bearer ${access}`;
        return api(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.clear();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

export default api;

// 使用示例
// import api from './api';
// api.patch('/plate-orders/123/', { urgency_level: '加急' });

🔬 诊断工具

1. 后端权限诊断

cd /home/f/coding/flower
uv run python diagnose_permissions.py <username>

2. 手动测试 API

# 获取 Token登录
curl -X POST http://localhost:8000/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username": "your_username", "password": "your_password"}'

# 使用 Token 测试 PATCH
curl -X PATCH http://localhost:8000/api/v1/plate-orders/1/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urgency_level": "加急"}'

3. 前端调试代码

// 在控制台运行,检查 Token 是否存在
console.log('Access Token:', localStorage.getItem('access_token'));
console.log('Refresh Token:', localStorage.getItem('refresh_token'));

// 手动测试 PATCH 请求
fetch('/api/v1/plate-orders/1/', {
  method: 'PATCH',
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ urgency_level: '加急' })
})
.then(res => res.json())
.then(data => console.log('Success:', data))
.catch(err => console.error('Error:', err));

📊 错误代码对照表

状态码 含义 可能原因
401 Unauthorized Token 缺失、格式错误、已过期
403 Forbidden Token 有效但用户缺少权限
400 Bad Request 请求数据格式错误
405 Method Not Allowed 接口不支持该 HTTP 方法

检查清单

在联系后端开发人员之前,前端请确认:

  • 请求头包含 Authorization: Bearer <token>
  • Token 格式正确(不是 undefinednull
  • Content-Type 设置为 application/json
  • 请求 URL 正确(末尾有 /
  • Token 未过期(可以用同样的 Token 测试 GET 请求)
  • 浏览器控制台无 CORS 错误
  • Network 标签显示请求实际发送了

如果以上都确认无误但仍然 401请提供

  1. Network 标签中完整的 Request Headers
  2. 完整的 Response包括状态码和响应体
  3. 用户名(用于后端检查权限配置)

📞 获取帮助

如果问题仍未解决:

  1. 运行诊断脚本并提供输出
  2. 提供前端的完整错误信息
  3. 提供用户名供后端检查权限
  4. 提供 Network 标签的截图

最后更新: 2025-11-19