news 2026/4/23 17:42:23

Python Web 开发进阶实战:Flask-Login 用户认证与权限管理 —— 构建多用户待办事项系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python Web 开发进阶实战:Flask-Login 用户认证与权限管理 —— 构建多用户待办事项系统

第一章:为什么需要用户系统?

在真实场景中,Web 应用几乎都离不开用户身份识别:

  • 数据隔离:张三的任务不能被李四看到
  • 个性化体验:记住用户偏好、历史记录
  • 操作审计:谁在何时做了什么
  • 商业闭环:用户是产品运营的基础单元

然而,自行实现用户系统极易出错

  • 明文存储密码 → 数据库泄露即全盘沦陷
  • 会话劫持(Session Hijacking)
  • 跨站请求伪造(CSRF)
  • 暴力破解登录

解决方案:使用成熟库Flask-Login+ 安全最佳实践。


第二章:设计用户模型(User Model)

2.1 用户字段规划

一个基础但安全的用户模型应包含:

字段类型说明
idInteger主键
usernameString(50)用户名(唯一)
emailString(120)邮箱(唯一,用于找回密码)
password_hashString(255)密码哈希值(绝不存明文!)
created_atDateTime注册时间

为什么不存明文密码?
即使数据库被拖库,攻击者也无法直接获取用户密码(需暴力破解哈希)。

2.2 实现 User 模型

更新models.py

from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db = SQLAlchemy() class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False, index=True) email = db.Column(db.String(120), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 关联任务(一对多) todos = db.relationship('Todo', backref='author', lazy='dynamic', cascade='all, delete-orphan') def set_password(self, password): """设置密码(自动哈希)""" self.password_hash = generate_password_hash(password) def check_password(self, password): """验证密码""" return check_password_hash(self.password_hash, password) def __repr__(self): return f'<User {self.username}>'

关键点解析

  • generate_password_hash():使用 PBKDF2 算法(默认)生成强哈希
  • check_password_hash():安全比对哈希值
  • cascade='all, delete-orphan':当用户删除时,自动清理其所有任务
  • backref='author':在Todo对象中可通过todo.author访问用户

安全提示:Werkzeug 默认使用pbkdf2:sha256,迭代次数 150,000+,足够抵御彩虹表攻击。

2.3 更新 Todo 模型以关联用户

修改Todo类,添加外键:

class Todo(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) done = db.Column(db.Boolean, default=False, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # === 新增:用户外键 === user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) __table_args__ = (db.Index('idx_title', 'title'),)

注意nullable=False确保每条任务必须属于某个用户。


第三章:集成 Flask-Login

3.1 安装与初始化

pip install Flask-Login

更新requirements.txt

Flask-Login==0.6.3

创建extensions.py(若尚未创建):

# extensions.py from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect login_manager = LoginManager() csrf = CSRFProtect()

app.py中初始化:

# app.py from flask import Flask from config import config from models import db from extensions import login_manager, csrf # 新增导入 def create_app(config_name='default'): app = Flask(__name__) app.config.from_object(config[config_name]) db.init_app(app) csrf.init_opt(app) # === 初始化 Flask-Login === login_manager.init_app(app) login_manager.login_view = 'auth.login' # 未登录时重定向到此视图 login_manager.login_message = "请先登录以访问该页面" login_manager.login_message_category = "warning" # ... 其他初始化 ... return app

3.2 实现用户加载回调

Flask-Login 需要知道如何从 session 中加载用户对象。

models.py末尾添加:

# models.py (底部) @login_manager.user_loader def load_user(user_id): """根据用户ID加载用户对象""" return User.query.get(int(user_id))

原理:登录成功后,Flask-Login 将user.id存入 session;后续请求通过此函数还原current_user


第四章:构建认证路由(Auth Blueprint)

为保持结构清晰,我们将认证相关路由放入独立蓝图。

4.1 创建 auth 蓝图目录

flask-todo-layui/ ├── routes/ │ ├── __init__.py │ ├── main.py # 原待办事项路由 │ └── auth.py # 新增:认证路由 └── ...

4.2 设计认证表单

新建forms.py(或扩展现有文件):

# forms.py from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError from models import User class LoginForm(FlaskForm): username = StringField('用户名', validators=[DataRequired(), Length(1, 50)]) password = PasswordField('密码', validators=[DataRequired()]) submit = SubmitField('登录') class RegistrationForm(FlaskForm): username = StringField('用户名', validators=[ DataRequired(), Length(3, 50, message='用户名需3-50字符') ]) email = StringField('邮箱', validators=[ DataRequired(), Email(message='请输入有效邮箱地址') ]) password = PasswordField('密码', validators=[ DataRequired(), Length(6, 128, message='密码至少6位') ]) password2 = PasswordField('确认密码', validators=[ DataRequired(), EqualTo('password', message='两次密码不一致') ]) submit = SubmitField('注册') def validate_username(self, username): if User.query.filter_by(username=username.data).first(): raise ValidationError('用户名已存在') def validate_email(self, email): if User.query.filter_by(email=email.data).first(): raise ValidationError('邮箱已被注册')

安全增强

  • 用户名/邮箱唯一性校验
  • 密码二次确认
  • 邮箱格式验证

4.3 实现注册与登录视图

routes/auth.py

from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_user, logout_user, current_user from models import db, User from forms import LoginForm, RegistrationForm auth = Blueprint('auth', __name__) @auth.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('main.index')) form = RegistrationForm() if form.validate_on_submit(): user = User(username=form.username.data, email=form.email.data) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash('注册成功!请登录。', 'success') return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form) @auth.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('main.index')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.username.data).first() if user is None or not user.check_password(form.password.data): flash('用户名或密码错误', 'error') return redirect(url_for('auth.login')) login_user(user, remember=True) # 启用“记住我” next_page = request.args.get('next') if next_page: return redirect(next_page) return redirect(url_for('main.index')) return render_template('auth/login.html', form=form) @auth.route('/logout') def logout(): logout_user() flash('您已退出登录', 'info') return redirect(url_for('main.index'))

关键逻辑

  • current_user.is_authenticated:判断是否已登录
  • login_user(user, remember=True):启动会话,并设置持久化 cookie(默认 365 天)
  • next参数:登录后跳转回原请求页面(如/add需登录)

第五章:创建认证模板

5.1 基础布局继承

复用base.html,确保风格统一。

5.2 注册页面templates/auth/register.html

{% extends "base.html" %} {% block title %}用户注册{% endblock %} {% block header %}创建新账户{% endblock %} {% block content %} <div style="max-width: 500px; margin: 30px auto;"> <form method="POST"> {{ form.hidden_tag() }} <div class="layui-form-item"> <label class="layui-form-label">用户名</label> <div class="layui-input-block"> {{ form.username(class="layui-input") }} {% if form.username.errors %} <div class="layui-form-mid layui-text" style="color:#FF5722;">{{ form.username.errors[0] }}</div> {% endif %} </div> </div> <div class="layui-form-item"> <label class="layui-form-label">邮箱</label> <div class="layui-input-block"> {{ form.email(class="layui-input") }} {% if form.email.errors %} <div class="layui-form-mid layui-text" style="color:#FF5722;">{{ form.email.errors[0] }}</div> {% endif %} </div> </div> <div class="layui-form-item"> <label class="layui-form-label">密码</label> <div class="layui-input-block"> {{ form.password(class="layui-input") }} {% if form.password.errors %} <div class="layui-form-mid layui-text" style="color:#FF5722;">{{ form.password.errors[0] }}</div> {% endif %} </div> </div> <div class="layui-form-item"> <label class="layui-form-label">确认密码</label> <div class="layui-input-block"> {{ form.password2(class="layui-input") }} {% if form.password2.errors %} <div class="layui-form-mid layui-text" style="color:#FF5722;">{{ form.password2.errors[0] }}</div> {% endif %} </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> {{ form.submit(class="layui-btn") }} <a href="{{ url_for('auth.login') }}" class="layui-btn layui-btn-primary">已有账户?去登录</a> </div> </div> </form> </div> {% endblock %}

5.3 登录页面templates/auth/login.html

{% extends "base.html" %} {% block title %}用户登录{% endblock %} {% block header %}欢迎回来{% endblock %} {% block content %} <div style="max-width: 400px; margin: 50px auto;"> <form method="POST"> {{ form.hidden_tag() }} <div class="layui-form-item"> <div class="layui-input-block"> {{ form.username(placeholder="用户名", class="layui-input") }} {% if form.username.errors %} <div class="layui-form-mid layui-text" style="color:#FF5722;">{{ form.username.errors[0] }}</div> {% endif %} </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> {{ form.password(placeholder="密码", class="layui-input") }} {% if form.password.errors %} <div class="layui-form-mid layui-text" style="color:#FF5722;">{{ form.password.errors[0] }}</div> {% endif %} </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> {{ form.submit(class="layui-btn", value="登录") }} <a href="{{ url_for('auth.register') }}" class="layui-btn layui-btn-primary">没有账户?去注册</a> </div> </div> </form> </div> {% endblock %}

第六章:改造主应用以支持多用户

6.1 在首页显示当前用户

更新templates/base.html的导航栏:

<!-- 在 header 区域添加 --> <div style="float: right; margin-top: 15px; color: #666;"> {% if current_user.is_authenticated %} 欢迎, {{ current_user.username }}! <a href="{{ url_for('auth.logout') }}" class="layui-btn layui-btn-xs layui-btn-primary">退出</a> {% else %} <a href="{{ url_for('auth.login') }}" class="layui-btn layui-btn-xs">登录</a> <a href="{{ url_for('auth.register') }}" class="layui-btn layui-btn-xs layui-btn-primary">注册</a> {% endif %} </div>

注意current_user是 Flask-Login 提供的全局代理对象,可在模板中直接使用。

6.2 限制任务操作仅限本人

修改routes/main.py

from flask_login import login_required, current_user # 新增导入 @main.route('/', methods=['GET', 'POST']) @login_required # 必须登录才能访问 def index(): form = TodoForm() if form.validate_on_submit(): title = form.title.data.strip() # === 关键:绑定当前用户 === new_todo = Todo(title=title, author=current_user) db.session.add(new_todo) db.session.commit() flash('任务添加成功!', 'success') return redirect(url_for('main.index')) # 仅查询当前用户的任务 query_str = request.args.get('q', '').strip() todos_query = Todo.query.filter_by(author=current_user) if query_str: todos_query = todos_query.filter(Todo.title.contains(query_str)) todos_query = todos_query.order_by(Todo.created_at.desc()) page = request.args.get('page', 1, type=int) pagination = todos_query.paginate(page=page, per_page=10, error_out=False) todos = pagination.items return render_template( 'index.html', form=form, todos=todos, search_query=query_str, pagination=pagination ) @main.route('/delete/<int:todo_id>', methods=['POST']) @login_required def delete_todo(todo_id): todo = Todo.query.get_or_404(todo_id) # === 安全检查:只能删除自己的任务 === if todo.author != current_user: flash('无权操作他人任务', 'error') return redirect(url_for('main.index')) db.session.delete(todo) db.session.commit() flash('任务已删除', 'info') return redirect(url_for('main.index')) @main.route('/complete_all', methods=['POST']) @login_required def complete_all(): # 仅标记当前用户任务为完成 Todo.query.filter_by(author=current_user).update({Todo.done: True}) db.session.commit() flash('所有任务已标记为完成', 'success') return redirect(url_for('main.index'))

安全加固点

  • @login_required:强制登录
  • filter_by(author=current_user):数据隔离
  • 删除前校验todo.author == current_user:防止 ID 猜测攻击

第七章:会话安全深度加固

7.1 配置安全 Cookie

config.pyConfig类中添加:

class Config: # ... 其他配置 ... REMEMBER_COOKIE_SECURE = True # 仅 HTTPS 传输(生产环境) REMEMBER_COOKIE_HTTPONLY = True # 禁止 JS 访问 REMEMBER_COOKIE_SAMESITE = 'Lax' # 防 CSRF SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax'

开发环境注意:本地 HTTP 测试时需临时设为False,否则“记住我”失效。

7.2 防止会话固定攻击(Session Fixation)

Flask-Login 默认在登录时更换 session ID,已内置防护。

7.3 密码强度策略(可选)

RegistrationForm中增加自定义验证:

import re def validate_password(self, password): if len(re.findall(r'[A-Z]', password.data)) == 0: raise ValidationError('密码需包含至少一个大写字母') if len(re.findall(r'\d', password.data)) == 0: raise ValidationError('密码需包含至少一个数字')

第八章:测试用户系统

8.1 测试注册流程

tests/test_auth.py

def test_register(client): """测试用户注册""" response = client.post('/register', data={ 'username': 'testuser', 'email': 'test@example.com', 'password': 'SecurePass123', 'password2': 'SecurePass123', 'submit': '注册' }, follow_redirects=True) assert response.status_code == 200 assert b'注册成功' in response.data # 验证用户已存入数据库 with client.application.app_context(): user = User.query.filter_by(username='testuser').first() assert user is not None assert user.check_password('SecurePass123') def test_register_duplicate_username(client): """测试重复用户名""" # 先注册一次 client.post('/register', data={ 'username': 'duplicate', 'email': 'dup1@example.com', 'password': 'Pass123', 'password2': 'Pass123' }) # 再次注册相同用户名 response = client.post('/register', data={ 'username': 'duplicate', 'email': 'dup2@example.com', 'password': 'Pass123', 'password2': 'Pass123' }) assert b'用户名已存在' in response.data

8.2 测试登录与权限

def test_login_logout(client): """测试登录登出""" # 先注册 client.post('/register', data={ 'username': 'logintest', 'email': 'login@test.com', 'password': 'LoginPass123', 'password2': 'LoginPass123' }) # 登录 response = client.post('/login', data={ 'username': 'logintest', 'password': 'LoginPass123' }, follow_redirects=True) assert b'欢迎, logintest!' in response.data # 登出 response = client.get('/logout', follow_redirects=True) assert b'您已退出登录' in response.data assert b'登录' in response.data def test_protected_route_requires_login(client): """测试未登录访问首页被重定向""" response = client.get('/') assert response.status_code == 302 assert '/login' in response.location

8.3 测试数据隔离

def test_todo_isolation(client): """测试任务数据隔离""" # 创建两个用户 client.post('/register', data={'username':'user1', 'email':'u1@test.com', 'password':'Pass123', 'password2':'Pass123'}) client.post('/login', data={'username':'user1', 'password':'Pass123'}) client.post('/', data={'title': 'User1 Task'}) client.get('/logout') client.post('/register', data={'username':'user2', 'email':'u2@test.com', 'password':'Pass123', 'password2':'Pass123'}) client.post('/login', data={'username':'user2', 'password':'Pass123'}) client.post('/', data={'title': 'User2 Task'}) # user2 的首页不应看到 user1 的任务 response = client.get('/') assert b'User2 Task' in response.data assert b'User1 Task' not in response.data

第九章:部署前的最终检查清单

项目状态说明
✅ 密码哈希存储使用generate_password_hash
✅ 会话 Cookie 安全HttpOnly+Secure+SameSite
✅ 数据隔离所有查询过滤author=current_user
✅ 权限校验删除前验证任务归属
✅ CSRF 防护Flask-WTF自动启用
✅ 错误页面不泄露信息自定义 404/500
✅ 自动化测试覆盖注册/登录/权限均有测试

总结:从单机到多用户的质变

通过本篇,你的待办事项系统完成了关键跃迁:

  • 身份认证:安全注册/登录,密码强哈希
  • 数据隔离:每个用户拥有独立任务空间
  • 权限控制:操作前校验所有权
  • 会话安全:防御常见 Web 攻击
  • 测试保障:核心流程 100% 覆盖

现在,它已是一个具备生产级安全性的多用户 Web 应用

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 9:33:51

企业级应用中Diffie-Hellman漏洞的实际影响与防护

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个Diffie-Hellman密钥交换协议漏洞演示系统&#xff0c;展示CVE-2002-20001资源管理错误漏洞的实际攻击场景。系统应包含&#xff1a;1. 有漏洞的协议实现 2. 漏洞利用演示 …

作者头像 李华
网站建设 2026/4/23 9:32:44

国足新名单暗藏逆袭故事,“青岛姆巴佩”改换门庭

2026年伊始&#xff0c;中国足球的齿轮在冬日里开始加速转动。当邵佳一作为国足新帅首次亮相&#xff0c;公布26人集训名单时&#xff0c;段刘愚的名字如同一颗被重新擦亮的宝石&#xff0c;引人注目。与此同时&#xff0c;转会市场的波澜同样汹涌&#xff1a;“青岛姆巴佩”韦…

作者头像 李华
网站建设 2026/4/23 9:33:50

用STATA快速验证研究假设:原型开发指南

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个STATA研究假设验证工具&#xff0c;能够&#xff1a;1) 根据研究问题自动设计最小可行分析方案 2) 生成模拟数据模板 3) 提供快速可视化功能 4) 输出初步统计结论。要求支…

作者头像 李华
网站建设 2026/4/23 11:03:59

Pannellum全景技术实战:企业级部署与性能优化完整指南

Pannellum全景技术实战&#xff1a;企业级部署与性能优化完整指南 【免费下载链接】pannellum Pannellum is a lightweight, free, and open source panorama viewer for the web. 项目地址: https://gitcode.com/gh_mirrors/pa/pannellum 在当今数字体验时代&#xff0…

作者头像 李华
网站建设 2026/4/23 11:07:04

Python环境变量完全指南:从零开始到实际应用

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个交互式Python学习工具&#xff0c;通过图形界面逐步引导新手学习环境变量&#xff1a;1. 基础概念讲解 2. 不同操作系统设置演示 3. Python中os.environ的使用示例 4. 常见…

作者头像 李华
网站建设 2026/4/23 1:08:21

直播源聚合革命:allinone_format智能管理完全指南

直播源聚合革命&#xff1a;allinone_format智能管理完全指南 【免费下载链接】allinone_format 本项目是对 https://hub.docker.com/r/youshandefeiyang/allinone /tv.m3u、/tptv.m3u、/migu.m3u 进行聚合 & 重新分组。 项目地址: https://gitcode.com/gh_mirrors/al/al…

作者头像 李华