第 6 章 · Flask-Login 认证与会话
本章目标:使用 Flask-Login 管理登录态;定义继承 UserMixin 的 User 模型;配置 LoginManager 与 user_loader;用 @login_required 保护 admin 商品管理路由;实现注册、登录、登出完整流程;理解 Session Cookie 与 remember 选项;与 Django LoginView 对照学习。
学时建议:4~5 小时(含 1.5 小时跟练)
前置:完成 flask-web ch05(Product/Category 已入库;User 本章新建)。
6.1 场景说明:谁能改商品?
api-demo 的 /admin/products/ 不能对匿名开放。与 shop-demo 的角色表类似,本章先实现单角色运营账号(is_staff),细粒度权限留作扩展。
| 路径 | 匿名 | 已登录 |
|---|---|---|
/auth/login | 可访问 | 跳转仪表盘 |
/auth/register | 可访问(练习环境) | 可关闭 |
/admin/products/* | 跳转登录 | 可 CRUD |
/api/v1/products | 可读(本章) | ch07 加 Token |
管理页:http://127.0.0.1:5000/admin/;对外 API 文档域名仍为虚构 https://api.example.com。
6.2 安装 Flask-Login
pip install "flask-login>=0.6,<1"
api_demo/extensions.py:
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
db = SQLAlchemy()
migrate = Migrate()
csrf = CSRFProtect()
login_manager = LoginManager()
api_demo/__init__.py:
from api_demo.extensions import csrf, db, login_manager, migrate
def create_app(config_name=None):
# ...
login_manager.init_app(app)
login_manager.login_view = "auth.login"
login_manager.login_message = "请先登录"
login_manager.login_message_category = "info"
# ...
| 配置 | 含义 |
|---|---|
login_view | 未登录时重定向的端点 |
login_message | flash 提示文案 |
6.3 User 模型与 UserMixin
api_demo/models/user.py:
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from api_demo.extensions import db
class User(UserMixin, db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(256), nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
is_staff = db.Column(db.Boolean, default=False, nullable=False)
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}>"
更新 api_demo/models/__init__.py:
from api_demo.models.category import Category
from api_demo.models.product import Product
from api_demo.models.user import User
__all__ = ["Category", "Product", "User"]
UserMixin 提供默认属性:is_authenticated、is_active、is_anonymous、get_id()。
迁移:
flask db migrate -m "add user"
flask db upgrade
6.4 LoginManager 与 user_loader
api_demo/__init__.py 或 api_demo/auth.py:
from api_demo.extensions import db, login_manager
from api_demo.models import User
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id)) # SQLAlchemy 2.x 推荐写法,替代旧的 Query.get
每个请求若 session 中有用户 id,load_user 会恢复 current_user。
from flask_login import current_user
# current_user.is_authenticated
# current_user.email
6.5 认证表单
api_demo/forms/auth.py:
from flask_wtf import FlaskForm
from wtforms import BooleanField, PasswordField, StringField
from wtforms.validators import DataRequired, Email, EqualTo, Length
class LoginForm(FlaskForm):
email = StringField("邮箱", validators=[DataRequired(), Email()])
password = PasswordField("密码", validators=[DataRequired()])
remember = BooleanField("记住我")
class RegisterForm(FlaskForm):
email = StringField("邮箱", validators=[DataRequired(), Email()])
password = PasswordField(
"密码",
validators=[DataRequired(), Length(min=8, message="至少 8 位")],
)
password2 = PasswordField(
"确认密码",
validators=[DataRequired(), EqualTo("password", message="两次密码不一致")],
)
6.6 auth 蓝图:登录、登出与注册
api_demo/routes/auth.py:
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required, login_user, logout_user
from api_demo.extensions import db
from api_demo.forms.auth import LoginForm, RegisterForm
from api_demo.models import User
bp = Blueprint("auth", __name__, url_prefix="/auth")
@bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated: