Flask-Login — 用户注册、登录、登出
本章你将学会用 Flask-Login 实现 Session 用户认证,这是 Flask Web 开发最重要的安全保障。
Flask-Login 是什么?
Web 应用的 HTTP 请求本质上是无状态的——服务器不会自动记住你是谁。
Flask-Login 通过 Session 机制实现用户状态的维护:登录后服务器在 Session 中存入用户 ID,后续请求自动识别当前用户。
安装与初始化
(venv) $ pip install flask-login
实例
# 文件路径:app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SECRET_KEY'] = 'your-secret-key-here' # Session 加密密钥(生产环境用环境变量)
db = SQLAlchemy(app)
# 初始化 LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login' # 未登录用户重定向到登录页
login_manager.login_message = '请先登录后再访问。'
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SECRET_KEY'] = 'your-secret-key-here' # Session 加密密钥(生产环境用环境变量)
db = SQLAlchemy(app)
# 初始化 LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login' # 未登录用户重定向到登录页
login_manager.login_message = '请先登录后再访问。'
SECRET_KEY是 Flask 安全体系的基石——Session 加密、CSRF Token、Flash 消息都依赖它。生产环境中必须从环境变量读取,不可硬编码。
定义 User 模型
Flask-Login 要求 User 模型实现 UserMixin 提供的几个方法。
实例
# 文件路径:models.py 新增
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model):
"""用户模型"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(256), 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}>'
# 定义 user_loader:Flask-Login 用此函数从 session 中恢复用户对象
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model):
"""用户模型"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(256), 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}>'
# 定义 user_loader:Flask-Login 用此函数从 session 中恢复用户对象
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
UserMixin 自动为 User 模型注入以下属性和方法:
| 属性/方法 | 说明 |
|---|---|
| is_authenticated | 用户是否已登录 |
| is_active | 用户是否处于活跃状态 |
| is_anonymous | 是否为匿名用户(未登录) |
| get_id() | 获取用户主键 ID |
生成迁移并执行:
(venv) $ flask db migrate -m "新增 User 模型" (venv) $ flask db upgrade
创建认证 Blueprint
实例
# 文件路径:app/blueprints/auth.py
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.models import User, db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/register", methods=['GET', 'POST'])
def register():
"""用户注册"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# 校验必填字段
if not username or not email or not password:
flash('所有字段都必须填写。', 'error')
return render_template('register.html')
# 检查用户名和邮箱是否已存在
if User.query.filter_by(username=username).first():
flash('用户名已被使用。', 'error')
return render_template('register.html')
if User.query.filter_by(email=email).first():
flash('邮箱已被注册。', 'error')
return render_template('register.html')
# 创建用户
user = User(username=username, email=email)
user.set_password(password) # 加密存储密码
db.session.add(user)
db.session.commit()
login_user(user) # 注册后自动登录
flash(f'注册成功,欢迎你,{username}!', 'success')
return redirect(url_for('main.index'))
return render_template('register.html')
@auth_bp.route("/login", methods=['GET', 'POST'])
def login():
"""用户登录"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
# user_loader 从数据库查找用户
user = User.query.filter_by(username=username).first()
# 验证密码(密码本身不会被记录到日志)
if user is None or not user.check_password(password):
flash('用户名或密码错误。', 'error')
return render_template('login.html')
login_user(user, remember=request.form.get('remember'))
flash(f'欢迎回来,{username}!', 'success')
# 登录后重定向到登录前访问的页面(URL 中 next 参数)
next_page = request.args.get('next')
return redirect(next_page or url_for('main.index'))
return render_template('login.html')
@auth_bp.route("/logout")
@login_required # 只有登录用户才能执行登出
def logout():
logout_user()
flash('已安全登出。', 'info')
return redirect(url_for('main.index'))
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.models import User, db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/register", methods=['GET', 'POST'])
def register():
"""用户注册"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# 校验必填字段
if not username or not email or not password:
flash('所有字段都必须填写。', 'error')
return render_template('register.html')
# 检查用户名和邮箱是否已存在
if User.query.filter_by(username=username).first():
flash('用户名已被使用。', 'error')
return render_template('register.html')
if User.query.filter_by(email=email).first():
flash('邮箱已被注册。', 'error')
return render_template('register.html')
# 创建用户
user = User(username=username, email=email)
user.set_password(password) # 加密存储密码
db.session.add(user)
db.session.commit()
login_user(user) # 注册后自动登录
flash(f'注册成功,欢迎你,{username}!', 'success')
return redirect(url_for('main.index'))
return render_template('register.html')
@auth_bp.route("/login", methods=['GET', 'POST'])
def login():
"""用户登录"""
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
# user_loader 从数据库查找用户
user = User.query.filter_by(username=username).first()
# 验证密码(密码本身不会被记录到日志)
if user is None or not user.check_password(password):
flash('用户名或密码错误。', 'error')
return render_template('login.html')
login_user(user, remember=request.form.get('remember'))
flash(f'欢迎回来,{username}!', 'success')
# 登录后重定向到登录前访问的页面(URL 中 next 参数)
next_page = request.args.get('next')
return redirect(next_page or url_for('main.index'))
return render_template('login.html')
@auth_bp.route("/logout")
@login_required # 只有登录用户才能执行登出
def logout():
logout_user()
flash('已安全登出。', 'info')
return redirect(url_for('main.index'))
在设计 App 中需要的引用
实例
# 文件路径:app.py 中注册 auth Blueprint
from app.blueprints.auth import auth_bp
app.register_blueprint(auth_bp)
from app.blueprints.auth import auth_bp
app.register_blueprint(auth_bp)
永远不要明文存储密码!
generate_password_hash()使用 PBKDF2 算法对密码做 salted hash。即使数据库泄露,攻击者也无法反向推导密码。校验密码时用check_password_hash()。
创建登录和注册模板
实例
<!-- 文件路径:app/templates/login.html -->
{% extends 'base.html' %}
{% block title %}登录 - RUNOOB 博客{% endblock %}
{% block content %}
<div class="auth-form">
<h2>登录</h2>
<form method="post">
<div class="form-group">
<label>用户名</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="remember"> 记住我
</label>
</div>
<button type="submit" class="btn-submit">登录</button>
</form>
<p class="form-footer">
还没有账号?<a href="{{ url_for('auth.register') }}">立即注册</a>
</p>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block title %}登录 - RUNOOB 博客{% endblock %}
{% block content %}
<div class="auth-form">
<h2>登录</h2>
<form method="post">
<div class="form-group">
<label>用户名</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="remember"> 记住我
</label>
</div>
<button type="submit" class="btn-submit">登录</button>
</form>
<p class="form-footer">
还没有账号?<a href="{{ url_for('auth.register') }}">立即注册</a>
</p>
</div>
{% endblock %}
更新导航栏显示登录状态
实例
<!-- 文件路径:app/templates/base.html 导航栏部分 -->
<nav>
<a href="/">首页</a>
{% if current_user.is_authenticated %}
<span class="user-name">{{ current_user.username }}</span>
<a href="{{ url_for('auth.logout') }}">登出</a>
{% else %}
<a href="{{ url_for('auth.login') }}">登录</a>
<a href="{{ url_for('auth.register') }}">注册</a>
{% endif %}
</nav>
<nav>
<a href="/">首页</a>
{% if current_user.is_authenticated %}
<span class="user-name">{{ current_user.username }}</span>
<a href="{{ url_for('auth.logout') }}">登出</a>
{% else %}
<a href="{{ url_for('auth.login') }}">登录</a>
<a href="{{ url_for('auth.register') }}">注册</a>
{% endif %}
</nav>
current_user是 Flask-Login 自动注入的代理对象,在所有模板和视图函数中都可以直接使用。登录状态下它是一个 User 对象,未登录时是 AnonymousUserMixin 实例。
本章小结
本章你接入了 Flask-Login Session 认证:UserMixin + user_loader 组合用户模型、login_user/logout_user 管理 Session、werkzeug 哈希密码、@login_required 保护视图、模板中 current_user 判断状态。
现在博客有了完整的用户注册、登录、登出流程。
