尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flask框架RBAC权限管理系统源码解析:从表设计到前端实现

Flask框架RBAC权限管理系统源码解析:从表设计到前端实现 简介这套基于Flask框架的权限管理系统源码定位为面向Python开发者的后台权限管理解决方案覆盖用户、角色、资源与机构四大核心模块适合需要快速搭建企业级后台或学习Flask项目结构的初中级开发者。资源包共2000个文件约17.35MB前端以HTML、CSS、JavaScript为主包含236个html模板、307个css样式文件以及236个js脚本界面组件覆盖bootstrap、easyui等常见框架后端由24个Python文件完成主要逻辑另含数据库SQL脚本、说明文档及少量配置文件从页面展示到数据存储均有可参考的完整链路。前端设计参考了sypro的界面风格交互直观图标与图片素材丰富可直接应用于中小型管理项目。目前已有657人学习下载用于权限模型理解、Flask项目实战或界面二次开发均具参考价值也可作为课程设计或毕业设计的基础框架展开改造。1. 先拆清楚这不是一个随便的Flask后台模板如果权限系统只在登录后判断is_admin角色一多、数据跨部门代码就开始打补丁。这套基于Flask框架的权限管理系统源码把权限拆成用户、角色、资源、机构四条独立主线资源管你能看到哪个菜单、点哪个按钮机构管你能动哪些部门的数据两者在角色上汇合互不干扰。项目一共2432个文件真正的后端逻辑只有23个Python文件剩下大量静态资源是为了把管理端界面做成参考sypro风格的成熟后台。适合两类人想直接拿来做内管系统起步工程的Python开发以及想研究Flask里RBAC权限数据流怎么落地的后端工程师。2. RBAC模型怎么落到Flask从四张表反推权限关系2.1 用户、角色、资源、机构各管什么用户、角色、资源、机构是权限模型里的四个独立维度。用户是账号主体解决“你是谁”的问题角色是权限集合的命名解决“你能干什么”的问题资源在源码里拆成menu、button、api三种类型目录、菜单、按钮的层级用parent_id串起来这样一棵权限树同时承担菜单渲染和接口鉴权两种职能。机构单独抽出来是为了让同一角色在不同的机构范围下看到的数据行不同典型场景是区域经理角色只允许查询自己辖区内的订单。很多自学例子把权限做成user表里的is_admin字段一开始确实省事但出现临时跨部门授权时就得改表结构改完还要处理历史数据。把角色和资源拆表之后新增一个跨部门角色只需要插入几条关联记录不用动业务代码。这套源码的数据模型基本沿用RBAC标准做法区别在于它把机构维度显式建模而不是让每个业务表都冗余一个org_id后各自判断。实体表名核心字段承担职责用户sys_userusername, password_hash, org_id登录凭证与机构归属角色sys_rolerole_code, role_name, data_scope权限集合与数据范围资源sys_resourceparent_id, resource_type, path, perms菜单层级与操作标识机构sys_organizationparent_id, org_name, org_code数据权限的过滤边界2.2 建表SQL与关联关系源码里表名可能带前缀或使用其他命名这里给出的是最常见的等价结构方便在它上面做二次开发。核心是四张主表加两张关联表CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, org_id INT NOT NULL COMMENT 所属机构, enabled TINYINT DEFAULT 1 COMMENT 0禁用 1启用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sys_organization ( id INT PRIMARY KEY AUTO_INCREMENT, parent_id INT DEFAULT 0 COMMENT 上级机构, org_name VARCHAR(128) NOT NULL, org_code VARCHAR(64) NOT NULL UNIQUE ); CREATE TABLE sys_role ( id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(64) NOT NULL, role_code VARCHAR(64) NOT NULL UNIQUE, data_scope TINYINT DEFAULT 0 COMMENT 0按机构 1全部 ); CREATE TABLE sys_resource ( id INT PRIMARY KEY AUTO_INCREMENT, parent_id INT DEFAULT 0, resource_name VARCHAR(64) NOT NULL, resource_type VARCHAR(16) NOT NULL COMMENT menu/button/api, path VARCHAR(255) COMMENT 路由地址, perms VARCHAR(128) COMMENT 权限标识如sys:user:add ); CREATE TABLE sys_user_role ( user_id INT NOT NULL, role_id INT NOT NULL, PRIMARY KEY (user_id, role_id) ); CREATE TABLE sys_role_resource ( role_id INT NOT NULL, resource_id INT NOT NULL, PRIMARY KEY (role_id, resource_id) );梳理一下设计逻辑sys_user.org_id指向用户所属机构用于数据级权限的默认归属后台可以根据角色的data_scope决定查询列表时是否拼接org_id条件resource_type区分的三种类型分别对应菜单树、按钮显隐、接口校验这样一个字段能同时满足前端菜单渲染和后端权限判断sys_role_resource把角色和资源关联起来一个角色拥有哪些菜单、哪些按钮、哪些API全部集中在这张表里后续加权限只插关联记录不碰代码。2.3 一次查询拿到完整权限集合登录成功后需要把用户拥有的角色码和权限标识一次性取出常用写法是五表JOINdef load_user_permissions(user_id, cursor): sql SELECT r.role_code, res.resource_name, res.resource_type, res.path, res.perms FROM sys_user u JOIN sys_user_role ur ON u.id ur.user_id JOIN sys_role r ON ur.role_id r.id JOIN sys_role_resource rr ON r.id rr.role_id JOIN sys_resource res ON rr.resource_id res.id WHERE u.id %s cursor.execute(sql, (user_id,)) rows cursor.fetchall() perms set() roles set() for row in rows: roles.add(row[role_code]) if row[perms]: perms.add(row[perms]) return {roles: roles, perms: perms}这里的SQL用JOIN链把四张主表串起来从user_id出发倒推权限标识。返回集合有两个原因过滤重复关联记录以及后续判断sys:user:add in perms时是哈希查找比list线性扫描快。如果你的数据库驱动用的是pymysql执行前要把cursorclass设为DictCursor否则row[role_code]取不到值。权限集合放进session后后续路由直接读内存集合即可不需要每次请求都回查数据库。3. Flask登录态与权限控制从装饰器到请求拦截3.1 session还是token这套系统以服务端模板渲染为主页面由Flask生成后输出浏览器因此登录态用Flask原生session最合适。session在服务端只下发一个签名Cookie用户数据存在服务器内存或Redis里配合app.config[SECRET_KEY]防止签名被篡改生产环境建议把session迁移到Redis避免多进程部署时重启丢失登录态。有两点要提前想清楚第一如果将来要拆前后端再把session改造成token或JWT不要在一开始就为用不到的前后端分离承担额外复杂度第二页面大量通过EasyUI的datagrid加载数据这类AJAX请求在登录超时后会收到一个HTML登录页而不是预期的JSON前端解析就会报模板错误所以装饰器里必须区分普通请求和AJAX请求分别返回不同结构。3.2 两层装饰器先登录再授权把登录判断和权限判断拆成两个装饰器方便路由组合复用from functools import wraps from flask import session, redirect, url_for, request, jsonify, abort def login_required(func): wraps(func) def wrapper(*args, **kwargs): if session.get(user_id) is None: if request.headers.get(X-Requested-With) XMLHttpRequest: return jsonify({code: 401, msg: 登录已失效}), 401 return redirect(url_for(auth.login)) return func(*args, **kwargs) return wrapper def permission_required(perm): def decorator(func): wraps(func) def wrapper(*args, **kwargs): if perm not in session.get(perms, []): if request.headers.get(X-Requested-With) XMLHttpRequest: return jsonify({code: 403, msg: 没有操作权限}), 403 abort(403) return func(*args, **kwargs) return wrapper return decorator路由上的组合写法app.route(/user/add) login_required permission_required(sys:user:add) def user_add(): # 新增用户逻辑 return render_template(user_add.html)从代码执行顺序看permission_required先包装原函数login_required再包装上一层所以访问路由时先检查登录态再检查权限标识。AJAX请求返回401或403状态码普通页面请求则跳转登录页或返回403页面。session.get(perms, [])写成一个空list兜底防止并发请求时session中的perms还没写入导致KeyError。实际项目里逐个路由加装饰器容易漏更常见的是用before_request统一拦截app.before_request def check_permission(): if request.endpoint static: return None path request.path perm_map { /user/add: sys:user:add, /user/delete: sys:user:delete, /role/edit: sys:role:edit, /resource/save: sys:resource:save, } perm perm_map.get(path) if perm and perm not in session.get(perms, []): if request.headers.get(X-Requested-With) XMLHttpRequest: return jsonify({code: 403, msg: 没有操作权限}), 403 abort(403)这段代码手动维护path - perm映射漏配的路径默认放行。安全要求高的场景建议反过来做成白名单机制只有登录页和首页放行其余URL全部要求存在对应的资源记录和权限标识这样新增接口后会立即暴露未授权问题。拦截方式粒度维护成本适用场景装饰器函数级低随路由定义可见模块少权限点固定before_request映射URL级中要单独维护映射表权限点较多统一收口数据库资源表动态高权限可配置管理后台权限经常变更3.3 前端按钮级权限怎么同步服务端权限判断只解决接口安全页面上仍然要控制按钮显隐。在渲染用户列表页面时把当前用户perms传入模板app.route(/user/list) login_required def user_list(): return render_template(user_list.html, permssession.get(perms, []))Jinja2模板配合tojson过滤器使用{% if sys:user:add in perms %} a idbtn-add classeasyui-linkbutton iconClsicon-add新增用户/a {% endif %} script var perms {{ session.get(perms, []) | tojson }}; function hasPerm(code) { return perms.indexOf(code) ! -1; } /scriptEasyUI的toolbar初始化时会解析>!DOCTYPE html html langzh-CN head meta charsetUTF-8 title{% block title %}权限管理系统{% endblock %}/title link relstylesheet href{{ url_for(static, filenamecss/bootstrap.min.css) }} link relstylesheet href{{ url_for(static, filenamecss/easyui.css) }} {% block css %}{% endblock %} /head body div idsidebar classlayout-side{% include components/sidebar.html %}/div div idmain {% block content %}{% endblock %} /div script src{{ url_for(static, filenamejs/jquery.min.js) }}/script script src{{ url_for(static, filenamejs/easyui.min.js) }}/script {% block scripts %}{% endblock %} /body /html有三个容易踩坑的点base模板里的block不要开太多保留title、content、scripts三个足以多了反而让子模板难以控制格式Bootstrap和EasyUI混排时表格用了classeasyui-datagrid就不要追加classtable否则Bootstrap会叠加边框和斑马纹与datagrid渲染后的样式冲突EasyUI组件初始化依赖jQuery所以easyui.min.js必须放在jQuery之后否则会报$.fn.datagrid is undefined。4.3 把resource表渲染成动态菜单资源表里resource_typemenu的记录数量往往最多取出后先筛选菜单类型再用递归函数转成树结构def build_menu_tree(resource_list, parent_id0): tree [] for item in resource_list: if item[parent_id] parent_id: children build_menu_tree(resource_list, item[id]) if children: item[children] children tree.append(item) return tree对应视图函数app.route(/) login_required def index(): menu_resources fetch_menu_resources(session[user_id]) menus build_menu_tree(menu_resources) return render_template(index.html, menusmenus)侧边栏的Jinja2渲染ul classnav-menu {% for menu in menus %} li a href{{ menu.path or javascript:; }} {{ menu.resource_name }} /a {% if menu.children %} ul classnav-submenu {% for child in menu.children %} lia href{{ child.path }}{{ child.resource_name }}/a/li {% endfor %} /ul {% endif %} /li {% endfor %} /ul递归函数把扁平的资源列表转成树结构后在模板中渲染比在Jinja2里套递归宏更直观排查问题时也容易在Python端打断点。resource_type为button的记录不要在菜单树里出现渲染前先过滤一遍否则导航栏会冒出大量空链接的按钮项。4.4 datagrid数据加载与机构过滤用户列表这种典型页面后端返回JSON前端datagrid直接拉取table classeasyui-datagrid title用户列表 >app.route(/user/list/data) login_required def user_list_data(): page int(request.args.get(page, 1)) rows int(request.args.get(rows, 10)) org_id session.get(org_id) # 如果角色不是全部数据范围则递归收集子机构id data query_users_in_scope(org_id, page, rows) return jsonify({total: data[total], rows: data[rows]})EasyUI datagrid的分页参数固定是page和rows前端自动携带后端直接读取。机构过滤不要只判断当前org_id部门层级深时要把子机构id递归收集到集合里放进SQL的IN条件否则会出现上级只能看到本部门直属用户、看不到下级部门用户的诡异现象。5. 拿到2432个文件后的第一件事资源瘦身与路由可达性验证5.1 文件构成怎么读项目文件分布很能说明问题图片1499个CSS/LESS 355个HTML/HTM 295个JS 236个Python 23个JSON 20个。图片占掉六成说明体积主要来自UI图标、背景图和主题素材对权限逻辑本身不是必需品。23个Python文件对应视图、模型、工具和配置一个权限管理系统的核心逻辑用23个py文件完全可以表达其余静态资源只是让管理后台看起来完整。拿到源码后先别急着把整个static目录放进生产环境按运行链路筛选真正的依赖。排查入口是base.html、login.html、index.html三类模板逐层跟踪CSS和JS引用。如果同时存在多个EasyUI主题优先保留与bootstrap-united.css配色一致的版本删掉其他CSS后刷新页面观察布局是否有明显变化。5.2 清理未引用静态文件的小脚本用Python扫描src和href里的静态路径可以快速找出明显未引用的文件import re import pathlib static_dir pathlib.Path(static) template_dir pathlib.Path(templates) pattern re.compile(r(?:src|href)\s*\s*[](/static/[^])[]) used set() for html_file in template_dir.rglob(*.html): for match in pattern.finditer( html_file.read_text(encodingutf-8, errorsignore)): used.add(match.group(1).replace(/static/, , 1)) unused_size 0 for asset in static_dir.rglob(*): if asset.suffix.lower() not in (.png, .gif, .jpg, .jpeg, .css, .js): continue relative asset.relative_to(static_dir).as_posix() if relative not in used: unused_size asset.stat().st_size print(未引用:, asset) print(可释放字节数:, unused_size)这个脚本只处理模板里直接写死的静态路径。如果项目里有动态拼接/static/xxx的代码正则匹配不到需要把动态路径加入白名单。删除任何文件前先在项目目录执行一次grep -r 文件名 templates static/js确认没有在JS字符串里被拼接过。5.3 路由可达性验证清理完静态资源还要验证每个主要页面是否能正常渲染返回200。Flask自带测试客户端写冒烟脚本很合适from app import app with app.test_client() as client: client.post(/auth/login, data{username: admin, password: admin123}) for route in (/, /user/list, /role/list, /resource/list, /org/list): response client.get(route) print(route, response.status_code)这里的admin账号对应源码初始化数据实际部署后要先创建管理员。登录成功后依次请求核心路由返回302说明登录态没写入session先检查登录视图是否调用了session[user_id]返回500重点看模板变量名和权限装饰器里的session.get(perms)是否类型不一致返回404则检查路由注册顺序和数据库中的资源path字段。配合上面的静态链路清理一轮下来就能把这套源码从“能看”变成“能跑”。当你能对着HTTP状态码逐条排除错误这套Flask框架权限管理系统才算真正被你接住了。本文还有配套的精品资源点击获取
返回列表