Python 从入门到实战(十四):Flask 用户认证(给 Web 应用加安全锁,区分管理员与普通用户)

文章目录

    • [一、为什么需要用户认证?前序系统的 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:添加管理员用户(执行一次))
    • [五、实战 3:配置 Flask-Login(管理登录状态)](#五、实战 3:配置 Flask-Login(管理登录状态))
      • [1. 更新`app.py`:初始化 Flask-Login](#1. 更新app.py:初始化 Flask-Login)
    • [六、实战 4:用 Flask-WTF 创建登录表单](#六、实战 4:用 Flask-WTF 创建登录表单)
      • [1. 在`app.py`中定义登录表单](#1. 在app.py中定义登录表单)
      • [2. 创建登录页面模板`templates/login.html`](#2. 创建登录页面模板templates/login.html)
    • [七、实战 5:实现登录与登出功能](#七、实战 5:实现登录与登出功能)
      • [1. 登录路由(`/login`)](#1. 登录路由(/login))
      • [2. 登出路由(`/logout`)](#2. 登出路由(/logout))
    • [八、实战 6:权限控制(区分管理员与普通用户)](#八、实战 6:权限控制(区分管理员与普通用户))
    • [九、实战 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 个安全漏洞

在学具体操作前,先明确 "为什么必须加用户认证"------ 回顾上一篇的系统,存在以下致命问题:

  1. 无登录验证 :任何人只要知道系统地址(比如http://localhost:5000),就能直接访问所有功能;
  2. 无权限区分:学生、老师、管理员看到的功能完全一样,学生可能误删或篡改成绩;
  3. 数据无保护:没有身份记录,无法追溯 "谁修改了数据",出问题后难以排查。

而加上用户认证后,系统会变成这样:

  • 管理员(比如老师):能登录,拥有 "新增 / 编辑 / 删除学生""查看所有数据" 的完整权限;
  • 普通用户(比如学生):能登录,只能 "查询自己的成绩""查看公开报告",看不到删除 / 编辑按钮;
  • 未登录用户:只能看到登录页面,无法访问任何核心功能。

这才是符合实际场景的安全设计 ------ 不同角色拥有不同权限,数据安全可控。

二、选择工具: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_authenticatedFlask-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

十一、小结与下一篇预告

这篇你学到了什么?

  1. 认证必要性:理解无权限控制的安全风险,明确管理员与普通用户的权限边界;
  2. 工具使用 :用Flask-Login管理登录状态,Flask-WTF处理表单,Werkzeug加密密码;
  3. 核心功能
    • 设计User模型,实现密码加密存储;
    • 配置登录 / 登出逻辑,维护用户会话;
    • 自定义admin_required装饰器,实现角色权限控制;
    • 模板中根据角色显示 / 隐藏元素,优化用户体验;
  4. 安全实践:密码哈希、CSRF 保护、会话加密,避免常见安全漏洞。

下一篇预告

现在,系统已经安全且功能完整,但只能在本地运行(localhost:5000),别人无法通过互联网访问。下一篇咱们会学习Flask 应用部署,将系统部署到云服务器(如阿里云、腾讯云),配置域名和 HTTPS,让任何人都能通过公网地址访问你的学生成绩管理系统,真正落地实用。

如果这篇内容帮你成功给系统加上了安全锁,欢迎在评论区分享你的测试结果或遇到的问题,咱们一起交流进步~

相关推荐
兴趣使然黄小黄2 小时前
【Pytest】使用Allure生成企业级测试报告
python·pytest
be or not to be2 小时前
前端基础实战笔记:文档流 + 盒子模型
前端·笔记
程序员码歌2 小时前
短思考第264天,每天复盘5分钟,胜过你盲目努力1整年(2)
前端·后端·ai编程
nono牛2 小时前
实战项目:设计一个智能温控服务
android·前端·网络·算法
010不二2 小时前
基于Appium爬虫文本导出可话个人动态
数据库·爬虫·python·appium
TTGGGFF2 小时前
实用代码工具:Python打造PDF选区OCR / 截图批量处理工具(支持手动/全自动模式)
python·pdf·ocr
山峰哥3 小时前
Python爬虫实战:从零构建高效数据采集系统
开发语言·数据库·爬虫·python·性能优化·架构
敲敲了个代码9 小时前
从硬编码到 Schema 推断:前端表单开发的工程化转型
前端·javascript·vue.js·学习·面试·职场和发展·前端框架
Jay_Franklin10 小时前
SRIM通过python计算dap
开发语言·python