列表、表单、Flash 都做好以后,常见需求是:有些页面只能登录后访问,比如后台管理、个人备忘录。
这一节讲三件事:
- 用户表和密码怎么存
session是什么、登录后存什么- 用 装饰器 保护需要登录的路由
例子仍用通用的 Note 备忘录,不涉及任何真实业务。
1. 学完后你能做什么
- 建一个简单的 用户表
- 实现 登录 / 退出
- 用
session记住「当前是谁」 - 用装饰器 未登录就跳转登录页
- 登录成功后 跳回原来要去的页面
2. 先认识 Session
HTTP 本身 无状态:每次请求互相独立,服务器默认不知道「这是同一个用户」。
Flask 用 Session 解决这个问题:
- 登录成功后,在
session里存一个标记(比如user_id) - 浏览器收到加密的 Cookie,下次请求自动带上
- 视图里读
session.get("user_id")就知道是谁
app/__init__.py 里要有 SECRET_KEY(前面应该已经配过):
app.config"SECRET_KEY" = "dev-secret-key" # 生产环境务必换成随机长字符串
没有 SECRET_KEY,Session 无法安全签名。

3. 用户模型:密码不要明文存
app/models.py 增加 User:
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
tablename = "user"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
def set_password(self, raw: str) -> None:
self.password_hash = generate_password_hash(raw)
def check_password(self, raw: str) -> bool:
return check_password_hash(self.password_hash, raw)
要点:
- 数据库里存 哈希,不存明文密码
set_password注册或改密码时用check_password登录校验时用
改完模型后,记得跑迁移:
flask db migrate -m "add user table"
flask db upgrade
本地第一次也可以写个小脚本插入测试用户:
from app import app, db
from app.models import User
with app.app_context():
if not User.query.filter_by(username="admin").first():
u = User(username="admin")
u.set_password("123456")
db.session.add(u)
db.session.commit()
4. 登录表单
app/forms.py:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField("用户名", validators=DataRequired("请输入用户名"))
password = PasswordField("密码", validators=DataRequired("请输入密码"))
submit = SubmitField("登录")
模板 app/templates/home/login.html:
{% extends "base.html" %}
{% block title %}登录{% endblock %}
{% block content %}
<h1>登录</h1>
{% for msg in get_flashed_messages(category_filter='err') %}
<p class="flash-item flash-item--err">{{ msg }}</p>
{% endfor %}
<form method="post">
{{ form.csrf_token }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=30) }}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=30) }}
</p>
<p>{{ form.submit }}</p>
</form>
{% endblock %}
5. 登录与退出视图
app/home/views.py:
from flask import session, redirect, url_for, flash, request
from app.forms import LoginForm
from app.models import User
@home.route("/login/", methods="GET", "POST")
def login():
if session.get("user_id"):
return redirect(url_for("home.note_list"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data.strip()).first()
if user and user.check_password(form.password.data):
session"user_id" = user.id
session"username" = user.username
flash("登录成功", "ok")
next_url = request.args.get("next") or ""
if next_url.startswith("/"):
return redirect(next_url)
return redirect(url_for("home.note_list"))
flash("用户名或密码错误", "err")
return render_template("home/login.html", form=form)
@home.route("/logout/")
def logout():
session.clear()
flash("已退出登录", "ok")
return redirect(url_for("home.login"))
几个细节:
| 写法 | 作用 |
|---|---|
session["user_id"] = user.id |
登录成功,记住用户 |
session.clear() |
退出,清空 Session |
next 参数 |
登录后跳回原页面(见下文) |
next 只允许以 / 开头的站内路径,避免 开放重定向 漏洞。
6. 用装饰器保护路由
不想在每个视图里重复写:
if not session.get("user_id"):
return redirect(url_for("home.login"))
可以抽成装饰器。新建 app/auth_utils.py:
from functools import wraps
from flask import session, redirect, url_for, request
def login_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get("user_id"):
login_url = url_for("home.login", next=request.path)
return redirect(login_url)
return view(*args, **kwargs)
return wrapped
使用方式:
from app.auth_utils import login_required
@home.route("/notes/add/", methods="GET", "POST")
@login_required
def note_add():
...
需要登录的视图 加上 @login_required 即可:新增、编辑、删除通常要保护;公开列表可以不加,按产品需求决定。
7. 登录后跳回原页面
用户在 /notes/add/ 被拦到登录页时,URL 可以是:
/login/?next=/notes/add/
登录视图里已有处理:
next_url = request.args.get("next") or ""
if next_url.startswith("/"):
return redirect(next_url)
装饰器里生成链接:
login_url = url_for("home.login", next=request.path)
这样登录成功后会回到 本来要访问的页面,体验更顺。
8. 模板里显示当前用户
base.html 导航可以加:
<header class="site-header">
{% if session.get("username") %}
<span>你好,{{ session.username }}</span>
<a href="{{ url_for('home.logout') }}">退出</a>
{% else %}
<a href="{{ url_for('home.login') }}">登录</a>
{% endif %}
</header>
session 在模板里可直接读,不必每个视图都传 current_user(小项目够用;大了可以用 Flask-Login)。
9. 流程示意
访问 /notes/add/(未登录)
│
▼
@login_required 发现没有 user_id
│
▼
redirect → /login/?next=/notes/add/
│
▼
校验用户名密码 → session"user_id" = 1
│
▼
redirect → /notes/add/
│
▼
正常打开新增页
10. 新手常踩的 5 个坑
坑 1:密码明文入库
password = db.Column(db.String(64)) # 错
password_hash = ... + set_password() # 对
坑 2:忘了 SECRET_KEY
Session 不稳定或报错,检查 app.config["SECRET_KEY"]。
坑 3:装饰器顺序写反
@login_required # 应靠近函数
@home.route("/notes/") # 路由在外层
def note_list():
...
正确顺序:
@home.route("/notes/")
@login_required
def note_list():
...
路由在最外,装饰器在函数正上方。
坑 4:next 不做校验
return redirect(request.args.get("next")) # 危险
必须限制为站内路径:next.startswith("/")。
坑 5:每个视图复制粘贴登录判断
维护成本高,漏一个就漏保护。统一用装饰器。
11. 和 Flask-Login 的关系
小练习项目,Session + 装饰器 足够。
项目变大、要「记住我」、要加载当前用户对象时,可以用 Flask-Login 扩展。
核心思路不变:登录写入身份标识,访问受保护路由前检查身份。
12. 实际项目里怎么用
- 密码 只存哈希
- 受保护路由 统一装饰器,不要散落 if
- 登录失败用 Flash 的
err分类 next只允许站内路径- 生产环境 SECRET_KEY 用环境变量,不要写死在代码里
记住四件事:
User+ 密码哈希 --- 不存明文session["user_id"]--- 登录态@login_required--- 统一保护路由next参数 --- 登录后跳回原页