HTTP 无状态:两次请求互不相识。登录的本质是:第一次请求验证了"你是 alice",之后每个请求都能证明你是 alice——通过 session(服务端签名存 Cookie)携带身份。
直觉类比:登录像"办会员卡"——第一次出示身份证办卡(登录),之后每次出示会员卡(session)即可进入。Flask-Login 就是"会员卡系统":谁发卡、卡里记什么、怎么验卡、没卡的人去哪。
💡 关键直觉:Flask-Login 只管理"已登录是谁"这件事——具体账号密码怎么验证、注册逻辑,由你自己写;它负责把"当前用户"变成全局可访问的
current_user。
pip install flask-login
# extensions.py from flask_login import LoginManager login_manager = LoginManager() login_manager.login_view = 'login' # 未登录跳转的视图 login_manager.login_message = '请先登录' # 工厂中 login_manager.init_app(app)
每次请求 Flask-Login 从 session 里读到"用户 id"后,需要回调把它变成用户对象:
# models.py from flask_login import UserMixin from .extensions import db, login_manager class User(UserMixin, db.Model): """UserMixin 提供 is_authenticated/is_active 等标准方法""" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True) password_hash = db.Column(db.String(128)) def set_password(self, password): from werkzeug.security import generate_password_hash self.password_hash = generate_password_hash(password) def check_password(self, password): from werkzeug.security import check_password_hash return check_password_hash(self.password_hash, password) @login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id))
from flask_login import login_user, logout_user, login_required, current_user @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = User.query.filter_by(username=username).first() if user and user.check_password(password): login_user(user, remember='remember' in request.form) return redirect(url_for('profile')) return '用户名或密码错误', 401 return render_template('login.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('login')) @app.route('/profile') @login_required def profile(): return f'你好,{current_user.username}'
核心 API:
| 函数/对象 | 作用 |
|---|---|
login_user(user, remember=...) |
写入 session,标记已登录 |
logout_user() |
清除登录态 |
@login_required |
未登录则跳转 login_view |
current_user |
当前用户对象(未登录时是 AnonymousUser) |
current_user.is_authenticated |
是否已登录 |
# 正确:哈希后入库 user.set_password('mypassword123') # 校验 if user.check_password('mypassword123'): login_user(user)
为什么必须哈希:数据库一旦泄露,明文密码直接暴露;哈希后即使泄露,也无法反推原文(暴力破解成本极高)。永远不要自己写哈希算法,用 Werkzeug 的 generate_password_hash(默认 pbkdf2,自动加盐)。
{% if current_user.is_authenticated %} <p>欢迎,{{ current_user.username }}!</p> <a href="{{ url_for('logout') }}">登出</a> {% else %} <a href="{{ url_for('login') }}">登录</a> {% endif %}
@login_manager.unauthorized_handler def unauthorized(): """未登录访问受保护页面的统一处理""" if request.is_xhr or request.accept_mimetypes.best == 'application/json': return {'error': '未授权'}, 401 return redirect(url_for('login', next=request.url))
Flask-Login 只管"是否登录",**角色(admin/user)**自己加:
class User(UserMixin, db.Model): role = db.Column(db.String(20), default='user') def admin_required(view): @wraps(view) def wrapped(*args, **kwargs): if not current_user.is_authenticated or current_user.role != 'admin': abort(403) return view(*args, **kwargs) return wrapped @app.route('/admin') @login_required @admin_required def admin_panel(): return '管理后台'
remember=True 时 Flask-Login 写一个长期 Cookie(默认 365 天,可配置 REMEMBER_COOKIE_DURATION)。不想用记住我,可以配置:
app.config.update( REMEMBER_COOKIE_DURATION=timedelta(days=7), SESSION_COOKIE_SECURE=True, # HTTPS 才发送 SESSION_COOKIE_HTTPONLY=True, # 禁止 JS 读取,防 XSS 偷 Cookie )
| 误区 | 现象 | 正解 |
|---|---|---|
| 明文存密码 | 数据库泄露=全量密码泄露 | 哈希(pbkdf2/加盐) |
| 忘记 user_loader | 请求报"user not found"或匿名 | 注册 user_loader 回调 |
| 未设 login_view | 未登录访问 401 而非跳转 | 配置 login_manager.login_view |
| 自己拼 session 记登录态 | 安全漏洞、登出不清 | 用 login_user/logout_user |
| current_user 当普通变量用 | 报错或拿到匿名对象 | 先判 is_authenticated |
| 用 md5/sha1 哈希密码 | 秒破 | Werkzeug generate_password_hash |
# extensions.py from flask_login import LoginManager login_manager = LoginManager() login_manager.login_view = 'auth.login' # auth.py(蓝图) from flask import Blueprint, render_template, request, redirect, url_for from flask_login import login_user, logout_user, login_required, current_user from .models import User from .extensions import db auth = Blueprint('auth', __name__, url_prefix='/auth') @auth.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('main.index')) if request.method == 'POST': user = User.query.filter_by( username=request.form['username']).first() if user and user.check_password(request.form['password']): login_user(user) return redirect(url_for('main.index')) return render_template('login.html', error='用户名或密码错误') return render_template('login.html') @auth.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('auth.login'))
把这个蓝图注册进应用,登录系统就有了:登录、登出、会话保持、未登录拦截——全部由 Flask-Login 托底。
Flask-Login 只负责"登录态管理",一个完整的认证系统还有几块拼图。
第一,注册时的"用户输入清洗"。 注册是用户数据的入口,必须校验+清洗:用户名去空格、长度限制、禁止特殊字符(表单层做);邮箱唯一性检查(数据库约束 + 业务校验双保险);密码强度规则(长度、复杂度)。注册接口是攻击者最常试探的入口——第四章 4.2 安全章节会系统讲。
第二,密码哈希的算法演进。 Werkzeug 默认 pbkdf2(加盐、可调迭代次数),已足够安全。了解即可的进阶:bcrypt/scrypt/argon2 是更"慢"的哈希算法(慢=暴力破解成本高)。核心原则不变:用成熟库、自动加盐、绝不自己发明哈希、绝不存明文。数据库泄露时,哈希是你的最后防线——而"加盐+慢哈希"决定这条防线能撑多久。
第三,登录失败的限速与锁定。 无限尝试密码 = 暴力破解的机会。常见措施:同一 IP/账号 5 次失败后锁定一段时间;增加验证码;记录失败日志用于告警。Flask 生态用 Flask-Limiter 或自己实现计数(Redis/数据库)。登录接口默认就该带限速,这是防爆破的第一道闸。
第四,"记住我"的安全权衡。 remember=True 会写入长期有效的令牌 Cookie——方便但风险更高(Cookie 被偷 = 长期登录态被接管)。折中方案:记住我令牌设置合理过期(7-30 天)、绑定设备信息、支持"在所有设备上注销"。敏感应用(支付、后台)不建议开记住我。
第五,忘记密码的流程。 标准做法:用户提交邮箱 → 生成一次性重置令牌(带过期时间)→ 发邮件 → 用户点链接重置密码 → 重置后使旧令牌失效并强制重新登录。绝不能在邮件里发密码或"安全问答"——发一次性链接是最安全的模式。Flask 生态有 Flask-Security 等扩展封装这些,但理解流程比套扩展重要。
第六,登录后的授权(Authorization)。 认证(你是谁)之后是授权(你能干什么)。Flask-Login 管认证,授权靠角色/权限体系:简单场景用 is_admin 布尔字段 + 装饰器;复杂场景用角色表 + 权限表(RBAC)。第四章 4.1 的结构里,admin 蓝图配一个"仅管理员"的 before_request 钩子,就是授权的最小实现。认证与授权分开设计,是权限系统的第一原则。