阶段 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/,本阶段将其接入前端 |
目录
- 完整项目结构
- 启动后端
- 启动前端
- 清理浏览器缓存数据
- [测试 admin:登录与用户管理](#测试 admin:登录与用户管理)
- [测试 zhangsan:登录与用户管理](#测试 zhangsan:登录与用户管理)
- [用 curl 验证后端权限拦截](#用 curl 验证后端权限拦截)
- [建议修复一:token 过期自动刷新](#建议修复一:token 过期自动刷新)
- 建议修复二:后端返回菜单路径
- 最终联调检查清单
- 阶段完整流程与完成标准
- 阶段小结
- 常见问题与排查
- 下一阶段衔接
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)可作为并行的后续优化方向,但不纳入当前闭环的必需环节。