
去年接到一个挺磨人的需求学校网络管理中心的老认证系统要重写了。旧系统是十年前用 PHP 写的登录、计费、角色切换逻辑全堆在一个控制器里换一个管理员密码都恨不得翻半个小时的代码日常运维的人心态早就崩了。折腾了两个月我和另一位同事用 Python Vue 重写了一套校园网络认证与访问控制系统后端主体跑在 Django 上几个独立小服务用 Flask 扛开发环境统一在 PyCharm 里完成。这套系统上线之后接手维护的同事反馈最多的一句话是“原来代码还能写成这样”。这篇文章不打算按教科书讲我把整个项目从选型、建工程、写认证、做访问控制到前端联调和部署踩坑的顺序捋一遍。适合正在做校园项目、毕业设计或者第一次用 Python Vue 写带权限管理的 Web 系统的朋友。你会发现大部分难点不在“登录怎么做”而在“放行谁、拦截谁、怎么方便地调整策略”。1. 校园网认证系统难点不在“认证”而在“访问控制”1.1 需求拆解一个认证系统要覆盖哪些人、哪些设备、哪些场景校园网络认证系统的核心使用场景说起来不复杂学生和老师要连入校园网系统判断是不是合法账号连进去之后不同的人能访问的资源不一样。比如学生账号只能访问教学系统和图书馆资源教师账号可以访问教务后台网络管理员还需要能做账号冻结、流量审计和策略下发。把这些场景翻译成技术需求就是三件事身份认证解决“你是谁”会话管理解决“你登录后我怎么记住你”访问控制解决“你进来之后能碰什么”。很多项目团队做的时候把大量精力放在登录页好不好看、验证码好不好用上结果上线之后才发现权限关系是一团乱麻改起来比重新做还麻烦。在设计这套系统时我先把角色权限关系梳理成了一张矩阵表格角色、可访问模块、可执行操作、可管理数据范围。这张表是整个系统后面所有逻辑的依据后面做后端中间件、前端路由守卫、菜单显隐全都直接对着它来。1.2 旧系统为什么必须重写旧系统最大的问题不是功能少而是没有分层。登录逻辑、数据库操作、页面渲染全部掺在一起管理员想给某个临时访客开通一天的上网权限得直接改数据库表。而且认证方式还停留在简单的用户名密码明文传输没有统一会话失效机制离职员工的账号经常处于“永远有效”的状态。重写之后系统按前后端分离架构重新设计。前端用 Vue 做单页应用负责登录页、管理后台、个人自助服务台的交互后端用 Django 做主要业务接口和权限控制独立的小工具服务用 Flask 写比如自助修改密码的服务、定时同步人事系统数据的脚本服务。所有开发在 PyCharm 里完成环境统一后团队协作的沟通成本明显降低了。1.3 为什么选 Python Vue 这个组合选择 Python 不只是因为熟练更重要的是整个校园网运维团队日常处理大量脚本任务比如解析交换机日志、统计在线用户数、对接防病毒系统这些都是 Python 的强项。认证系统只是整个网络运维体系里的一环用 Python 写后端能方便后续把其他运维功能吸收进来避免“系统之间互相看不懂”的尴尬。Vue 这边则是因为生态成熟、上手快而且社区里现成的后台管理系统模板很多能直接把精力集中在认证和权限逻辑上不用从零抠 UI 组件。这套组合对于校园项目的团队构成来说非常合适后端人员可以完全不管前端细节前端人员只要按照接口文档对接就行。2. 双后端并存Django 做主体、Flask 做旁路服务的选择逻辑2.1 Django 自带 Auth 体系这是选它当主后端的主要原因项目标题里同时出现了 django 和 flask很多朋友以为这是二选一的关系。实际上在我这个项目里两者是明确分工的。主后端我选了 Django核心原因就是它自带一套完整的认证和权限体系。Django 内置的django.contrib.auth提供了 User 模型、Group 模型、Permission 模型还有基于 session 的认证机制和后台 admin 管理界面。我们项目里最核心的“角色-权限-用户”关系Django 几乎开箱即用。另外 Django 的 ORM 在处理复杂查询时非常省心。例如要查“最近七天登录次数超过三次、且属于教师分组的所有用户”直接用 ORM 的链式查询就能搞定不用手写复杂的关联 SQL。对于校园网这种数据量不大、但查询条件经常变化的场景Django ORM 的灵活性非常合适。2.2 Flask 在项目里负责什么Flask 在这个项目里负责的是几个 Django 不太愿意“加班”的场景。第一个是临时性小服务。比如校园网偶尔要出一个“临时开放端口”的功能需要快速写一个页面让管理员提交申请、写一个接口让交换机自动执行命令。这种功能用 Django 做会显得重要建 app、配路由、写迁移文件而以 Flask 写一个单文件脚本就能跑起来部署也很轻。第二个是定时任务和事件回调。我们有一个联动服务负责对接人事系统的数据同步人事数据库一更新自动触发认证系统的用户信息同步。这个服务用 Flask 写成一个常驻进程只需要暴露一个回调接口再把数据处理逻辑写在同一个文件里维护成本极低。2.3 选型对比什么时候该用 Django什么时候该用 Flask我这里说得直接一点不绕弯子维度DjangoFlask用户与权限模块自带认证、分组、权限二次开发快需要自己写或者集成第三方库ORM 与数据库迁移内置 migrations改模型后一条命令同步需要单独配 SQLAlchemy 和迁移工具Admin 后台开箱即用适合快速搭管理界面需要扩展 flask-admin自由度和体积结构固定适合项目协作灵活度高适合小服务学习曲线概念多但体系完整简单但要自己拼装组件本项目中的位置认证、权限、业务接口主后端自助修改密码、数据同步、回调服务如果只是做一个轻量 API 或脚本服务甚至只是一个工具页用 Flask 就足够了。但一个需要长期维护、多人协作、权限关系明确的认证与访问控制系统老老实实用 Django 当主后端长期看能省下非常多的开发时间。2.4 让我确定框架分工的一个关键事件项目启动第二周我们接到一个需求后勤部门要暂时给一部分外来施工人员开通校园网络访问权限并且要限制他们最多只能访问特定几个业务系统。如果用 Flask 从零设计这套权限模型至少要多花两天时间。而 Django 本身就提供了 permission 和 group 的管理方式我只需要创建一个“临时施工人员”组把几个目标系统的访问权限挂到这个组下再把施工人员账号分到这个组里就算完事。从那一刻起“Django 当主力、Flask 做辅助”这个分工就没有再动摇过。后面所有涉及用户、角色、权限的改动都进 Django所有独立小工具都扔给 Flask。3. PyCharm 里初始化 Django 工程这几步配置决定后续联调是否顺利3.1 虚拟环境与解释器配置别图省事直接用全局环境用 PyCharm 创建 Django 项目的第一步是配置好独立的虚拟环境。很多新手习惯直接选择全局 Python 解释器结果后面一堆依赖版本冲突。我这里用的方式是在 PyCharm 里新建项目时选择VirtualenvPython 版本选 3.10 或 3.11然后等待 PyCharm 自动装好 venv。装好之后用 PyCharm 的终端运行python -m pip install django djangorestframework django-cors-headers pymysql这样做的好处非常明显项目换台电脑拉到本地只需要pip freeze requirements.txt到新机器上pip install -r requirements.txt就能恢复整个环境。我们项目组里有人用 Windows、有人用 macOS这套流程没有出过一次环境不一致的问题。3.2 创建工程和应用时的目录规划很多个人项目一开始只有一个models.py、一个views.py全塞在里面。但这类校园项目后续一定会加功能我建议在创建时就按功能模块拆分 app。我的目录结构大概是这样的network_auth/ ├── config/ # 工程配置目录settings、urls、wsgi ├── apps/ │ ├── accounts/ # 用户注册、登录、认证相关 │ ├── roles/ # 角色、权限、分组管理 │ ├── network/ # 网络设备对接、在线用户管理 │ └── audit/ # 登录日志、操作审计 ├── static/ ├── templates/ └── frontend/ # 前端 Vue 项目在 PyCharm 里依次执行django-admin startproject config . python manage.py startapp accounts python manage.py startapp roles创建完应用之后记得在settings.py的INSTALLED_APPS里注册。这一步是新手最容易漏的经常有人说“我明明建了模型但 migrate 不生效”十有八九是忘了注册 app。3.3 自定义用户模型一个必须一开始就做的决定Django 默认的 User 模型字段只包含用户名、密码、邮箱、姓名等基础信息。但校园网认证系统需要学号、工号、所属院系、账号状态、有效期这些字段直接用默认模型根本无法满足。这里有个很重要的教训自定义 User 模型必须在第一次 migrate 之前就配置好。Django 一旦执行了初始迁移再想切换 User 模型会非常麻烦需要删库重来。我建议在accounts/models.py里继承AbstractUser扩展字段from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): student_id models.CharField(max_length32, blankTrue, nullTrue, uniqueTrue) employee_id models.CharField(max_length32, blankTrue, nullTrue, uniqueTrue) department models.CharField(max_length64, blankTrue) account_status models.CharField(max_length16, defaultactive) expiry_date models.DateTimeField(nullTrue, blankTrue) class Meta: db_table auth_user然后在settings.py里指定AUTH_USER_MODEL accounts.User这一步做完之后后续所有关联外键都用这个自定义 User不用回头去改。3.4 本机调试时CORS 和 Host 配置必须提前处理前后端分离开发时前端 Vue 默认跑在http://localhost:8080后端 Django 跑在http://localhost:8000两者端口不同必然出现跨域问题。解决方式是在 Django 里安装并配置django-cors-headersINSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]同时把settings.py里的ALLOWED_HOSTS配成ALLOWED_HOSTS [*]这只建议在开发阶段用等部署到生产环境必须收紧成具体域名避免不必要的安全风险。4. 认证模块实战MTV 模式下打通注册、登录、注销4.1 理解 Django 的 MTV 模式和教科书 MVC 的差异每次给团队里新同事讲 Django都要解释一遍 MTV。MVC 是 Model-View-Controller而 Django 的 MTV 是 Model-Template-View。对应关系是Model 负责数据Template 负责渲染View 负责业务逻辑和路由处理。Django 里没有传统意义的 Controller因为 URL 路由和 View 一起承担了控制器的职责。在我们这个系统里前端是 Vue 单页应用后端用 Django 只提供 JSON 接口Template 用得少但理解 MTV 依然重要尤其是“视图里不要写模板逻辑”“模板里不要放业务判断”这种边界感直接决定了代码后面能不能维护。4.2 注册接口与用户序列化注册接口的核心工作是创建用户、设置密码、分配默认角色。Django 中创建用户一定要用create_user方法这个方法会自动对密码进行哈希加密绝对不能用objects.create直接塞明文密码。我用的是 Django REST Framework 的ModelSerializer处理注册数据from rest_framework import serializers from .models import User class RegisterSerializer(serializers.ModelSerializer): password serializers.CharField(write_onlyTrue, min_length8) class Meta: model User fields [username, password, student_id, department] def create(self, validated_data): user User.objects.create_user( usernamevalidated_data[username], passwordvalidated_data[password], student_idvalidated_data.get(student_id, ), departmentvalidated_data.get(department, ), ) # 默认分配到“普通学生”角色组 default_group, _ Group.objects.get_or_create(namestudent) user.groups.add(default_group) return user这里有个细节把默认角色分配放在注册逻辑里而不是让前端传参。否则任何人都可以把自己注册成管理员。4.3 登录逻辑的三个关键细节登录接口表面简单实际上需要注意的点非常多。我这里只挑三个对项目影响最大的说。第一个是密码验证。我用 Django 的authenticate方法它会自动处理哈希比对from django.contrib.auth import authenticate, login def login_view(request): username request.data.get(username) password request.data.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) return JsonResponse({code: 0, data: {username: user.username, role: list(user.groups.values_list(name, flatTrue))}}) return JsonResponse({code: 1, message: 用户名或密码错误})第二个是登录状态保存方式。考虑到校园网系统主要面向浏览器访问我没有使用复杂的 JWT 方案而是直接使用 Django 的 session 机制。服务器端保存会话客户端保存 sessionid Cookie。这样注销时直接清掉服务端 session控制力更强也避免了 JWT 过期时间不好控制的麻烦。第三个是登录成功后的返回信息。一定要返回用户的角色列表和权限标识前端要根据这些信息决定登录后跳转哪个页面、菜单显示哪些项。如果登录接口只返回一个 “success”前端没法区分普通学生和管理员后面全是麻烦。4.4 日常操作中的查询与删除对象几个容易踩的坑标题相关热词里有“django执行查询-删除对象”这块确实很典型。我在项目里遇见的坑主要有三个第一是get方法查不到对象会抛异常。代码不能假设数据一定存在尤其是关联查询的时候。更安全的写法是from django.shortcuts import get_object_or_404 user get_object_or_404(User, usernameusername)第二是删除对象时要注意级联删除。比如删除一个角色组组下的用户关系怎么办Django 的默认行为是级联删除关联数据但这不一定是你想要的。需要在模型的ForeignKey字段上显式设置on_deletemodels.SET_NULL或on_deletemodels.PROTECT。第三是批量删除时尽量用objects.filter(...).delete()避免在 Python 层循环一条一条删性能差距在几千条数据时就很明显了。5. 访问控制关键RBAC 权限模型的落地细节5.1 什么是 RBAC以及为什么说“django rabc”是个拼写误区热词里有个 “django rabc”这其实是 RBAC 的常见拼写错误。RBAC 全称是 Role-Based Access Control基于角色的访问控制是权限管理领域最经典、也最适合校园网这种多角色场景的模型。RBAC 的核心思想是用户不直接关联权限而是关联角色角色再关联权限。这样做的好处是如果有一百个学生需要开通访问某系统的权限不需要给一百个账号分别配权限只要给“学生”这个角色加一个权限所有学生账号自动生效。校园网场景下典型的 RBAC 角色表大概是这样的角色权限范围student访问教学系统、图书馆资源、修改个人密码teacher学生权限 教务系统、成绩录入network_admin账号管理、角色管理、在线用户管理、设备配置operator查看日志、导出报表、临时账号开通5.2 在 Django 里如何落地 RBAC 关系Django 自带User、Group、Permission三个模型天然支持 RBAC 落地。Group就是角色Permission就是权限点User.groups建立了用户和角色的多对多关系。实际开发中我为每个业务动作创建对应的权限点例如python manage.py shell然后在代码中注册权限from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from .models import User content_type ContentType.objects.get_for_model(User) permission Permission.objects.create( codenamecan_freeze_account, name可以冻结账号, content_typecontent_type, )然后把这个权限挂到network_admin角色上from django.contrib.auth.models import Group admin_group Group.objects.get(namenetwork_admin) admin_group.permissions.add(permission)以后判断用户是否有权限只需要一行代码user.has_perm(accounts.can_freeze_account)这套机制全部是 Django 内置能力不用自己重复造轮子。5.3 后端接口鉴权中间件的实现界面上的菜单隐藏只是防君子后端接口必须做真正的权限拦截。我用 Django 中间件实现了一个简单的权限校验逻辑。思路是定义一个装饰器标注某个接口需要什么权限码在中间件或视图里统一检查当前用户的权限。这里我推荐用装饰器因为更直观from django.http import JsonResponse from functools import wraps def require_permission(codename): def decorator(func): wraps(func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return JsonResponse({code: 401, message: 请先登录}, status401) if not request.user.has_perm(codename): return JsonResponse({code: 403, message: 没有操作权限}, status403) return func(request, *args, **kwargs) return wrapper return decorator使用的时候require_permission(accounts.can_freeze_account) def freeze_user(request, user_id): ...这个方案的扩展性很好权限点变了只需要在装饰器里对应调整新增接口也不会影响其他接口。5.4 前端路由和按钮级权限控制前端 Vue 侧的权限控制主要分两层路由守卫控制页面级跳转自定义指令控制按钮级显隐。路由守卫可以这样做router.beforeEach((to, from, next) { const token localStorage.getItem(token) const roles JSON.parse(localStorage.getItem(roles) || []) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.roles !to.meta.roles.some(r roles.includes(r))) { next(/403) } else { next() } })这种方法把角色信息保存在前端 localStorage可以快速判断页面访问权限避免每次跳转都请求后端。后面我会提到更严格的方案是在后端再校验一次但前端先做拦截体验更好。6. Vue 前端接入的实操细节与常见报错6.1 Vue 环境安装与依赖版本安装依赖时的关心点Vue 项目的创建建议直接用官方脚手架。在 PyCharm 自带的终端里执行npm create vuelatest或者如果要用 Vue 3 Vite 的标准模板npm create vitelatest frontend -- --template vue创建之后进入目录安装依赖cd frontend npm install这里需要提醒的是新版本脚手架默认安装的依赖版本很新有时候会和旧项目有兼容问题。遇到依赖冲突时不要盲目npm install到底可以查看package.json确认版本或者删掉node_modules和package-lock.json后重新安装。在一台干净的机器上装 Vue 环境时Node.js 版本建议保持 18 以上否则很多新库根本不支持。装完环境先跑一下默认模板确认网络没问题再接手项目代码。6.2 axios 封装、Token 保存与请求拦截器axios 是 Vue 项目里最常用的 HTTP 请求库。我习惯在src/utils/request.js里做一个统一封装把所有重复逻辑收口import axios from axios const request axios.create({ baseURL: http://localhost:8000/api, timeout: 10000, withCredentials: true }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request在这个项目里我们同时使用了 Django session 和自定义 token。withCredentials: true是为了让浏览器在跨域请求时带上 Cookie保证 Django session 能正常识别用户登录状态。6.3 路由守卫与登录状态判断前端的登录状态判断是很多同学容易写错的地方。一个误区是只看 localStorage 里有没有 token不看 token 是否过期。更完善的做法是在路由守卫里做一个“静默验证”router.beforeEach(async (to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } try { const res await request.get(/auth/profile) if (res.code 0) { next() } else { localStorage.removeItem(token) next(/login) } } catch (e) { localStorage.removeItem(token) next(/login) } })这样即使 token 过期用户刷新页面后也会被自动踢到登录页而不会出现“明明登录了接口却一直 401”的诡异问题。6.4 路由参数传递的典型写法热词里提到“vue路由参数”这个在前后端分离项目里很常见。比如管理员在用户列表点击某个用户进入详情页需要传递用户 ID。使用 Vue Router 4 的写法// 路由配置 { path: /user/:id, component: UserDetail } // 跳转 router.push({ name: UserDetail, params: { id: 1 } }) // 在组件中获取 const route useRoute() console.log(route.params.id)需要注意的一个坑是用params传参时刷新页面后参数会丢失。如果信息重要要把 ID 放到路由路径里或者从后端重新拉取详情数据不要依赖页面间的临时状态。7. 上线部署waitress nginx 配合 Django 的方案与踩坑7.1 开发环境跟生产环境最大的差异不在代码在服务运行方式很多同学的项目在本地用python manage.py runserver跑得飞起一部署到服务器就出各种问题。原因很简单runserver是 Django 自带的开发服务器性能和稳定性都不适合生产环境。网上有很多教程推荐用 gunicorn 部署 Django但 gunicorn 在 Windows 服务器上支持不好。我们这台校园网服务器是 Windows Server所以最终选择了 waitress这是一个跨平台的纯 Python WSGI 服务器Windows 和 Linux 都能稳定跑。7.2 waitress 部署的基本配置使用 waitress 前首先安装pip install waitress然后在项目根目录创建一个wsgi.py或者直接用命令启动waitress-serve --listen127.0.0.1:8000 config.wsgi:application注意监听端口要跟 nginx 配置保持一致。这里我先监听127.0.0.1目的是不让 Django 直接暴露在公网而是通过前面的 nginx 做一层转发。为了确保服务崩溃后能自动重启我用了 Windows 的任务计划程序或者直接在服务器上注册成 NSSM 服务把 waitress 做成一个常驻后台服务。这样服务器重启后服务也能自动拉起不需要人工登录服务器手动执行命令。7.3 nginx 反向代理配置与静态文件收集nginx 在项目中承担了两个任务一是把来自 80 端口的请求转发给 Django二是托管 Vue 打包后的静态文件和 Django 的静态资源。一个最小配置示例server { listen 80; server_name auth.example.edu.cn; # Vue 前端打包文件 root /home/www/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } # API 反向代理到 Django location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Django 静态文件 location /static/ { alias /home/www/django_static/; } }这里的try_files $uri $uri/ /index.html是 Vue Router 使用 history 模式时的关键配置不加这一行用户手动刷新非首页路由时会 404。7.4 静态文件显示不了这个经典问题的完整排查链路热词里有“vscode写img标签 在django的static文件中显示不了”我在这个项目里也遇到过排查链路值得分享。首先确认DEBUG False后Django 默认不会自动服务静态文件必须先执行收集命令python manage.py collectstatic这个命令会把所有 app 下的静态文件复制到STATIC_ROOT指定目录。排查顺序是确认STATIC_URL是否正确例如/static/确认STATIC_ROOT是否配置成绝对路径确认collectstatic是否执行成功目录里有没有文件确认 nginx 的location /static/是否正确指向STATIC_ROOT直接访问http://ip/static/xxx.png看返回什么状态码大部分情况下问题都出在第二步和第四步的路径不一致。尤其是 Windows 服务器上路径可能带有盘符nginx 的 alias 路径很容易配错。8. 项目结束后的一些体会和可扩展方向项目上线稳定运行了一段时间我自己总结了几条真实体会给后面做同类系统的朋友一个参考。第一认证系统这类项目最容易翻车的地方不是认证本身而是权限数据的前后端一致性。前端隐藏了菜单后端接口也得拦两边但凡有一个漏了就等于没做权限。我们的做法是维护一张权限清单表前端路由表和后端装饰器都从这张表里生成减少人为不一致。第二校园网环境里用户量虽然不小但并发峰值其实不高真正卡脖子的往往是旧系统和乱七八糟的网络设备对接。做系统时一定要提前考虑协议对接的复杂度给设备联动部分留足开发时间。第三这个小系统后续可以扩展的方向很多。热词里有人搜“vue播放m3u8”其实就是典型的认证后资源访问场景用户通过认证后才能访问特定的流媒体资源。接入一个视频资源服务时只要在访问控制层加一个新的资源类型和权限点前端拿到授权后用 vue-video-player 或者原生 video 标签播放 m3u8 流即可。认证和权限层搭建好之后这些扩展都是比较顺理成章的事情。我个人在实际操作中还有一个习惯就是每完成一个阶段任务都会把 PyCharm 里跑通的服务和 nginx 配置一起写成一份简单的部署文档。别小看这几百字文档后来同事接手维护时全靠它省下了大量重复排查时间。做这类系统代码只是其中一部分让别人能接手、敢维护才算是真正交付完成。