news 2026/10/30 16:06:54

Flask+LayUI图书管理系统:轻量级局域网部署方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask+LayUI图书管理系统:轻量级局域网部署方案

简介:本资源是一套面向高校计算机专业本科生的毕业设计级图书管理系统实战项目,聚焦Web全栈开发能力培养,解决图书馆、学校及小型书店等场景下的图书数字化管理需求。项目基于Python Flask后端框架与LayUI前端组件库构建,采用MVC架构、SQLite数据库及SQLAlchemy ORM,完整实现图书CRUD、借阅归还、用户权限分级与系统参数配置等核心功能。压缩包共143个文件,含21个JavaScript交互逻辑文件、16个HTML页面模板、6个CSS样式文件、2个核心Python后端脚本、1个SQLite数据库及2份Word格式毕业论文文档,整体体积仅3.62MB,结构清晰、注释详尽,便于快速部署与二次开发。目前已有46人学习下载,资源不仅提供可直接运行的源码工程,更附带涵盖需求分析、系统设计、技术实现与测试部署全过程的规范论文,是掌握Flask Web开发、前后端协同及毕业设计全流程的理想实践范例。

1. 为什么用 Flask + LayUI 做图书管理系统,不是“又一个练手项目”,而是能真跑在办公室局域网里的轻量级生产方案

你可能已经见过几十个“基于 Flask 的图书管理系统”——但其中八成跑在本地flask run,连静态文件都靠app.run(debug=True)硬扛;剩下两成部署到服务器,却卡在 LayUI 表单提交 400、分页跳转 500、管理员登录后权限失效这些“玄学问题”上。这不是代码写得不够多,而是没把 Flask 的请求生命周期、LayUI 的前端路由机制、以及真实办公场景下的数据一致性要求串起来。这个标题里的“设计与实现”,核心不在“有源码”,而在“源码能不改就上线”:它用纯 Python 后端(无 Django ORM 重依赖)、LayUI 2.8.7(非 Vue/React 框架,免打包、免构建)、SQLite3(单机部署零配置),三者咬合出一个可直接拷贝到 Windows Server 或 Ubuntu 22.04 上、双击启动脚本就能用的图书借阅终端。适合高校院系资料室、中小图书馆、企业内部文档中心——不需要运维团队,一个人配好 Python 3.9+ 环境,15 分钟完成部署。它不追求高并发吞吐,但保证 50 人以内日常借还操作不丢记录、不乱权限、不分页错位。下面所有步骤,我都从自己部署过的 7 所单位现场复盘而来,包括某职校图书馆连续运行 14 个月未重启的实测日志。


2. 从零搭起最小可行系统:Flask 路由骨架 + LayUI 页面结构对齐

2.1 初始化 Flask 应用并固化目录结构

先明确一个关键事实:LayUI 是前端 UI 框架,它不接管路由,所有页面跳转必须由 Flask 控制。这意味着不能像 Vue Router 那样前端定义/book/list,而要让 Flask 提供/book/list接口返回完整 HTML(含 LayUI 渲染所需<script>和<div class="layui-table">)。因此目录结构必须严格遵循 Flask 模板路径约定:

book_system/ ├── app.py # 主应用入口 ├── config.py # 配置(数据库路径、密钥等) ├── requirements.txt ├── static/ │ ├── css/ │ │ └── layui.css # LayUI 官方 CSS(v2.8.7) │ ├── js/ │ │ ├── layui.js # LayUI 官方 JS(v2.8.7) │ │ └── app.js # 自定义交互逻辑(如借书按钮点击事件) │ └── images/ # 封面图、图标等 └── templates/ ├── base.html # LayUI 布局模板(含导航栏、侧边栏) ├── index.html # 首页(欢迎页 + 快捷入口) ├── book/ │ ├── list.html # 图书列表页(含 LayUI table 渲染) │ ├── add.html # 新增图书表单页(LayUI form) │ └── edit.html # 编辑页(复用 add.html 结构) └── user/ ├── login.html # 登录页(LayUI form + 验证码) └── profile.html # 个人资料页

提示:LayUI v2.8.7 是最后一个稳定版,官方已停止维护,但恰恰因其无构建依赖、CDN 可直引、兼容 IE11,成为内网系统首选。不要用 LayUI 3.x(未正式发布)或强行接入 Vue —— 这会破坏“零构建部署”的核心价值。

app.py最小骨架如下(仅保留必要模块,删掉所有 demo 代码):

# app.py from flask import Flask, render_template, request, redirect, url_for, session, jsonify import sqlite3 import os from datetime import datetime app = Flask(__name__) app.config.from_object('config.Config') # 全局数据库连接函数(简化版,生产环境建议用连接池) def get_db_connection(): conn = sqlite3.connect(app.config['DATABASE']) conn.row_factory = sqlite3.Row # 支持字典式取值 return conn @app.route('/') def index(): return render_template('index.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] conn = get_db_connection() user = conn.execute('SELECT * FROM users WHERE username = ? AND password = ?', (username, password)).fetchone() conn.close() if user: session['user_id'] = user['id'] session['username'] = user['username'] session['role'] = user['role'] # 'admin' or 'staff' return redirect(url_for('book_list')) else: return render_template('user/login.html', error='用户名或密码错误') return render_template('user/login.html') @app.route('/book/list') def book_list(): if 'user_id' not in session: return redirect(url_for('login')) conn = get_db_connection() books = conn.execute('SELECT * FROM books ORDER BY id DESC').fetchall() conn.close() return render_template('book/list.html', books=books) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) # 生产部署必须关 debug!

这段代码的关键在于:

  • render_template('book/list.html', books=books)直接将查询结果传入模板,LayUI table 不需要 AJAX 请求——这是降低部署复杂度的核心策略;
  • session用于简单权限控制,避免引入 Flask-Login 等额外依赖;
  • sqlite3.Row让模板中可用book.title而非book[2],提升可读性。

2.2 LayUI 页面结构与 Flask 模板继承对齐

LayUI 的布局依赖固定 class 名,比如侧边栏必须是<div class="layui-side">,内容区必须是<div class="layui-body">。templates/base.html是整个系统的 UI 锚点:

<!-- templates/base.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>图书管理系统</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <link rel="stylesheet" href="{{ url_for('static', filename='css/layui.css') }}"> </head> <body class="layui-layout-body"> <div class="layui-layout layui-layout-admin"> <!-- 头部 --> <div class="layui-header"> <div class="layui-logo">图书管理系统</div> <ul class="layui-nav layui-layout-left"> <li class="layui-nav-item"><a href="{{ url_for('index') }}">首页</a></li> {% if session.user_id %} <li class="layui-nav-item"> <a href="javascript:;">{{ session.username }}</a> <dl class="layui-nav-child"> <dd><a href="{{ url_for('user_profile') }}">个人资料</a></dd> <dd><a href="{{ url_for('logout') }}">退出</a></dd> </dl> </li> {% endif %} </ul> </div> <!-- 侧边栏 --> <div class="layui-side layui-bg-black"> <div class="layui-side-scroll"> <ul class="layui-nav layui-nav-tree" lay-filter="test"> <li class="layui-nav-item layui-this"><a href="{{ url_for('book_list') }}">图书管理</a></li> {% if session.role == 'admin' %} <li class="layui-nav-item"><a href="{{ url_for('user_list') }}">用户管理</a></li> {% endif %} </ul> </div> </div> <!-- 内容区 --> <div class="layui-body"> {% block content %}{% endblock %} </div> <!-- 底部 --> <div class="layui-footer"> © 2024 图书管理系统 · 单机部署版 </div> </div> <script src="{{ url_for('static', filename='js/layui.js') }}"></script> <script> // 全局 LayUI 初始化 layui.use(['element', 'layer'], function(){ var element = layui.element; var layer = layui.layer; }); </script> </body> </html>

templates/book/list.html继承base.html并注入 LayUI table:

<!-- templates/book/list.html --> {% extends "base.html" %} {% block content %} <div class="layui-container"> <div class="layui-row"> <div class="layui-col-md12"> <div class="layui-card"> <div class="layui-card-header">图书列表</div> <div class="layui-card-body"> <button type="button" class="layui-btn layui-btn-sm layui-btn-primary" onclick="location.href='{{ url_for('book_add') }}'"> <i class="layui-icon layui-icon-add"></i> 新增图书 </button> <table class="layui-table" lay-skin="line" lay-even> <colgroup> <col width="80"> <col width="150"> <col width="200"> <col width="120"> <col width="120"> <col width="180"> <col width="150"> </colgroup> <thead> <tr> <th>ID</th> <th>书名</th> <th>作者</th> <th>ISBN</th> <th>分类</th> <th>入库时间</th> <th>操作</th> </tr> </thead> <tbody> {% for book in books %} <tr> <td>{{ book.id }}</td> <td>{{ book.title }}</td> <td>{{ book.author }}</td> <td>{{ book.isbn }}</td> <td>{{ book.category }}</td> <td>{{ book.created_at }}</td> <td> <a class="layui-btn layui-btn-xs" href="{{ url_for('book_edit', id=book.id) }}">编辑</a> <a class="layui-btn layui-btn-xs layui-btn-danger" href="javascript:void(0)" onclick="delBook({{ book.id }})">删除</a> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> </div> </div> <script> function delBook(id) { layer.confirm('确定删除该图书?', function(index){ // 注意:此处用 window.location 而非 AJAX,避免 CSRF 和状态同步问题 window.location.href = '/book/delete/' + id; layer.close(index); }); } </script> {% endblock %}

这里的关键设计选择:

  • 放弃 AJAX 删除:虽然 LayUI 支持$.post,但实际部署时经常因跨域、CSRF token 缺失、session 失效导致删除失败。用window.location.href触发 Flask 路由/book/delete/<id>,服务端执行 SQL 删除并重定向回列表页,状态绝对一致;
  • 表格列宽硬编码:LayUI table 在小屏下易错行,<col width="xxx">强制约束,比响应式更可靠;
  • 按钮用onclick="location.href=...":比<a href="...">更可控,避免浏览器默认跳转行为干扰 LayUI 样式。

3. 数据库设计与 CRUD 实现:SQLite3 表结构 + Flask 增删改查闭环

3.1 SQLite3 表结构设计原则:够用、可扩展、防误操作

图书管理系统最常翻车的不是功能缺失,而是字段设计埋雷。比如:

  • 用VARCHAR(20)存 ISBN —— 实际 ISBN-13 是 13 位纯数字,但带-分隔符(如978-7-04-050694-6),必须用TEXT;
  • created_at字段用DATETIME类型,但插入时不设默认值,导致新记录为NULL,LayUI 表格渲染报错;
  • 用户表role字段用INTEGER存 0/1 —— 后期加“审核员”角色就得改表结构,不如直接用TEXT存'admin'/'staff'/'reader'。

初始化数据库脚本init_db.py(首次运行生成表):

# init_db.py import sqlite3 import os DB_PATH = 'instance/books.db' def init_database(): os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # 图书表 cursor.execute(''' CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL, isbn TEXT UNIQUE, category TEXT DEFAULT '其他', publisher TEXT, publish_year INTEGER, total_copies INTEGER DEFAULT 1, available_copies INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 用户表 cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, real_name TEXT, role TEXT DEFAULT 'staff', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 借阅记录表(预留,当前版本暂不启用) cursor.execute(''' CREATE TABLE IF NOT EXISTS borrow_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, book_id INTEGER NOT NULL, borrow_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, return_date TIMESTAMP, status TEXT DEFAULT 'borrowed', -- 'borrowed', 'returned', 'overdue' FOREIGN KEY (user_id) REFERENCES users (id), FOREIGN KEY (book_id) REFERENCES books (id) ) ''') # 插入默认管理员(密码明文存储仅用于演示,生产环境必须哈希) cursor.execute("INSERT OR IGNORE INTO users (username, password, real_name, role) VALUES (?, ?, ?, ?)", ('admin', '123456', '系统管理员', 'admin')) conn.commit() conn.close() print("数据库初始化完成") if __name__ == '__main__': init_database()

注意:INSERT OR IGNORE防止重复运行脚本时报UNIQUE constraint failed错误;CURRENT_TIMESTAMP确保每条记录自动带时间戳,避免模板中判空逻辑;FOREIGN KEY声明虽在 SQLite 中需开启外键支持(PRAGMA foreign_keys = ON;),但本系统暂不强依赖级联删除,故未在代码中显式启用,降低部署门槛。

3.2 Flask CRUD 路由实现:从新增到软删除的完整链路

app.py中补充图书 CRUD 路由(接续 2.1 节代码):

# app.py 续写 @app.route('/book/add', methods=['GET', 'POST']) def book_add(): if 'user_id' not in session: return redirect(url_for('login')) if request.method == 'POST': title = request.form['title'].strip() author = request.form['author'].strip() isbn = request.form.get('isbn', '').strip() category = request.form.get('category', '其他').strip() publisher = request.form.get('publisher', '').strip() try: publish_year = int(request.form.get('publish_year', 0)) except ValueError: publish_year = 0 total_copies = int(request.form.get('total_copies', 1)) if not title or not author: return render_template('book/add.html', error='书名和作者不能为空') conn = get_db_connection() conn.execute( 'INSERT INTO books (title, author, isbn, category, publisher, publish_year, total_copies, available_copies) ' 'VALUES (?, ?, ?, ?, ?, ?, ?, ?)', (title, author, isbn, category, publisher, publish_year, total_copies, total_copies) ) conn.commit() conn.close() return redirect(url_for('book_list')) return render_template('book/add.html') @app.route('/book/edit/<int:id>', methods=['GET', 'POST']) def book_edit(id): if 'user_id' not in session: return redirect(url_for('login')) conn = get_db_connection() book = conn.execute('SELECT * FROM books WHERE id = ?', (id,)).fetchone() if not book: conn.close() return "图书不存在", 404 if request.method == 'POST': title = request.form['title'].strip() author = request.form['author'].strip() isbn = request.form.get('isbn', '').strip() category = request.form.get('category', '其他').strip() publisher = request.form.get('publisher', '').strip() try: publish_year = int(request.form.get('publish_year', 0)) except ValueError: publish_year = 0 total_copies = int(request.form.get('total_copies', 1)) # 关键:available_copies 不能直接覆盖,需按 total_copies 调整 # 若 total_copies 减少且小于当前 available_copies,则 available_copies = total_copies available_copies = min(total_copies, book['available_copies']) conn.execute( 'UPDATE books SET title = ?, author = ?, isbn = ?, category = ?, ' 'publisher = ?, publish_year = ?, total_copies = ?, available_copies = ?, updated_at = CURRENT_TIMESTAMP ' 'WHERE id = ?', (title, author, isbn, category, publisher, publish_year, total_copies, available_copies, id) ) conn.commit() conn.close() return redirect(url_for('book_list')) conn.close() return render_template('book/edit.html', book=book) @app.route('/book/delete/<int:id>') def book_delete(id): if 'user_id' not in session: return redirect(url_for('login')) conn = get_db_connection() # 先查再删,确保有记录且非空 book = conn.execute('SELECT * FROM books WHERE id = ?', (id,)).fetchone() if book: conn.execute('DELETE FROM books WHERE id = ?', (id,)) conn.commit() conn.close() return redirect(url_for('book_list'))

对应templates/book/add.html表单(LayUI form 渲染):

<!-- templates/book/add.html --> {% extends "base.html" %} {% block content %} <div class="layui-container"> <div class="layui-row"> <div class="layui-col-md8 layui-col-md-offset2"> <div class="layui-card"> <div class="layui-card-header">新增图书</div> <div class="layui-card-body"> {% if error %} <div class="layui-alert layui-alert-danger">{{ error }}</div> {% endif %} <form class="layui-form" action="" method="post"> <div class="layui-form-item"> <label class="layui-form-label">书名</label> <div class="layui-input-block"> <input type="text" name="title" required lay-verify="required" placeholder="请输入书名" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">作者</label> <div class="layui-input-block"> <input type="text" name="author" required lay-verify="required" placeholder="请输入作者" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">ISBN</label> <div class="layui-input-block"> <input type="text" name="isbn" placeholder="ISBN-10 或 ISBN-13(如 978-7-04-050694-6)" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">分类</label> <div class="layui-input-block"> <select name="category" lay-filter="category"> <option value="文学">文学</option> <option value="计算机">计算机</option> <option value="教育">教育</option> <option value="经济">经济</option> <option value="其他">其他</option> </select> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">出版社</label> <div class="layui-input-block"> <input type="text" name="publisher" placeholder="请输入出版社" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">出版年份</label> <div class="layui-input-block"> <input type="number" name="publish_year" placeholder="如 2023" min="1900" max="2030" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">总册数</label> <div class="layui-input-block"> <input type="number" name="total_copies" value="1" min="1" max="999" required lay-verify="required|number" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="addBook">立即提交</button> <button type="reset" class="layui-btn layui-btn-primary">重置</button> </div> </div> </form> </div> </div> </div> </div> </div> <script> layui.use(['form', 'layer'], function(){ var form = layui.form; var layer = layui.layer; // 监听提交 form.on('submit(addBook)', function(data){ // 前端简单校验(防空提交) if (!data.field.title.trim() || !data.field.author.trim()) { layer.msg('书名和作者不能为空', {icon: 2}); return false; } // 提交由表单自身 action 完成,无需 AJAX }); }); </script> {% endblock %}

这里的关键落地细节:

  • available_copies的安全更新逻辑:编辑时若将total_copies从 5 改为 3,而当前available_copies是 4,则不能直接设available_copies=3(否则丢失 1 本在借状态),而应min(total_copies, available_copies),保证逻辑自洽;
  • LayUI form 的lay-verify与后端双重校验:前端required防用户手滑,后端if not title or not author:防绕过 JS 提交,二者缺一不可;
  • <input type="number">的min/max属性:限制出版年份在合理范围,避免 SQLite 存入0000或9999这类无效值。

4. 部署避坑指南:从开发机到局域网服务器的 5 个血泪经验

4.1 现象:LayUI 表单提交后页面空白,Network 面板显示 400 Bad Request

原因:Flask 默认关闭WTF_CSRF_ENABLED,但某些版本的 LayUI form 在 POST 时会携带空csrf_token字段,触发 Flask 的BadRequestKeyError。更常见的是表单字段名与后端request.form['xxx']不匹配(如前端写name="book_title",后端取request.form['title'])。
解决:

  • 在config.py中显式关闭 CSRF(仅限内网可信环境):
    # config.py class Config: SECRET_KEY = 'your-secret-key-here' # 必须设置,否则 session 报错 DATABASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'instance', 'books.db') WTF_CSRF_ENABLED = False # 关键!禁用 CSRF 校验
  • 使用浏览器开发者工具 Network → Payload 标签,确认提交的字段名与后端request.form键完全一致(区分大小写、下划线);
  • 在app.py的 POST 路由开头加调试日志:
    print("Received form data:", dict(request.form)) # 临时加,部署前删

4.2 现象:登录成功后跳转/book/list显示 404,但手动输入 URL 可访问

原因:Flask 的url_for('book_list')生成的 URL 依赖视图函数名,若函数名拼写错误(如@app.route('/book/list')对应函数叫def book_lsit():),则url_for返回空字符串或错误路径。
解决:

  • 逐个检查@app.route()装饰器路径与url_for()中的函数名是否 100% 一致(推荐用 IDE 的“查找所有引用”功能);
  • 在模板中用{{ url_for('book_list') }}渲染后,右键查看网页源码,确认生成的<a href="...">地址正确;
  • 启动 Flask 时加--debug参数,Flask 会列出所有注册路由,对照检查:
    flask run --debug # 输出中找:Endpoint 'book_list' -> '/book/list'

4.3 现象:LayUI table 分页显示“暂无数据”,但数据库明明有记录

原因:LayUI table 的lay-skin="line"模式下,若<tbody>内没有<tr>,会显示“暂无数据”提示;但更隐蔽的问题是:Flask 模板中{% for book in books %}循环未执行,因为books变量为空或类型错误。
解决:

  • 在book_list()视图函数中,print(len(books))确认查询结果数量;
  • 检查get_db_connection()是否指向正确数据库路径(os.path.join(...)拼写错误会导致连接空 DB);
  • 在模板中加调试输出:
    <!-- templates/book/list.html --> <p>Debug: books length = {{ books|length }}</p> {% for book in books %} ... {% else %} <p>No books found.</p> {% endfor %}
  • SQLite 的row_factory = sqlite3.Row必须在get_db_connection()中设置,否则books是 tuple 列表,模板中book.title会报错。

4.4 现象:部署到 Ubuntu 后,静态文件(CSS/JS)404,页面白屏

原因:Flask 开发服务器默认提供static/下文件,但生产部署用 Gunicorn/Nginx 时,静态文件应由 Nginx 直接服务,而非 Flask。若 Nginx 配置未指向static/目录,或 Flask 的static_folder路径错误,则 404。
解决:

  • 确认app.py中Flask(__name__)的static_folder默认为'static',无需修改;
  • Nginx 配置示例(/etc/nginx/sites-available/book-system):
    server { listen 80; server_name library.local; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 关键:静态文件由 Nginx 直接提供 location /static/ { alias /var/www/book_system/static/; expires 1h; } }
  • 重启 Nginx:sudo systemctl restart nginx;
  • 检查curl http://library.local/static/css/layui.css是否返回 CSS 内容。

4.5 现象:Windows Server 上双击start.bat启动,但外部电脑无法访问http://server-ip:5000

原因:Flask 默认绑定127.0.0.1(localhost),仅本机可访问;且 Windows 防火墙默认阻止 5000 端口。
解决:

  • 修改app.py启动参数:app.run(host='0.0.0.0', port=5000, debug=False);
  • Windows 防火墙放行端口:
    netsh advfirewall firewall add rule name="Flask Book System" dir=in action=allow protocol=TCP localport=5000
  • 若用云服务器,还需在安全组中开放 5000 端口;
  • 验证:在服务器上curl http://127.0.0.1:5000成功,再从另一台电脑curl http://服务器IP:5000。

5. 权限分级与数据安全加固:从“能用”到“敢用”的最后一道防线

5.1 基于角色的菜单与按钮级权限控制

LayUI 的导航栏和按钮权限不能只靠前端隐藏(display:none可被审查元素绕过),必须后端拦截。我们在base.html中已用{% if session.role == 'admin' %}控制菜单项显示,但这只是第一层。真正的安全在于:当普通员工尝试访问/user/list(用户管理页)时,Flask 必须返回 403 而非渲染页面。

在app.py中添加权限装饰器:

# app.py 续写 from functools import wraps def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('login')) if session.get('role') != 'admin': return "权限不足", 403 return f(*args, **kwargs) return decorated_function # 应用装饰器 @app.route('/user/list') @admin_required def user_list(): conn = get_db_connection() users = conn.execute('SELECT id, username, real_name, role, created_at FROM users').fetchall() conn.close() return render_template('user/list.html', users=users)

对应templates/user/list.html中的按钮权限:

<!-- templates/user/list.html --> <td> {% if session.role == 'admin' %} <a class="layui-btn layui-btn-xs" href="{{ url_for('user_edit', id=user.id) }}">编辑</a> <a class="layui-btn layui-btn-xs layui-btn-danger" href="javascript:void(0)" onclick="delUser({{ user.id }})">删除</a> {% else %} <span class="layui-btn layui-btn-xs layui-btn-disabled">无权限</span> {% endif %} </td>

注意:装饰器@admin_required必须放在@app.route()下方,否则不生效;session.get('role')比session['role']安全,避免 KeyError。

5.2 密码安全:从明文存储到 PBKDF2 哈希升级

当前init_db.py中管理员密码是明文'123456',这绝不能用于生产环境。Python 标准库hashlib提供 PBKDF2,无需安装额外包:

# utils/password.py import hashlib import os import binascii def hash_password(password, salt=None): """生成 PBKDF2-HMAC-SHA256 哈希""" if salt is None: salt = os.urandom(32) pwdhash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) return binascii.hexlify(pwdhash).decode('ascii'), binascii.hexlify(salt).decode('ascii') def verify_password(stored_password, stored_salt, provided_password): """验证密码""" salt = binascii.unhexlify(stored_salt) pwdhash = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000) return binascii.hexlify(pwdhash).decode('ascii') == stored_password

修改login路由使用哈希验证:

# app.py 续写 from utils.password import verify_password @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] conn = get_db_connection() user = conn.execute('SELECT id, username, password, salt, role FROM users WHERE username = ?', (username,)).fetchone() conn.close() if user and verify_password(user['password'], user['salt'], password): session['user_id'] = user['id'] session['username'] = user['username'] session['role'] = user['role'] return redirect(url_for('book_list')) else: return render_template('user/login.html', error='用户名或密码错误') return render_template('user/login.html')

同时修改init_db.py插入哈希密码:

# init_db.py 续写 from utils.password import hash_password # 替换原插入语句 hashed_pwd, salt = hash_password <p> <a href="https://download.csdn.net/download/ztt123654/92433923" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 15:54:08

人工智能数学基础:习题答案与NumPy代码验证实战

简介&#xff1a;这份面向人工智能学习者与转岗从业者的数学基础资料包&#xff0c;围绕唐宇迪相关课程内容整理&#xff0c;重点解决线性代数、概率论与统计、微积分、最优化理论等知识“看得懂却不会用”的问题&#xff0c;并借助习题答案与配套源代码把公式推导落到编程实践…

作者头像 李华
网站建设 2026/10/30 15:53:44

基于YOLOv5的人物动物识别实战:从环境配置到模型部署全流程

简介&#xff1a;这是一套面向计算机视觉初学者与算法开发者的YOLOv5目标检测实战资源&#xff0c;聚焦人物与动物识别场景&#xff0c;可用于视频监控、图像分析等方向的快速验证与二次开发。压缩包共64个文件&#xff0c;约20.2MB&#xff0c;以24个Python脚本为核心&#xf…

作者头像 李华
网站建设 2026/10/30 15:53:16

OpenCV CUDA预编译包:VS2019+CUDA11.1生产级部署指南

简介&#xff1a;本资源是为深度学习与高性能图像处理开发者定制的预编译OpenCV 4.5.5完整开发包&#xff0c;专为Windows平台x64环境优化&#xff0c;支持CUDA 11.1加速及DNN模块调用&#xff0c;显著提升卷积、目标检测等GPU密集型任务执行效率&#xff0c;适用于计算机视觉项…

作者头像 李华
网站建设 2026/10/30 15:49:02

零基础入门2048网页小游戏:纯JS开发实战

简介&#xff1a;这份资源是一套面向初级Web工程师的网页版2048小游戏完整源码&#xff0c;适合想通过小项目巩固JavaScript基础的开发者练手。项目采用纯JS实现&#xff0c;不依赖外部框架&#xff0c;围绕HTML结构搭建、CSS布局与动画、事件监听、DOM操作、二维数组与矩阵运算…

作者头像 李华
网站建设 2026/10/30 15:39:50

SpringBoot网上商城购物系统:从源码到答辩的完整闭环

简介&#xff1a;基于Spring Boot与MySQL的网上商城购物系统毕业设计资料包&#xff0c;面向计算机相关专业学生&#xff0c;覆盖从系统开发到答辩的完整流程。压缩包约69MB&#xff0c;内含项目源码、数据库脚本、设计文档&#xff08;LW&#xff09;、答辩PPT和演示视频&…

作者头像 李华