Django 后端实战指南_9:前后端完整联调和问题修复

阶段 9:前后端完整联调和问题修复

合辑 :Django 系统阶段开发手册 | 阶段 :9 / 18 | 定位 :将前后端放在一起完整联调,逐项验证权限效果并集中修复常见问题 | 建议学时:6--8 小时

大家好,我是小马不起床。


阶段导语

阶段 5 与阶段 6 建成的后端登录与接口权限拦截,阶段 7 与阶段 8 建成的前端登录与权限渲染,到本阶段被放在同一环境中完整联调。本阶段同时启动 Django 后端与 Vue3 前端,逐一验证登录、token 携带、RBAC 权限拦截的实际效果,并针对 CORS、404、401、403 等高频问题进行集中排查与修复。本阶段是前后端最小闭环的收官验证。

本阶段目标:

text 复制代码
1. 同时启动 Django 后端和 Vue3 前端
2. 测试登录流程
3. 测试 JWT 是否正常携带
4. 测试 RBAC 权限是否生效
5. 修复常见问题
6. 让 admin 和 zhangsan 的权限效果完全跑通

联调通过后的预期效果:

text 复制代码
admin:
- 可以登录
- 可以查看用户列表
- 可以新增用户
- 可以删除用户

zhangsan:
- 可以登录
- 可以查看用户列表
- 看不到新增按钮
- 看不到删除按钮
- 手动请求新增 / 删除接口也会被后端拒绝

学习目标

完成本阶段后,应能够:

  • 同时启动后端与前端,并在清理浏览器残留数据后进入联调状态
  • 以 admin 与 zhangsan 两个账号验证菜单、按钮与接口的权限效果
  • 使用 curl 手动发起请求,验证后端对无权限操作的 403 拦截
  • 优化 request.js,实现 access_token 过期时自动刷新并重放失败请求
  • 按检查清单逐项核对后端接口与前端页面的联调结果
  • 依据问题现象定位 CORS、404、401、403 等常见故障的原因并处理

前置知识

本阶段直接依赖阶段 8(阶段 8:Vue3 根据权限显示菜单和按钮.md)及其之前的前后端能力:

前置条目 具体要求
后端登录与拦截 阶段 5 提供 JWT 登录,阶段 6 提供按 permission_code 的接口权限拦截
前端登录 阶段 7 完成登录页、请求封装与状态存储
前端权限渲染 阶段 8 完成菜单、按钮的按权限渲染与用户管理接口对接
token 刷新接口 阶段 5 已提供 /api/token/refresh/,本阶段将其接入前端

目录

  1. 完整项目结构
  2. 启动后端
  3. 启动前端
  4. 清理浏览器缓存数据
  5. [测试 admin:登录与用户管理](#测试 admin:登录与用户管理)
  6. [测试 zhangsan:登录与用户管理](#测试 zhangsan:登录与用户管理)
  7. [用 curl 验证后端权限拦截](#用 curl 验证后端权限拦截)
  8. [建议修复一:token 过期自动刷新](#建议修复一:token 过期自动刷新)
  9. 建议修复二:后端返回菜单路径
  10. 最终联调检查清单
  11. 阶段完整流程与完成标准
  12. 阶段小结
  13. 常见问题与排查
  14. 下一阶段衔接

1. 完整项目结构

联调前确认项目结构完整:

text 复制代码
rbac_project/
├── backend/
│   ├── manage.py
│   ├── config/
│   │   ├── settings.py
│   │   └── urls.py
│   └── rbac/
│       ├── models.py
│       ├── services.py
│       ├── jwt_utils.py
│       ├── auth.py
│       ├── responses.py
│       ├── views.py
│       ├── urls.py
│       └── management/
│           └── commands/
│               ├── init_rbac_data.py
│               └── check_permission.py
└── frontend/
    └── src/
        ├── api/
        │   ├── auth.js
        │   └── user.js
        ├── router/
        │   └── index.js
        ├── stores/
        │   └── auth.js
        ├── utils/
        │   └── request.js
        ├── views/
        │   ├── Login.vue
        │   ├── Home.vue
        │   ├── UserList.vue
        │   └── Forbidden.vue
        ├── App.vue
        └── main.js

要点

  • 后端由 config 与 rbac 两部分构成,rbac 含模型、服务、JWT 工具、视图、路由与管理命令。
  • 前端由 api、router、stores、utils、views 五个目录组织。
  • 结构缺失会直接导致接口 404 或页面无法加载。

2. 启动后端

进入后端目录:

bash 复制代码
cd rbac_project/backend

启动虚拟环境。Windows:

bash 复制代码
venv\Scripts\activate

macOS / Linux:

bash 复制代码
source venv/bin/activate

确认数据库迁移已完成:

bash 复制代码
python manage.py migrate

确认测试数据已初始化:

bash 复制代码
python manage.py init_rbac_data

启动后端:

bash 复制代码
python manage.py runserver

后端地址:

text 复制代码
http://127.0.0.1:8000

要点

  • 启动前需激活虚拟环境,保证依赖可被解释器找到。
  • migrate 与 init_rbac_data 是接口可正常返回数据的的前提。
  • 后端监听 8000 端口。

3. 启动前端

新开一个终端。进入前端目录:

bash 复制代码
cd rbac_project/frontend

启动前端:

bash 复制代码
npm run dev

前端地址:

text 复制代码
http://localhost:5173

要点

  • 前端需在后端之外的独立终端启动,两者并行运行。
  • 前端监听 5173 端口,与 CORS_ALLOWED_ORIGINS 保持一致。

4. 清理浏览器缓存数据

由于此前多次修改过 token、权限与接口,浏览器可能残留旧数据,联调前应先清理。打开浏览器控制台:

text 复制代码
F12

进入:

text 复制代码
Application
  ↓
Local Storage
  ↓
http://localhost:5173

删除以下条目:

text 复制代码
access_token
refresh_token
user
permissions

或在控制台直接执行:

javascript 复制代码
localStorage.clear()

随后刷新页面。

要点

  • 残留的旧 token 或旧 permissions 会干扰本次联调判断。
  • 清理后可通过 localStorage.clear() 一次性完成。
  • 清理后需重新登录以写入最新数据。

5. 测试 admin:登录与用户管理

5.1 登录

访问:

text 复制代码
http://localhost:5173/login

输入:

text 复制代码
admin
123456

登录成功后应跳转至:

text 复制代码
http://localhost:5173/

首页显示:

text 复制代码
用户名:admin
真实姓名:管理员
当前角色:管理员

权限列表应包含:

text 复制代码
user:list
user:add
user:delete
role:list

菜单应显示:

text 复制代码
用户管理
角色管理(后续阶段再做页面)

5.2 用户管理

点击:

text 复制代码
用户管理

进入:

text 复制代码
http://localhost:5173/users

admin 应看到:

text 复制代码
新增用户按钮
删除用户按钮
用户列表

点击新增用户,输入:

text 复制代码
用户名:lisi
密码:123456
真实姓名:李四

新增成功后,用户列表应出现:

text 复制代码
lisi

点击删除按钮可删除用户。需注意:

text 复制代码
admin 不能删除自己

删除 admin 自身时,后端返回:

text 复制代码
不能删除自己

该行为属于正常的保护逻辑。

要点

  • admin 拥有全部权限,菜单与按钮均可见。
  • 新增成功后列表刷新,新账号即时出现。
  • 后端对删除自身操作做了保护性拦截。

6. 测试 zhangsan:登录与用户管理

6.1 登录

先退出登录,再登录:

text 复制代码
zhangsan
123456

登录成功后首页显示:

text 复制代码
用户名:zhangsan
真实姓名:张三
当前角色:普通用户

权限列表应仅有:

text 复制代码
user:list

首页可见:

text 复制代码
用户管理

但不应看到:

text 复制代码
角色管理

6.2 用户管理

进入:

text 复制代码
http://localhost:5173/users

zhangsan 能看到用户列表,但看不到:

text 复制代码
新增用户按钮
删除用户按钮

页面显示类似:

text 复制代码
当前账号没有 user:add 权限,所以不显示新增按钮
无删除权限

这表明前端权限显示已生效。

要点

  • zhangsan 仅拥有 user:list,故只可见用户管理菜单。
  • 新增、删除按钮及角色管理菜单对 zhangsan 均不可见。
  • 前端可见性差异由后端下发的 permissions 决定。

7. 用 curl 验证后端权限拦截

前端隐藏按钮属于用户体验,真正的安全必须由后端保证。因此需验证 zhangsan 即使手动请求新增 / 删除接口也必须失败。

7.1 登录 zhangsan 获取 token

macOS / Linux:

bash 复制代码
curl -X POST http://127.0.0.1:8000/api/login/ \
  -H "Content-Type: application/json" \
  -d '{"username":"zhangsan","password":"123456"}'

Windows PowerShell:

bash 复制代码
curl -Method POST "http://127.0.0.1:8000/api/login/" `
  -Headers @{"Content-Type"="application/json"} `
  -Body '{"username":"zhangsan","password":"123456"}'

复制返回的:

text 复制代码
access_token

7.2 用 zhangsan token 新增用户

macOS / Linux:

bash 复制代码
curl -X POST http://127.0.0.1:8000/api/users/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer 你的zhangsan_access_token" \
  -d '{"username":"test001","password":"123456","real_name":"测试用户","phone":"13600000000","role_id":2,"status":1}'

正常应返回:

text 复制代码
{
  "code": 403,
  "message": "没有权限访问该接口,需要权限:user:add",
  "data": null
}

这表明后端权限拦截成功。

7.3 用 zhangsan token 删除用户

bash 复制代码
curl -X DELETE http://127.0.0.1:8000/api/users/3/ \
  -H "Authorization: Bearer 你的zhangsan_access_token"

正常应返回:

text 复制代码
{
  "code": 403,
  "message": "没有权限访问该接口,需要权限:user:delete",
  "data": null
}

这表明:

text 复制代码
前端隐藏按钮成功
后端接口拦截也成功

要点

  • 先获取 zhangsan 的 access_token,再以该 token 手动发起越权请求。
  • 新增与删除接口均应返回 403,并指明缺失的权限编码。
  • 后端拦截独立于前端可见性,是安全性的最终保障。

8. 建议修复一:token 过期自动刷新

阶段 5 已提供:

text 复制代码
/api/token/refresh/

而阶段 7 的前端在遇到 401 时会直接跳回登录页。此处将其优化为:

text 复制代码
access_token 过期
  ↓
自动用 refresh_token 换新的 access_token
  ↓
重新请求刚才失败的接口
  ↓
如果 refresh_token 也失效,再跳登录页

打开文件:

text 复制代码
frontend/src/utils/request.js

替换为完整代码:

javascript 复制代码
import axios from "axios"

const request = axios.create({
  baseURL: "http://127.0.0.1:8000/api",
  timeout: 10000
})

let isRefreshing = false
let waitingQueue = []

function clearLoginInfo() {
  localStorage.removeItem("access_token")
  localStorage.removeItem("refresh_token")
  localStorage.removeItem("user")
  localStorage.removeItem("permissions")
}

function retryWaitingQueue(newAccessToken) {
  waitingQueue.forEach(callback => {
    callback(newAccessToken)
  })

  waitingQueue = []
}

request.interceptors.request.use(
  config => {
    const accessToken = localStorage.getItem("access_token")

    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`
    }

    return config
  },
  error => {
    return Promise.reject(error)
  }
)

request.interceptors.response.use(
  response => {
    return response.data
  },
  async error => {
    const originalRequest = error.config

    if (!error.response) {
      return Promise.reject(error)
    }

    if (error.response.status !== 401) {
      return Promise.reject(error)
    }

    if (originalRequest.url === "/token/refresh/") {
      clearLoginInfo()
      window.location.href = "/login"
      return Promise.reject(error)
    }

    if (originalRequest._retry) {
      clearLoginInfo()
      window.location.href = "/login"
      return Promise.reject(error)
    }

    const refreshToken = localStorage.getItem("refresh_token")

    if (!refreshToken) {
      clearLoginInfo()
      window.location.href = "/login"
      return Promise.reject(error)
    }

    originalRequest._retry = true

    if (isRefreshing) {
      return new Promise(resolve => {
        waitingQueue.push(newAccessToken => {
          originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
          resolve(request(originalRequest))
        })
      })
    }

    isRefreshing = true

    try {
      const refreshResponse = await axios.post(
        "http://127.0.0.1:8000/api/token/refresh/",
        {
          refresh_token: refreshToken
        }
      )

      const res = refreshResponse.data

      if (res.code !== 200) {
        throw new Error(res.message || "刷新 token 失败")
      }

      const newAccessToken = res.data.access_token

      localStorage.setItem("access_token", newAccessToken)

      retryWaitingQueue(newAccessToken)

      originalRequest.headers.Authorization = `Bearer ${newAccessToken}`

      return request(originalRequest)
    } catch (refreshError) {
      clearLoginInfo()
      window.location.href = "/login"
      return Promise.reject(refreshError)
    } finally {
      isRefreshing = false
    }
  }
)

export default request

该修复完成后:

text 复制代码
access_token 过期时不会立刻退出登录
会先自动刷新 token

要点

  • isRefreshing 与 waitingQueue 配合,保证刷新期间仅发起一次刷新请求。
  • 刷新成功后,排队请求携带新 token 重放,原请求也重发。
  • 对刷新接口本身的 401、以及重试过一次的请求再次 401,均直接清理并跳登录页。

9. 建议修复二:后端返回菜单路径

当前前端依据 permission_code 写死菜单:

text 复制代码
有 user:list 显示用户管理
有 role:list 显示角色管理

本阶段可先不改动。若需由后端返回菜单,可在登录接口返回:

text 复制代码
menu_path
permission_name
permission_code

目前已具备:

text 复制代码
sys_permission.menu_path

后续若要实现动态菜单,可进入新阶段:

text 复制代码
阶段 10:后端返回菜单,Vue3 动态生成菜单

本阶段暂不展开,避免一次引入过多内容。

要点

  • 当前菜单为前端依据权限编码硬编码,未消费后端的 menu_path。
  • sys_permission.menu_path 字段已存在,为后续动态菜单预留数据来源。
  • 动态菜单留作后续阶段,本阶段保持范围收敛。

10. 最终联调检查清单

10.1 后端检查

text 复制代码
1. python manage.py runserver 能启动
2. /api/login/ 能登录
3. /api/me/ 不带 token 返回 401
4. /api/users/ 不带 token 返回 401
5. admin token 请求 /api/users/ 成功
6. zhangsan token 请求 /api/users/ 成功
7. zhangsan token 请求 POST /api/users/ 返回 403
8. zhangsan token 请求 DELETE /api/users/3/ 返回 403

10.2 前端检查

text 复制代码
1. npm run dev 能启动
2. /login 能打开
3. admin 能登录
4. zhangsan 能登录
5. 登录后 localStorage 有 token
6. admin 首页显示用户管理和角色管理
7. zhangsan 首页只显示用户管理
8. admin 用户管理页有新增和删除按钮
9. zhangsan 用户管理页没有新增和删除按钮
10. 退出登录后不能访问 /users

要点

  • 后端检查覆盖鉴权(无 token 401)与授权(越权 403)两条线。
  • 前端检查覆盖启动、登录、状态存储、菜单与按钮渲染及退出拦截。
  • 两份清单逐项通过,即代表前后端最小闭环打通。

11. 阶段完整流程与完成标准

11.1 阶段完整流程

到阶段 9,系统已跑通:

text 复制代码
Vue3 登录页面
  ↓
输入账号密码
  ↓
请求 Django /api/login/
  ↓
Django 查询 MySQL 的 sys_user
  ↓
校验密码
  ↓
生成 JWT
  ↓
返回 access_token、refresh_token、用户信息、权限列表
  ↓
Vue3 保存 token 和 permissions
  ↓
Vue3 根据 permissions 显示菜单
  ↓
Vue3 根据 permissions 显示按钮
  ↓
Vue3 请求接口时自动携带 access_token
  ↓
Django 解析 JWT
  ↓
Django 获取当前用户
  ↓
Django 根据用户角色查询权限
  ↓
Django 判断接口需要的 permission_code
  ↓
有权限,执行接口
  ↓
没权限,返回 403

11.2 阶段完成标准

确认以下 4 个效果,即说明完整项目已跑通:

text 复制代码
admin 登录:
可以查看用户列表
可以新增用户
可以删除用户

zhangsan 登录:
可以查看用户列表
不能新增用户
不能删除用户

至此,当前版本的:

text 复制代码
Django + MySQL + JWT + RBAC + Vue3

已完成最小闭环。

要点

  • 完整流程串联前端渲染与后端 JWT 解析、RBAC 校验。
  • 完成标准以两个账号的四项可观测效果为准。
  • 最小闭环意味着后续阶段可在此之上叠加业务表与业务接口。

阶段小结

环节 产出 关键点
双端启动 后端 8000 与前端 5173 并行运行 先 migrate、init_rbac_data
缓存清理 移除旧 token 与 permissions localStorage.clear()
admin 测试 菜单、按钮全可见,新增/删除成功 不能删除自身为保护逻辑
zhangsan 测试 仅可见列表,越权按钮隐藏 由 permissions 决定
curl 验证 越权请求返回 403 后端为安全最终关口
token 自动刷新 过期先刷新并重放请求 见第 8 章
检查清单 后端 8 项、前端 10 项逐项通过 最小闭环达成

常见问题与排查

本阶段为问题修复专题,联调中的高频问题汇总如下,逐项给出定位与处理。

序号 现象 原因 处理
1 前端登录时报 CORS 错误 后端跨域未正确配置或改后未重启 核对 settings.py 三处配置并重启后端(见问题 1)
2 登录接口 404 接口地址与后端路由不一致 核对前端地址与 config/urls.py、rbac/urls.py(见问题 2)
3 登录提示用户名或密码错误 初始化数据未执行或密码未正确加密 执行 init_rbac_data,在 shell 校验并重设密码(见问题 3)
4 请求用户列表返回 401 未登录或 token 未正确携带 检查 localStorage 与请求头,核对 request.js 拦截器(见问题 4)
5 zhangsan 能看到新增按钮 后端初始化数据分配错了权限 重新初始化,清缓存后重登(见问题 5)
6 zhangsan 看不到按钮但接口仍能新增 后端权限装饰器缺失 为接口补 @permission_required(见问题 6)
7 admin 新增用户提示角色不存在 前端默认 role_id 与数据库实际不符 核对 sys_role 表并调整前端 role_id(见问题 7)

问题 1:前端登录时报 CORS 错误

错误通常表现为:

text 复制代码
Access to XMLHttpRequest has been blocked by CORS policy

检查后端:

text 复制代码
backend/config/settings.py

确认 INSTALLED_APPS 含 corsheaders:

python 复制代码
INSTALLED_APPS = [
    ...
    "corsheaders",
    ...
]

确认中间件列表最上方为:

python 复制代码
MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    ...
]

确认文件末尾为:

python 复制代码
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]

改完后必须重启后端:

bash 复制代码
python manage.py runserver

问题 2:登录接口 404

若前端提示:

text 复制代码
404 Not Found

说明接口地址不对。确认前端请求地址为:

text 复制代码
http://127.0.0.1:8000/api/login/

确认后端文件:

text 复制代码
backend/config/urls.py

其中包含:

python 复制代码
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("rbac.urls")),
]

确认:

text 复制代码
backend/rbac/urls.py

其中包含:

python 复制代码
path("login/", LoginView.as_view()),

最终地址才为:

text 复制代码
/api/login/

问题 3:登录提示用户名或密码错误

先确认初始化数据已执行:

bash 复制代码
python manage.py init_rbac_data

随后确认数据库中存在用户:

bash 复制代码
python manage.py shell

输入:

python 复制代码
from rbac.models import SysUser

SysUser.objects.values("username", "real_name", "status")

应能看到:

text 复制代码
admin
zhangsan

再测试密码是否正确:

python 复制代码
user = SysUser.objects.get(username="admin")
user.check_password("123456")

应返回:

text 复制代码
True

若返回 False,说明密码未正确加密,可重新初始化或在 shell 内手动修改:

python 复制代码
user.set_password("123456")
user.save()

问题 4:请求用户列表返回 401

401 表示:

text 复制代码
没有登录,或者 token 不正确

检查浏览器 Local Storage 中是否存在:

text 复制代码
access_token

再检查请求头中是否存在:

text 复制代码
Authorization: Bearer xxx

在浏览器控制台 Network 中点开 /api/users/ 请求,查看 Request Headers,应含:

text 复制代码
Authorization: Bearer access_token

若缺失,重点检查:

text 复制代码
frontend/src/utils/request.js

其中是否有:

javascript 复制代码
config.headers.Authorization = `Bearer ${accessToken}`

问题 5:zhangsan 能看到新增按钮

这说明前端权限判断有问题。检查登录后 Local Storage 中的:

text 复制代码
permissions

zhangsan 应仅有:

text 复制代码
["user:list"]

若 zhangsan 拥有:

text 复制代码
user:add
user:delete

说明后端初始化数据分配错了权限。重新执行:

bash 复制代码
python manage.py init_rbac_data

随后退出登录、清空 localStorage 并重新登录。

问题 6:zhangsan 看不到按钮,但接口还能新增

这说明前端正常,后端权限拦截有问题。重点检查:

text 复制代码
backend/rbac/views.py

新增用户接口须含:

python 复制代码
@permission_required("user:add")
def post(self, request):

删除用户接口须含:

python 复制代码
@permission_required("user:delete")
def delete(self, request, user_id):

若缺失这两个装饰器,后端不会拦截。

问题 7:admin 新增用户时提示角色不存在

前端新增用户默认传:

text 复制代码
role_id: 2

该默认值假定:

text 复制代码
普通用户角色 id = 2

若数据库中的普通用户角色 ID 不为 2,则会失败。进入 MySQL 查看:

text 复制代码
SELECT * FROM sys_role;

确认普通用户的 id 值。若不为 2,则将:

text 复制代码
frontend/src/views/UserList.vue

中的:

text 复制代码
role_id: 2

改为实际的普通用户角色 ID。更完善的做法是后续提供角色下拉框接口,本阶段先使用固定值。

要点

  • CORS、404、401 多因配置或地址不一致,先核对配置再重启。
  • 密码错误先查初始化数据与 check_password 结果。
  • 按钮显示异常区分前端数据问题与后端拦截器缺失。
  • role_id 硬编码依赖数据库实际角色 ID,需按库核对。

下一阶段衔接

阶段 9 完成了前后端的完整联调与问题修复,Django + MySQL + JWT + RBAC + Vue3 的最小闭环已达成。阶段 10(阶段 10:用户扩展档案信息表.md)将在稳定的鉴权与授权基座上转向业务扩展,新增用户扩展档案信息表。本阶段修复并确认的登录态与权限渲染,正是阶段 10 扩展用户档案字段的依赖前提。此外,第 9 章提及的动态菜单(后端返回 menu_path)可作为并行的后续优化方向,但不纳入当前闭环的必需环节。

相关推荐
java资料站1 小时前
十四、Spring AI Alibaba · 案例 · 基于 HumanInTheLoop 的内容审核系统
java·spring·状态模式
pride.li1 小时前
第12章 Python 教程-流程控制:循环结构
linux·服务器·python·算法
ycydynq1 小时前
django用Bootstrap 模态框 在同一页面实现增删改查
django·bootstrap·jquery
鬼手点金2 小时前
模型即 Agent:当模型本身成为产品
开发语言·前端·人工智能·python·机器学习
田里的水稻2 小时前
PyTorch-Dataset\DataLoader数据加载
人工智能·pytorch·python
卷无止境2 小时前
Python 3.14 自由线程:拆掉 GIL 之后,我们到底得到了什么
后端·python
染指11102 小时前
147.Agent-LangGraph归并函数
开发语言·人工智能·python·langchain·agents
小白学大数据2 小时前
逆向分析短视频平台 a_bogus 参数:从 JavaScript 混淆到 Python 复现
javascript·python·音视频
外收内放2 小时前
数据分析(数据清洗)
python·学习·数据分析