Flask 笔记九:登录、Session 与路由保护

列表、表单、Flash 都做好以后,常见需求是:有些页面只能登录后访问,比如后台管理、个人备忘录。

这一节讲三件事:

  1. 用户表和密码怎么存
  2. session 是什么、登录后存什么
  3. 用 装饰器 保护需要登录的路由

例子仍用通用的 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 用环境变量,不要写死在代码里

记住四件事:

  1. User + 密码哈希 --- 不存明文
  2. session["user_id"] --- 登录态
  3. @login_required --- 统一保护路由
  4. next 参数 --- 登录后跳回原页