欢迎光临
我们一直在努力

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

c6e3154d215982a75f2280a6ca6650b4.jpeg

文章目录

    • 一、为什么需要用户认证?前序系统的 3 个安全漏洞
    • 二、选择工具:Flask 生态的认证组合
    • 三、实战 1:环境准备与依赖安装
    • 四、实战 2:扩展数据模型(新增 User 表)
      • 1. 修改`models.py`:新增 User 模型
        • 关键说明:
      • 2. 创建 User 表并添加管理员用户
        • 步骤 1:更新`app.py`的模型导入(确保包含 User)
        • 步骤 2:创建 User 表(执行一次)
        • 步骤 3:添加管理员用户(执行一次)
    • 五、实战 3:配置 Flask-Login(管理登录状态)
      • 1. 更新`app.py`:初始化 Flask-Login
        • 关键说明:
    • 六、实战 4:用 Flask-WTF 创建登录表单
      • 1. 在`app.py`中定义登录表单
        • 关键说明:
      • 2. 创建登录页面模板`templates/login.html`
        • 关键说明:
    • 七、实战 5:实现登录与登出功能
      • 1. 登录路由(`/login`)
      • 2. 登出路由(`/logout`)
        • 关键说明:
    • 八、实战 6:权限控制(区分管理员与普通用户)
      • 1. 自定义管理员权限装饰器
      • 2. 给原有路由添加权限控制
        • (1)需要管理员权限的路由(增删改)
        • (2)普通用户可访问的路由(查询类)
      • 3. 模板权限控制(根据角色显示 / 隐藏元素)
        • (1)修改导航栏(`templates/base.html`)
        • (2)修改学生列表模板(`templates/student_list.html`)
    • 九、实战 7:测试权限控制
      • 1. 管理员登录(用户名:admin,密码:admin123)
      • 2. 创建普通用户并测试
    • 十、新手必踩的 5 个坑:认证与权限避坑指南
      • 坑 1:密码明文存储(未使用加密方法)
      • 坑 2:`current_user`使用时未初始化登录管理器
      • 坑 3:装饰器顺序错误(`@login_required`在`@app.route`上面)
      • 坑 4:表单提交报错 “CSRF token missing”
      • 坑 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

    # 安装Flask-Login(登录状态管理)
    pip install flask-login
    # 安装Flask-WTF(表单处理)
    pip install flask-wtf
    # Werkzeug已随Flask安装,无需额外安装(用于密码加密)

    安装完成后,验证是否成功:打开 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

    # 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

    # app.py(更新模型导入,原有代码修改)
    from models import User, Student, Course # 新增User

    步骤 2:创建 User 表(执行一次)

    打开 Python 终端,执行以下命令创建users表:

    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

    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

    # 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

    # 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

    <!– 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

    # 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

    # 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

    # 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

    # 新增学生路由:需要管理员权限
    @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

    # 首页路由:登录即可访问
    @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

    <!– 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

    <!– 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

    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

    # 错误示例:直接存储明文密码
    user = User(username='admin', password_hash='admin123') # 危险!

    解决:必须用set_password方法生成哈希:

    python

    user = User(username='admin')
    user.set_password('admin123') # 正确,存储哈希值

    坑 2:current_user使用时未初始化登录管理器

    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

    # 正确步骤:
    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

    # 错误示例:装饰器顺序错误,导致权限控制失效
    @login_required
    @app.route('/index') # 错误:@app.route应在最上面
    def index():
    pass

    解决:路由装饰器(@app.route)必须在最上面,权限装饰器在下面:

    python

    @app.route('/index')
    @login_required # 正确顺序
    def index():
    pass

    坑 4:表单提交报错 “CSRF token missing”

    python

    # 错误示例:登录表单中未添加form.hidden_tag()
    <form method="POST">
    {{ form.username() }}
    {{ form.password() }}
    {{ form.submit() }}
    </form>

    解决:必须添加form.hidden_tag()生成 CSRF 令牌:

    python

    <form method="POST">
    {{ form.hidden_tag() }} # 正确,添加CSRF保护
    {{ form.username() }}
    {{ form.password() }}
    {{ form.submit() }}
    </form>

    坑 5:未登录用户能直接访问受保护路由

    python

    # 错误示例:忘记添加@login_required装饰器
    @app.route('/student/list')
    def student_list(): # 未登录用户也能访问
    pass

    解决:所有核心路由必须添加@login_required(查询类)或@admin_required(增删改类):

    python

    @app.route('/student/list')
    @login_required # 正确,未登录用户会跳转
    def student_list():
    pass

    十一、小结与下一篇预告

    这篇你学到了什么?

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

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

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

    赞(0)
    未经允许不得转载:171主机测评 » Python 从入门到实战(十四):Flask 用户认证(给 Web 应用加安全锁,区分管理员与普通用户)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址