
文章目录
-
- [一、为什么需要用户认证?前序系统的 3 个安全漏洞](#一、为什么需要用户认证?前序系统的 3 个安全漏洞)
- [二、选择工具:Flask 生态的认证组合](#二、选择工具:Flask 生态的认证组合)
- [三、实战 1:环境准备与依赖安装](#三、实战 1:环境准备与依赖安装)
- [四、实战 2:扩展数据模型(新增 User 表)](#四、实战 2:扩展数据模型(新增 User 表))
-
- [1. 修改`models.py`:新增 User 模型](#1. 修改
models.py:新增 User 模型) - [2. 创建 User 表并添加管理员用户](#2. 创建 User 表并添加管理员用户)
-
- [步骤 1:更新`app.py`的模型导入(确保包含 User)](#步骤 1:更新
app.py的模型导入(确保包含 User)) - [步骤 2:创建 User 表(执行一次)](#步骤 2:创建 User 表(执行一次))
- [步骤 3:添加管理员用户(执行一次)](#步骤 3:添加管理员用户(执行一次))
- [步骤 1:更新`app.py`的模型导入(确保包含 User)](#步骤 1:更新
- [1. 修改`models.py`:新增 User 模型](#1. 修改
- [五、实战 3:配置 Flask-Login(管理登录状态)](#五、实战 3:配置 Flask-Login(管理登录状态))
-
- [1. 更新`app.py`:初始化 Flask-Login](#1. 更新
app.py:初始化 Flask-Login)
- [1. 更新`app.py`:初始化 Flask-Login](#1. 更新
- [六、实战 4:用 Flask-WTF 创建登录表单](#六、实战 4:用 Flask-WTF 创建登录表单)
- [七、实战 5:实现登录与登出功能](#七、实战 5:实现登录与登出功能)
-
- [1. 登录路由(`/login`)](#1. 登录路由(
/login)) - [2. 登出路由(`/logout`)](#2. 登出路由(
/logout))
- [1. 登录路由(`/login`)](#1. 登录路由(
- [八、实战 6:权限控制(区分管理员与普通用户)](#八、实战 6:权限控制(区分管理员与普通用户))
-
- [1. 自定义管理员权限装饰器](#1. 自定义管理员权限装饰器)
- [2. 给原有路由添加权限控制](#2. 给原有路由添加权限控制)
- [3. 模板权限控制(根据角色显示 / 隐藏元素)](#3. 模板权限控制(根据角色显示 / 隐藏元素))
- [九、实战 7:测试权限控制](#九、实战 7:测试权限控制)
-
- [1. 管理员登录(用户名:admin,密码:admin123)](#1. 管理员登录(用户名:admin,密码:admin123))
- [2. 创建普通用户并测试](#2. 创建普通用户并测试)
- [十、新手必踩的 5 个坑:认证与权限避坑指南](#十、新手必踩的 5 个坑:认证与权限避坑指南)
-
- [坑 1:密码明文存储(未使用加密方法)](#坑 1:密码明文存储(未使用加密方法))
- [坑 2:`current_user`使用时未初始化登录管理器](#坑 2:
current_user使用时未初始化登录管理器) - [坑 3:装饰器顺序错误(`@login_required`在`@app.route`上面)](#坑 3:装饰器顺序错误(
@login_required在@app.route上面)) - [坑 4:表单提交报错 "CSRF token missing"](#坑 4:表单提交报错 “CSRF token missing”)
- [坑 5:未登录用户能直接访问受保护路由](#坑 5:未登录用户能直接访问受保护路由)
- 十一、小结与下一篇预告
欢迎回到「Python 从入门到实战」系列专栏。上一篇咱们用 Flask+SQLite 打造了支持数据增删改查的学生成绩管理系统,但有个关键漏洞: 任何人都能访问系统,甚至删除学生数据 ------ 没有登录验证,也没有权限区分,这在实际使用中完全不安全(比如学生自己修改成绩、误删数据)。
今天咱们要给系统加上 "安全锁"------Flask 用户认证与权限控制 。通过Flask-Login(管理登录状态)和Flask-WTF(处理登录表单),实现 "用户登录""角色区分(管理员 / 普通用户)""权限管控" 三大核心功能:管理员能增删改查所有数据,普通用户只能查询成绩,从根本上解决数据安全问题。咱们会从 "认证必要性" 入手,逐步完成 "用户模型设计→登录功能实现→权限控制",让系统真正具备生产环境的安全性。
一、为什么需要用户认证?前序系统的 3 个安全漏洞
在学具体操作前,先明确 "为什么必须加用户认证"------ 回顾上一篇的系统,存在以下致命问题:
- 无登录验证 :任何人只要知道系统地址(比如
http://localhost:5000),就能直接访问所有功能; - 无权限区分:学生、老师、管理员看到的功能完全一样,学生可能误删或篡改成绩;
- 数据无保护:没有身份记录,无法追溯 "谁修改了数据",出问题后难以排查。
而加上用户认证后,系统会变成这样:
- 管理员(比如老师):能登录,拥有 "新增 / 编辑 / 删除学生""查看所有数据" 的完整权限;
- 普通用户(比如学生):能登录,只能 "查询自己的成绩""查看公开报告",看不到删除 / 编辑按钮;
- 未登录用户:只能看到登录页面,无法访问任何核心功能。
这才是符合实际场景的安全设计 ------ 不同角色拥有不同权限,数据安全可控。
二、选择工具:Flask 生态的认证组合
为了降低开发难度,咱们选择 Flask 生态中成熟的扩展,不用从零实现认证逻辑:
| 工具 | 作用 | 核心优势 |
|---|---|---|
Flask-Login |
管理用户登录状态(登录 / 登出 / 会话保持) | 无需手动处理 Cookie/Session,支持 "记住我" 功能 |
Flask-WTF |
处理表单(登录表单的验证、CSRF 保护) | 自动生成表单 HTML,支持字段验证(必填、格式等) |
Werkzeug |
密码加密(避免明文存储) | 提供安全的哈希算法,防止密码泄露 |
这些工具都是 Flask 官方推荐的,兼容性好,文档丰富,新手容易上手。
三、实战 1:环境准备与依赖安装
首先,安装所需扩展(确保已激活项目环境):
bash
bash
# 安装Flask-Login(登录状态管理)
pip install flask-login
# 安装Flask-WTF(表单处理)
pip install flask-wtf
# Werkzeug已随Flask安装,无需额外安装(用于密码加密)
安装完成后,验证是否成功:打开 Python 终端,输入以下命令,无报错即安装成功:
python
python
import flask_login
import flask_wtf
from werkzeug.security import generate_password_hash
print("安装成功!")
四、实战 2:扩展数据模型(新增 User 表)
要实现用户认证,首先需要一个User表存储用户信息(用户名、加密密码、角色)。咱们在models.py中新增User模型,与之前的Student"Course模型共存。
1. 修改models.py:新增 User 模型
python
python
# models.py(新增User模型,放在Student模型前面)
from app import db
from datetime import datetime
# 从Werkzeug导入密码加密相关函数
from werkzeug.security import generate_password_hash, check_password_hash
# 从Flask-Login导入用户状态相关类
from flask_login import UserMixin
# 1. 用户模型(存储登录信息与角色)
class User(UserMixin, db.Model): # 继承UserMixin,获得Flask-Login所需的默认方法
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 用户名(唯一,用于登录)
username = db.Column(db.String(50), nullable=False, unique=True)
# 密码哈希(存储加密后的密码,不存储明文)
password_hash = db.Column(db.String(256), nullable=False) # 哈希值较长,设256长度
# 角色(admin:管理员,user:普通用户)
role = db.Column(db.String(20), nullable=False, default='user') # 默认普通用户
# 创建时间
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 方法1:设置密码(生成哈希值,不存储明文)
def set_password(self, password):
# generate_password_hash:生成密码哈希,method指定加密算法
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
# 方法2:验证密码(对比输入密码的哈希与存储的哈希)
def check_password(self, password):
# check_password_hash:验证密码是否匹配
return check_password_hash(self.password_hash, password)
# 方法3:判断是否为管理员
def is_admin(self):
return self.role == 'admin'
def __repr__(self):
return f'<User {self.username} ({self.role})>'
# 2. 学生模型(原有代码,不变)
class Student(db.Model):
__tablename__ = 'students'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(50), nullable=False, unique=True)
age = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
courses = db.relationship('Course', backref='student', lazy=True, cascade='all, delete-orphan')
def __repr__(self):
return f'<Student {self.name}>'
# 3. 课程模型(原有代码,不变)
class Course(db.Model):
__tablename__ = 'courses'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(50), nullable=False)
score = db.Column(db.Integer, nullable=False)
student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)
def __repr__(self):
return f'<Course {self.name} - {self.score}>'
关键说明:
UserMixin:必须继承,它提供了Flask-Login所需的默认方法(如get_id()"is_authenticated),避免手动实现;- 密码加密 :
set_password生成哈希值,check_password验证哈希,绝对不存储明文密码(防止数据库泄露后密码被窃取); - 角色字段 :
role区分 "admin" 和 "user",默认是普通用户,管理员需要手动设置。
2. 创建 User 表并添加管理员用户
修改app.py,确保db初始化后包含User模型,然后创建表并添加第一个管理员:
步骤 1:更新app.py的模型导入(确保包含 User)
python
python
# app.py(更新模型导入,原有代码修改)
from models import User, Student, Course # 新增User
步骤 2:创建 User 表(执行一次)
打开 Python 终端,执行以下命令创建users表:
python
python
# 进入Python终端
python
# 执行代码创建表
from app import app, db
from models import User
with app.app_context():
db.create_all() # 会创建users表(如果不存在)
print("User表创建成功!")
步骤 3:添加管理员用户(执行一次)
继续在 Python 终端中执行,创建一个用户名admin、密码admin123的管理员:
python
python
with app.app_context():
# 检查admin用户是否已存在
admin = User.query.filter_by(username='admin').first()
if not admin:
# 创建管理员用户
admin = User(username='admin', role='admin') # 角色设为admin
admin.set_password('admin123') # 密码设为admin123(会自动加密)
db.session.add(admin)
db.session.commit()
print("管理员用户创建成功!用户名:admin,密码:admin123")
else:
print("管理员用户已存在")
执行后,数据库中会新增一条管理员记录,密码存储的是哈希值(不是明文admin123)。
五、实战 3:配置 Flask-Login(管理登录状态)
接下来,在app.py中初始化Flask-Login,配置用户加载逻辑(登录后如何获取当前用户信息)。
1. 更新app.py:初始化 Flask-Login
python
python
# app.py(更新配置,新增Flask-Login相关代码)
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user # 新增
from flask_wtf import FlaskForm # 新增,用于表单
from wtforms import StringField, PasswordField, SubmitField # 新增,表单字段
from wtforms.validators import DataRequired, Length # 新增,表单验证
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os
# 1. 初始化Flask应用(原有代码)
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///instance/student.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 必须设置SECRET_KEY(用于Flask-WTF表单CSRF保护、Flask-Login会话加密)
app.config['SECRET_KEY'] = 'student-system-2024-secure-key' # 建议修改为更复杂的字符串
# 2. 初始化SQLAlchemy(原有代码)
db = SQLAlchemy(app)
# 3. 初始化Flask-Login(新增)
login_manager = LoginManager()
login_manager.init_app(app)
# 配置登录页面的路由(未登录用户访问需要登录的页面时,自动跳转到这里)
login_manager.login_view = 'login'
# 配置登录提示信息
login_manager.login_message = '请先登录以访问该页面'
login_manager.login_message_category = 'info'
# 4. 导入模型(原有代码,确保包含User)
from models import User, Student, Course
# 5. 配置用户加载回调函数(Flask-Login核心:根据用户id获取用户)
@login_manager.user_loader
def load_user(user_id):
# 根据用户id查询User模型,返回用户对象
with app.app_context():
return User.query.get(int(user_id))
# 6. 中文字体配置(原有代码,不变)
plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
if not os.path.exists('static/images'):
os.makedirs('static/images')
关键说明:
login_manager.login_view = 'login':指定登录页面的路由函数名(后面会定义login路由),未登录用户访问受保护页面时会自动跳转;user_loader回调 :Flask-Login 需要通过这个函数根据user_id获取用户对象,确保登录状态能正确保持;SECRET_KEY:必须设置,用于加密会话数据和表单 CSRF 保护,生产环境中要设置为复杂的随机字符串(避免泄露)。
六、实战 4:用 Flask-WTF 创建登录表单
登录需要一个表单让用户输入用户名和密码,用Flask-WTF创建表单,自动处理验证和 CSRF 保护。
1. 在app.py中定义登录表单
python
python
# app.py(新增登录表单定义,放在Flask-Login配置后面)
# 登录表单类(继承FlaskForm)
class LoginForm(FlaskForm):
# 用户名字段:必填,长度2-20字符
username = StringField(
'用户名',
validators=[
DataRequired(message='用户名不能为空'), # 必填验证
Length(min=2, max=20, message='用户名长度必须在2-20字符之间') # 长度验证
],
render_kw={'class': 'form-control', 'placeholder': '请输入用户名'} # 生成HTML时的属性(样式、提示)
)
# 密码字段:必填,长度6-20字符(密码不能太短)
password = PasswordField(
'密码',
validators=[
DataRequired(message='密码不能为空'),
Length(min=6, max=20, message='密码长度必须在6-20字符之间')
],
render_kw={'class': 'form-control', 'placeholder': '请输入密码'}
)
# 提交按钮
submit = SubmitField(
'登录',
render_kw={'class': 'btn btn-primary w-100'} # 按钮样式(宽100%)
)
关键说明:
StringField/PasswordField:分别对应文本输入框和密码输入框(密码会隐藏显示);validators:验证规则,DataRequired确保字段不为空,Length限制长度;render_kw:指定生成 HTML 元素时的属性,比如class用于设置 Bootstrap 样式,placeholder显示提示文字。
2. 创建登录页面模板templates/login.html
html
html
<!-- templates/login.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录 - 学生成绩管理系统</title>
<!-- 引入Bootstrap样式(保持和其他页面一致) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- 登录卡片(居中显示) -->
<div class="container d-flex justify-content-center align-items-center min-vh-100">
<div class="card shadow-sm w-100 max-w-md">
<div class="card-header bg-primary text-white text-center">
<h4 class="mb-0">学生成绩管理系统 - 登录</h4>
</div>
<div class="card-body p-4">
<!-- 显示flash消息(如登录失败提示) -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- 登录表单:action为空,提交到当前路由;method为POST -->
<form method="POST">
<!-- CSRF令牌(Flask-WTF自动生成,防止跨站请求伪造) -->
{{ form.hidden_tag() }}
<!-- 用户名字段 -->
<div class="mb-3">
{{ form.username.label(class="form-label") }}
<!-- 显示字段输入框 -->
{{ form.username() }}
<!-- 显示字段验证错误 -->
{% if form.username.errors %}
{% for error in form.username.errors %}
<div class="text-danger mt-1">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<!-- 密码字段 -->
<div class="mb-4">
{{ form.password.label(class="form-label") }}
{{ form.password() }}
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="text-danger mt-1">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<!-- 提交按钮 -->
{{ form.submit() }}
</form>
</div>
</div>
</div>
<!-- Bootstrap JS(用于关闭alert等交互) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
关键说明:
form.hidden_tag():必须添加,生成 CSRF 令牌,防止跨站请求伪造攻击(Flask-WTF 的核心安全功能);- 错误显示 :
form.username.errors获取字段验证错误(如 "用户名不能为空"),友好提示用户; - flash 消息 :显示登录失败的提示(如 "用户名或密码错误"),通过
get_flashed_messages获取。
七、实战 5:实现登录与登出功能
现在,编写登录和登出的路由,处理表单提交、验证用户、维护登录状态。
1. 登录路由(/login)
python
python
# app.py(新增登录路由)
@app.route('/login', methods=['GET', 'POST'])
def login():
# 如果用户已登录,直接跳转到首页(避免重复登录)
if current_user.is_authenticated:
return redirect(url_for('index'))
# 创建登录表单对象
form = LoginForm()
# 如果表单提交且验证通过(POST请求 + 字段验证无错误)
if form.validate_on_submit():
# 1. 根据用户名查询用户
with app.app_context():
user = User.query.filter_by(username=form.username.data).first()
# 2. 验证用户是否存在且密码正确
if user and user.check_password(form.password.data):
# 3. 登录用户:login_user函数(Flask-Login提供)
# remember=True:勾选"记住我"时生效,这里简化不做,默认会话结束后登出
login_user(user)
# 4. 跳转到"登录前想访问的页面"(如访问/student/list被拦截,登录后跳回)
next_page = request.args.get('next')
# 如果没有next_page,跳转到首页
return redirect(next_page or url_for('index'))
else:
# 登录失败,显示提示
flash('登录失败!用户名或密码错误', 'danger')
# GET请求或表单验证失败,显示登录页面
return render_template('login.html', form=form)
2. 登出路由(/logout)
python
python
# app.py(新增登出路由)
@app.route('/logout')
@login_required # 必须登录才能访问登出功能
def logout():
# 登出用户:logout_user函数(Flask-Login提供)
logout_user()
flash('已成功登出', 'success')
# 登出后跳转到登录页面
return redirect(url_for('login'))
关键说明:
current_user.is_authenticated:Flask-Login提供的属性,判断用户是否已登录;form.validate_on_submit():Flask-WTF提供的方法,判断表单是否提交且所有字段验证通过;login_user(user):登录用户,创建会话,后续请求中current_user会指向该用户;@login_required:装饰器,未登录用户访问该路由时,会自动跳转到login_view指定的登录页面。
八、实战 6:权限控制(区分管理员与普通用户)
登录功能实现后,需要给不同角色分配不同权限:
- 管理员(admin):能访问所有功能(新增 / 编辑 / 删除 / 查询);
- 普通用户(user):只能访问查询类功能(首页、学生列表、成绩查询、可视化报告),看不到增删改按钮。
1. 自定义管理员权限装饰器
在app.py中新增admin_required装饰器,限制只有管理员能访问:
python
python
# app.py(新增管理员装饰器,放在登录路由前面)
from functools import wraps # 用于自定义装饰器
# 自定义装饰器:只有管理员才能访问
def admin_required(f):
@wraps(f) # 保留原函数的元信息(如函数名、文档字符串)
def decorated_function(*args, **kwargs):
# 1. 先判断是否登录(如果未登录,会被@login_required拦截,这里双重保险)
if not current_user.is_authenticated:
return redirect(url_for('login', next=request.url))
# 2. 判断是否为管理员
if not current_user.is_admin():
flash('无权限访问!需要管理员权限', 'danger')
return redirect(url_for('index')) # 跳转到首页
# 3. 是管理员,执行原函数
return f(*args, **kwargs)
return decorated_function
2. 给原有路由添加权限控制
修改app.py中需要权限控制的路由,添加对应的装饰器:
(1)需要管理员权限的路由(增删改)
python
python
# 新增学生路由:需要管理员权限
@app.route('/student/add', methods=['GET', 'POST'])
@login_required # 先确保登录
@admin_required # 再确保是管理员
def student_add():
# 原有代码不变...
# 编辑成绩路由:需要管理员权限
@app.route('/student/edit/<int:student_id>', methods=['GET', 'POST'])
@login_required
@admin_required
def student_edit(student_id):
# 原有代码不变...
# 删除学生路由:需要管理员权限
@app.route('/student/delete/<int:student_id>', methods=['POST'])
@login_required
@admin_required
def student_delete(student_id):
# 原有代码不变...
(2)普通用户可访问的路由(查询类)
python
python
# 首页路由:登录即可访问
@app.route('/')
@login_required
def index():
return render_template('index.html')
# 学生列表路由:登录即可访问
@app.route('/student/list')
@login_required
def student_list():
# 原有代码不变...
# 成绩查询路由:登录即可访问
@app.route('/student/search', methods=['GET', 'POST'])
@login_required
def student_search():
# 原有代码不变...
# 可视化报告路由:登录即可访问
@app.route('/report')
@login_required
def report():
# 原有代码不变...
3. 模板权限控制(根据角色显示 / 隐藏元素)
修改模板文件,让普通用户看不到 "新增 / 编辑 / 删除" 按钮,避免界面混淆。
(1)修改导航栏(templates/base.html)
在导航栏添加登录状态和登出按钮,管理员显示 "新增学生" 按钮:
html
html
<!-- templates/base.html(修改导航栏部分) -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">学生成绩系统</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/student/list">学生列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/student/search">成绩查询</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/report">可视化报告</a>
</li>
<!-- 只有管理员显示"新增学生"按钮 -->
{% if current_user.is_authenticated and current_user.is_admin() %}
<li class="nav-item">
<a class="nav-link" href="/student/add">新增学生</a>
</li>
{% endif %}
</ul>
<!-- 登录状态显示 -->
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<!-- 已登录:显示用户名和登出按钮 -->
<li class="nav-item me-3">
<span class="nav-link text-white">欢迎,{{ current_user.username }}</span>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">登出</a>
</li>
{% else %}
<!-- 未登录:显示登录按钮 -->
<li class="nav-item">
<a class="nav-link" href="/login">登录</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
(2)修改学生列表模板(templates/student_list.html)
普通用户隐藏 "编辑 / 删除" 按钮:
html
html
<!-- templates/student_list.html(修改表格操作列) -->
<tbody>
{% for student in students %}
<tr>
<td>{{ student.name }}</td>
<td>{{ student.age }}</td>
<td>{{ student.course_name }}</td>
<td>{{ student.course_score }}</td>
<td>
{% if student.grade == 'A级(90+)' %}
<span class="badge bg-success">{{ student.grade }}</span>
{% elif student.grade == 'B级(80-89)' %}
<span class="badge bg-warning">{{ student.grade }}</span>
{% else %}
<span class="badge bg-danger">{{ student.grade }}</span>
{% endif %}
</td>
<td>
<!-- 只有管理员显示编辑/删除按钮 -->
{% if current_user.is_admin() %}
<a href="{{ url_for('student_edit', student_id=student.id) }}" class="btn btn-sm btn-warning">编辑</a>
<form method="POST" action="{{ url_for('student_delete', student_id=student.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除吗?')">删除</button>
</form>
{% else %}
<!-- 普通用户显示"无权限"提示 -->
<span class="text-muted">无操作权限</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
九、实战 7:测试权限控制
现在,启动系统,测试不同角色的权限是否生效:
1. 管理员登录(用户名:admin,密码:admin123)
- 能看到 "新增学生" 导航按钮;
- 学生列表中有 "编辑 / 删除" 按钮;
- 能访问
/student/add"/student/edit/1等路由; - 登出后跳转到登录页面。
2. 创建普通用户并测试
在 Python 终端中创建一个普通用户(用户名:user1,密码:user123):
python
python
from app import app, db
from models import User
with app.app_context():
user = User(username='user1')
user.set_password('user123') # 角色默认是user
db.session.add(user)
db.session.commit()
print("普通用户创建成功!用户名:user1,密码:user123")
用user1登录:
- 看不到 "新增学生" 按钮;
- 学生列表中操作列显示 "无操作权限";
- 直接访问
/student/add会被@admin_required拦截,跳回首页并提示 "无权限"。
十、新手必踩的 5 个坑:认证与权限避坑指南
用户认证涉及会话、加密、权限等细节,新手容易踩坑,总结如下:
坑 1:密码明文存储(未使用加密方法)
python
python
# 错误示例:直接存储明文密码
user = User(username='admin', password_hash='admin123') # 危险!
解决 :必须用set_password方法生成哈希:
python
python
user = User(username='admin')
user.set_password('admin123') # 正确,存储哈希值
坑 2:current_user使用时未初始化登录管理器
python
python
# 错误示例:未初始化login_manager,直接使用current_user
@app.route('/')
def index():
print(current_user.username) # 报错:AttributeError: 'AnonymousUserMixin' object has no attribute 'username'
解决 :必须先初始化login_manager并配置user_loader:
python
python
# 正确步骤:
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
with app.app_context():
return User.query.get(int(user_id))
坑 3:装饰器顺序错误(@login_required在@app.route上面)
python
python
# 错误示例:装饰器顺序错误,导致权限控制失效
@login_required
@app.route('/index') # 错误:@app.route应在最上面
def index():
pass
解决 :路由装饰器(@app.route)必须在最上面,权限装饰器在下面:
python
python
@app.route('/index')
@login_required # 正确顺序
def index():
pass
坑 4:表单提交报错 "CSRF token missing"
python
python
# 错误示例:登录表单中未添加form.hidden_tag()
<form method="POST">
{{ form.username() }}
{{ form.password() }}
{{ form.submit() }}
</form>
解决 :必须添加form.hidden_tag()生成 CSRF 令牌:
python
python
<form method="POST">
{{ form.hidden_tag() }} # 正确,添加CSRF保护
{{ form.username() }}
{{ form.password() }}
{{ form.submit() }}
</form>
坑 5:未登录用户能直接访问受保护路由
python
python
# 错误示例:忘记添加@login_required装饰器
@app.route('/student/list')
def student_list(): # 未登录用户也能访问
pass
解决 :所有核心路由必须添加@login_required(查询类)或@admin_required(增删改类):
python
python
@app.route('/student/list')
@login_required # 正确,未登录用户会跳转
def student_list():
pass
十一、小结与下一篇预告
这篇你学到了什么?
- 认证必要性:理解无权限控制的安全风险,明确管理员与普通用户的权限边界;
- 工具使用 :用
Flask-Login管理登录状态,Flask-WTF处理表单,Werkzeug加密密码; - 核心功能 :
- 设计
User模型,实现密码加密存储; - 配置登录 / 登出逻辑,维护用户会话;
- 自定义
admin_required装饰器,实现角色权限控制; - 模板中根据角色显示 / 隐藏元素,优化用户体验;
- 设计
- 安全实践:密码哈希、CSRF 保护、会话加密,避免常见安全漏洞。
下一篇预告
现在,系统已经安全且功能完整,但只能在本地运行(localhost:5000),别人无法通过互联网访问。下一篇咱们会学习Flask 应用部署,将系统部署到云服务器(如阿里云、腾讯云),配置域名和 HTTPS,让任何人都能通过公网地址访问你的学生成绩管理系统,真正落地实用。
如果这篇内容帮你成功给系统加上了安全锁,欢迎在评论区分享你的测试结果或遇到的问题,咱们一起交流进步~