news 2026/4/17 8:38:11

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/10 5:56:47

CRNN OCR模型压缩部署:在树莓派上运行OCR服务

CRNN OCR模型压缩部署&#xff1a;在树莓派上运行OCR服务 &#x1f4d6; 项目简介 随着边缘计算与物联网设备的普及&#xff0c;将AI能力下沉至终端设备成为提升响应速度、降低带宽成本的关键路径。OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&am…

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

智能视频生成技术实战:从原理到落地的完整指南

智能视频生成技术实战&#xff1a;从原理到落地的完整指南 【免费下载链接】imaginaire NVIDIAs Deep Imagination Teams PyTorch Library 项目地址: https://gitcode.com/gh_mirrors/im/imaginaire 在人工智能技术飞速发展的今天&#xff0c;视频生成技术正以前所未有的…

作者头像 李华
网站建设 2026/3/26 23:51:15

SOFTCNKILLER官网开发实战:从零到上线的完整流程

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 基于快马平台&#xff0c;开发一个完整的SOFTCNKILLER官网。要求包括首页、产品介绍、下载页面、技术支持、关于我们和联系方式等模块。使用AI生成初始代码后&#xff0c;手动调整…

作者头像 李华
网站建设 2026/4/16 22:40:47

EL-SELECT开发效率提升300%的AI技巧

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 请对比生成两份代码&#xff1a;1.传统手动编写的EL-SELECT组件&#xff08;包含远程搜索、多选、验证等功能&#xff09;&#xff1b;2.AI自动生成的同等功能组件。要求展示完整开…

作者头像 李华
网站建设 2026/4/16 18:26:35

Stable Diffusion WebUI完全攻略:5大核心模块深度拆解

Stable Diffusion WebUI完全攻略&#xff1a;5大核心模块深度拆解 【免费下载链接】stable-diffusion-webui AUTOMATIC1111/stable-diffusion-webui - 一个为Stable Diffusion模型提供的Web界面&#xff0c;使用Gradio库实现&#xff0c;允许用户通过Web界面使用Stable Diffusi…

作者头像 李华
网站建设 2026/4/16 12:30:36

Toggl Desktop 时间管理终极指南:告别时间浪费的完整教程

Toggl Desktop 时间管理终极指南&#xff1a;告别时间浪费的完整教程 【免费下载链接】toggldesktop Toggl Desktop app for Windows, Mac and Linux 项目地址: https://gitcode.com/gh_mirrors/to/toggldesktop 你是否曾经在一天结束时回顾&#xff0c;却发现自己完全不…

作者头像 李华