<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Flask-Login &#8211; 李辉 / Grey Li</title>
	<atom:link href="https://greyli.com/tag/flask-login/feed/" rel="self" type="application/rss+xml" />
	<link>https://greyli.com</link>
	<description>一个编程和写作爱好者的在线记事本</description>
	<lastBuildDate>Thu, 06 Nov 2025 11:36:11 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.9.26</generator>

<image>
	<url>https://greyli.com/wp-content/uploads/2025/03/avatar-500-compressed-144x144.jpg</url>
	<title>Flask-Login &#8211; 李辉 / Grey Li</title>
	<link>https://greyli.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>《Flask 入门教程》第 8 章：用户认证</title>
		<link>https://greyli.com/flask-tutorial-chapter-8-user-authentication/</link>
		<comments>https://greyli.com/flask-tutorial-chapter-8-user-authentication/#respond</comments>
		<pubDate>Thu, 03 Jan 2019 10:53:15 +0000</pubDate>
		<dc:creator><![CDATA[李辉]]></dc:creator>
				<category><![CDATA[计算机与编程]]></category>
		<category><![CDATA[Flask]]></category>
		<category><![CDATA[Flask 入门教程]]></category>
		<category><![CDATA[Flask-Login]]></category>
		<category><![CDATA[用户认证]]></category>

		<guid isPermaLink="false">http://greyli.com/?p=2128</guid>
		<description><![CDATA[目前为止，虽然程序的功能大部分已经实现，但还缺少一个非常重要的部分&#8212;&#8212;用户认证保护。页 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>
	目前为止，虽然程序的功能大部分已经实现，但还缺少一个非常重要的部分&mdash;&mdash;用户认证保护。页面上的编辑和删除按钮是公开的，所有人都可以看到。假如我们现在把程序部署到网络上，那么任何人都可以执行编辑和删除条目的操作，这显然是不合理的。
</p>
<p>
	这一章我们会为程序添加用户认证功能，这会把用户分成两类，一类是管理员，通过用户名和密码登入程序，可以执行数据相关的操作；另一个是访客，只能浏览页面。在此之前，我们先来看看密码应该如何安全的存储到数据库中。
</p>
<h2>
	安全存储密码<br />
</h2>
<p>
	把密码明文存储在数据库中是极其危险的，假如攻击者窃取了你的数据库，那么用户的账号和密码就会被直接泄露。更保险的方式是对每个密码进行计算生成独一无二的密码散列值，这样即使攻击者拿到了散列值，也几乎无法逆向获取到密码。
</p>
<p>
	Flask 的依赖 Werkzeug 内置了用于生成和验证密码散列值的函数，<code>werkzeug.security.generate_password_hash()</code>&nbsp;用来为给定的密码生成密码散列值，而&nbsp;<code>werkzeug.security.check_password_hash()</code>&nbsp;则用来检查给定的散列值和密码是否对应。使用示例如下所示：
</p>
<pre>
&gt;&gt;&gt; from werkzeug.security import generate_password_hash, check_password_hash
&gt;&gt;&gt; pw_hash = generate_password_hash(&#39;dog&#39;)  # 为密码 dog 生成密码散列值
&gt;&gt;&gt; pw_hash  # 查看密码散列值
&#39;pbkdf2:sha256:50000$mm9UPTRI$ee68ebc71434a4405a28d34ae3f170757fb424663dc0ca15198cb881edc0978f&#39;
&gt;&gt;&gt; check_password_hash(pw_hash, &#39;dog&#39;)  # 检查散列值是否对应密码 dog
True
&gt;&gt;&gt; check_password_hash(pw_hash, &#39;cat&#39;)  # 检查散列值是否对应密码 cat
False</pre>
<p>
	我们在存储用户信息的&nbsp;<code>User</code>&nbsp;模型类添加&nbsp;<code>username</code>&nbsp;字段和&nbsp;<code>password_hash</code>&nbsp;字段，分别用来存储登录所需的用户名和密码散列值，同时添加两个方法来实现设置密码和验证密码的功能：
</p>
<pre>
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    username = db.Column(db.String(20))  # 用户名
    password_hash = db.Column(db.String(128))  # 密码散列值

    def set_password(self, password):  # 用来设置密码的方法，接受密码作为参数
        self.password_hash = generate_password_hash(password)  # 将生成的密码保持到对应字段

    def validate_password(self, password):  # 用于验证密码的方法，接受密码作为参数
        return check_password_hash(self.password_hash, password)  # 返回布尔值</pre>
<p>
	因为模型（表结构）发生变化，我们需要重新生成数据库（这会清空数据）：
</p>
<pre>
$ flask initdb --drop</pre>
<h2>
	生成管理员账户<br />
</h2>
<p>
	因为程序只允许一个人使用，没有必要编写一个注册页面。我们可以编写一个命令来创建管理员账户，下面是实现这个功能的&nbsp;<code>admin()</code>&nbsp;函数：
</p>
<pre>
import click

@app.cli.command()
@click.option(&#39;--username&#39;, prompt=True, help=&#39;The username used to login.&#39;)
@click.option(&#39;--password&#39;, prompt=True, hide_input=True, confirmation_prompt=True, help=&#39;The password used to login.&#39;)
def admin(username, password):
    &quot;&quot;&quot;Create user.&quot;&quot;&quot;
    db.create_all()

    user = User.query.first()
    if user is not None:
        click.echo(&#39;Updating user...&#39;)
        user.username = username
        user.set_password(password)  # 设置密码
    else:
        click.echo(&#39;Creating user...&#39;)
        user = User(username=username, name=&#39;Admin&#39;)
        user.set_password(password)  # 设置密码
        db.session.add(user)

    db.session.commit()  # 提交数据库会话
    click.echo(&#39;Done.&#39;)</pre>
<p>
	使用&nbsp;<code>click.option()</code>&nbsp;装饰器设置的两个选项分别用来接受输入用户名和密码。执行&nbsp;<code>flask admin</code>&nbsp;命令，输入用户名和密码后，即可创建管理员账户。如果执行这个命令时账户已存在，则更新相关信息：
</p>
<pre>
$ flask admin
Username: greyli
Password: 123  # hide_input=True 会让密码输入隐藏
Repeat for confirmation: 123  # confirmation_prompt=True 会要求二次确认输入
Updating user...
Done.</pre>
<h2>
	使用 Flask-Login 实现用户认证<br />
</h2>
<p>
	扩展&nbsp;<a href="https://github.com/maxcountryman/flask-login">Flask-Login</a>&nbsp;提供了实现用户认证需要的各类功能函数，我们将使用它来实现程序的用户认证，首先使用 Pipenv 安装它：
</p>
<pre>
$ pipenv install flask-login</pre>
<p>
	这个扩展的初始化步骤稍微有些不同，除了实例化扩展类之外，我们还要实现一个&ldquo;用户加载回调函数&rdquo;，具体代码如下所示：
</p>
<p>
	<em>app.py：初始化 Flask-Login</em>
</p>
<pre>
from flask_login import LoginManager

login_manager = LoginManager(app)  # 实例化扩展类


@login_manager.user_loader
def load_user(user_id):  # 创建用户加载回调函数，接受用户 ID 作为参数
    user = User.query.get(int(user_id))  # 用 ID 作为 User 模型的主键查询对应的用户
    return user  # 返回用户对象</pre>
<p>
	Flask-Login 提供了一个&nbsp;<code>current_user</code>&nbsp;变量，注册这个函数的目的是，当程序运行后，如果用户已登录，&nbsp;<code>current_user</code>&nbsp;变量的值会是当前用户的用户模型类记录。
</p>
<p>
	另一个步骤是让存储用户的 User 模型类继承 Flask-Login 提供的&nbsp;<code>UserMixin</code>&nbsp;类：
</p>
<pre>
from flask_login import UserMixin

class User(db.Model, UserMixin):
    # ...</pre>
<p>
	继承这个类会让&nbsp;<code>User</code>&nbsp;类拥有几个用于判断认证状态的属性和方法，其中最常用的是&nbsp;<code>is_authenticated</code>&nbsp;属性：如果当前用户已经登录，那么&nbsp;<code>current_user.is_authenticated</code>&nbsp;会返回&nbsp;<code>True</code>， 否则返回&nbsp;<code>False</code>。有了&nbsp;<code>current_user</code>&nbsp;变量和这几个验证方法和属性，我们可以很轻松的判断当前用户的认证状态。
</p>
<h2>
	登录<br />
</h2>
<p>
	登录用户使用 Flask-Login 提供的&nbsp;<code>login_user()</code>&nbsp;函数实现，需要传入用户模型类对象作为参数。下面是用于显示登录页面和处理登录表单提交请求的视图函数：
</p>
<p>
	<em>app.py：用户登录</em>
</p>
<pre>
from flask_login import login_user

# ...

@app.route(&#39;/login&#39;, methods=['GET', 'POST'])
def login():
    if request.method == &#39;POST&#39;:
        username = request.form['username']
        password = request.form['password']

        if not username or not password:
            flash(&#39;Invalid input.&#39;)
            return redirect(url_for(&#39;login&#39;))
        
        user = User.query.first()
        # 验证用户名和密码是否一致
        if username == user.username and user.validate_password(password):
            login_user(user)  # 登入用户
            flash(&#39;Login success.&#39;)
            return redirect(url_for(&#39;index&#39;))  # 重定向到主页

        flash(&#39;Invalid username or password.&#39;)  # 如果验证失败，显示错误消息
        return redirect(url_for(&#39;login&#39;))  # 重定向回登录页面
    
    return render_template(&#39;login.html&#39;)</pre>
<p>
	下面是包含登录表单的登录页面模板：
</p>
<p>
	<em>templates/login.html：登录页面</em>
</p>
<pre>
{% extends &#39;base.html&#39; %}

{% block content %}
&lt;h3&gt;Login&lt;/h3&gt;
&lt;form method=&quot;post&quot;&gt;
    Username&lt;br&gt;
    &lt;input type=&quot;text&quot; name=&quot;username&quot; required&gt;&lt;br&gt;&lt;br&gt;
    Password&lt;br&gt;
    &lt;!-- 密码输入框的 type 属性使用 password，会将输入值显示为圆点 --&gt;
    &lt;input type=&quot;password&quot; name=&quot;password&quot; required&gt;&lt;br&gt;&lt;br&gt;
    &lt;input class=&quot;btn&quot; type=&quot;submit&quot; name=&quot;submit&quot; value=&quot;Submit&quot;&gt;
&lt;/form&gt;
{% endblock %}</pre>
<h2>
	登出<br />
</h2>
<p>
	和登录相对，登出操作则需要调用&nbsp;<code>logout_user()</code>&nbsp;函数，使用下面的视图函数实现：
</p>
<pre>
from flask_login import login_required, logout_user

# ...

@app.route(&#39;/logout&#39;)
@login_required  # 用于视图保护，后面会详细介绍
def logout():
    logout_user()  # 登出用户
    flash(&#39;Goodbye.&#39;)
    return redirect(url_for(&#39;index&#39;))  # 重定向回首页</pre>
<p>
	实现了登录和登出后，我们先来看看认证保护，最后再把对应这两个视图函数的登录/登出链接放到导航栏上。
</p>
<h2>
	认证保护<br />
</h2>
<p>
	在 Web 程序中，有些页面或 URL 不允许未登录的用户访问，而页面上有些内容则需要对未登陆的用户隐藏，这就是认证保护。
</p>
<h3>
	视图保护<br />
</h3>
<p>
	在视图保护层面来说，未登录用户不能执行下面的操作：
</p>
<ul>
<li>
		访问编辑页面
	</li>
<li>
		访问设置页面
	</li>
<li>
		执行注销操作
	</li>
<li>
		执行删除操作
	</li>
<li>
		执行添加新条目操作
	</li>
</ul>
<p>
	对于不允许未登录用户访问的视图，只需要为视图函数附加一个&nbsp;<code>login_required</code>&nbsp;装饰器就可以将未登录用户拒之门外。以删除条目视图为例：
</p>
<pre>
@app.route(&#39;/movie/delete/&lt;int:movie_id&gt;&#39;, methods=['POST'])
@login_required  # 登录保护
def delete(movie_id):
    movie = Movie.query.get_or_404(movie_id)
    db.session.delete(movie)
    db.session.commit()
    flash(&#39;Item deleted.&#39;)
    return redirect(url_for(&#39;index&#39;))</pre>
<p>
	添加了这个装饰器后，如果未登录的用户访问对应的 URL，Flask-Login 会把用户重定向到登录页面，并显示一个错误提示。为了让这个重定向操作正确执行，我们还需要把&nbsp;<code>login_manager.login_view</code>&nbsp;的值设为我们程序的登录视图端点（函数名）：
</p>
<pre>
login_manager.login_view = &#39;login&#39;</pre>
<p>
	<strong>提示</strong>&nbsp;如果你需要的话，可以通过设置&nbsp;<code>login_manager.login_message</code>&nbsp;来自定义错误提示消息。
</p>
<p>
	编辑视图同样需要附加这个装饰器：
</p>
<pre>
@app.route(&#39;/movie/edit/&lt;int:movie_id&gt;&#39;, methods=['GET', 'POST'])
@login_required
def edit(movie_id):
    # ...</pre>
<p>
	创建新条目的操作稍微有些不同，因为对应的视图同时处理显示页面的 GET 请求和创建新条目的 POST 请求，我们仅需要禁止未登录用户创建新条目，因此不能使用&nbsp;<code>login_required</code>，而是在函数内部的 POST 请求处理代码前进行过滤：
</p>
<pre>
from flask_login import login_required, current_user

# ...

@app.route(&#39;/&#39;, methods=['GET', 'POST'])
def index():
    if request.method == &#39;POST&#39;:
        if not current_user.is_authenticated:  # 如果当前用户未认证
            return redirect(url_for(&#39;index&#39;))  # 重定向到主页
        # ...</pre>
<p>
	最后，我们为程序添加一个设置页面，支持修改用户的名字：
</p>
<p>
	<em>app.py：支持设置用户名字</em>
</p>
<pre>
from flask_login import login_required, current_user

# ...

@app.route(&#39;/settings&#39;, methods=['GET', 'POST'])
@login_required
def settings():
    if request.method == &#39;POST&#39;:
        name = request.form['name']
        
        if not name or len(name) &gt; 20:
            flash(&#39;Invalid input.&#39;)
            return redirect(url_for(&#39;settings&#39;))
        
        current_user.name = name
        # current_user 会返回当前登录用户的数据库记录对象
        # 等同于下面的用法
        # user = User.query.first()
        # user.name = name
        db.session.commit()
        flash(&#39;Settings updated.&#39;)
        return redirect(url_for(&#39;index&#39;))
    
    return render_template(&#39;settings.html&#39;)</pre>
<p>
	下面是对应的模板：
</p>
<p>
	<em>templates/settings.html：设置页面模板</em>
</p>
<pre>
{% extends &#39;base.html&#39; %}

{% block content %}
&lt;h3&gt;Settings&lt;/h3&gt;
&lt;form method=&quot;post&quot;&gt;
    Your Name &lt;input type=&quot;text&quot; name=&quot;name&quot; autocomplete=&quot;off&quot; required value=&quot;{{ current_user.name }}&quot;&gt;
    &lt;input class=&quot;btn&quot; type=&quot;submit&quot; name=&quot;submit&quot; value=&quot;Save&quot;&gt;
&lt;/form&gt;
{% endblock %}</pre>
<h3>
	模板内容保护<br />
</h3>
<p>
	认证保护的另一形式是页面模板内容的保护。比如，不能对未登录用户显示下列内容：
</p>
<ul>
<li>
		创建新条目表单
	</li>
<li>
		编辑按钮
	</li>
<li>
		删除按钮
	</li>
</ul>
<p>
	这几个元素的定义都在首页模板（index.html）中，以创建新条目表单为例，我们在表单外部添加一个&nbsp;<code>if</code>&nbsp;判断：
</p>
<pre>
&lt;!-- 在模板中可以直接使用 current_user 变量 --&gt;
{% if current_user.is_authenticated %}
&lt;form method=&quot;post&quot;&gt;
    Name &lt;input type=&quot;text&quot; name=&quot;title&quot; autocomplete=&quot;off&quot; required&gt;
    Year &lt;input type=&quot;text&quot; name=&quot;year&quot; autocomplete=&quot;off&quot; required&gt;
    &lt;input class=&quot;btn&quot; type=&quot;submit&quot; name=&quot;submit&quot; value=&quot;Add&quot;&gt;
&lt;/form&gt;
{% endif %}</pre>
<p>
	在模板渲染时，会先判断当前用户的登录状态（<code>current_user.is_authenticated</code>）。如果用户没有登录（<code>current_user.is_authenticated</code>&nbsp;返回&nbsp;<code>False</code>），就不会渲染表单部分的 HTML 代码，即上面代码块中&nbsp;<code>{% if ... %}</code>&nbsp;和&nbsp;<code>{% endif %}</code>&nbsp;之间的代码。类似的还有编辑和删除按钮：
</p>
<pre>
{% if current_user.is_authenticated %}
	&lt;a class=&quot;btn&quot; href=&quot;{{ url_for(&#39;edit&#39;, movie_id=movie.id) }}&quot;&gt;Edit&lt;/a&gt;
	&lt;form class=&quot;inline-form&quot; method=&quot;post&quot; action=&quot;{{ url_for(&#39;.delete&#39;, movie_id=movie.id) }}&quot;&gt;
		&lt;input class=&quot;btn&quot; type=&quot;submit&quot; name=&quot;delete&quot; value=&quot;Delete&quot; onclick=&quot;return confirm(&#39;Are you sure?&#39;)&quot;&gt;
	&lt;/form&gt;
{% endif %}</pre>
<p>
	有些地方则需要根据登录状态分别显示不同的内容，比如基模板（base.html）中的导航栏。如果用户已经登录，就显示设置和登出链接，否则显示登录链接：
</p>
<pre>
{% if current_user.is_authenticated %}
	&lt;li&gt;&lt;a href=&quot;{{ url_for(&#39;settings&#39;) }}&quot;&gt;Settings&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href=&quot;{{ url_for(&#39;logout&#39;) }}&quot;&gt;Logout&lt;/a&gt;&lt;/li&gt;
{% else %}
	&lt;li&gt;&lt;a href=&quot;{{ url_for(&#39;login&#39;) }}&quot;&gt;Login&lt;/a&gt;&lt;/li&gt;
{% endif %}</pre>
<p>
	现在的程序中，未登录用户看到的主页如下所示：
</p>
<p>
	<a href="https://github.com/greyli/flask-tutorial/blob/master/chapters/images/8-1.png" rel="noopener noreferrer" target="_blank"><img alt="对未登录用户显示的主页" src="https://github.com/greyli/flask-tutorial/raw/master/chapters/images/8-1.png" /></a>
</p>
<p>
	在登录页面，输入用户名和密码登入：
</p>
<p>
	<a href="https://github.com/greyli/flask-tutorial/blob/master/chapters/images/8-2.png" rel="noopener noreferrer" target="_blank"><img alt="登录" src="https://github.com/greyli/flask-tutorial/raw/master/chapters/images/8-2.png" /></a>
</p>
<p>
	登录后看到的主页如下所示：
</p>
<p>
	<a href="https://github.com/greyli/flask-tutorial/blob/master/chapters/images/8-3.png" rel="noopener noreferrer" target="_blank"><img alt="对已登录用户显示的主页" src="https://github.com/greyli/flask-tutorial/raw/master/chapters/images/8-3.png" /></a>
</p>
<h2>
	本章小结<br />
</h2>
<p>
	添加用户认证后，在功能层面，我们的程序基本算是完成了。结束前，让我们提交代码：
</p>
<pre>
$ git add .
$ git commit -m &quot;User authentication with Flask-Login&quot;
$ git push</pre>
<p>
	<strong>提示</strong> 你可以在 GitHub 上查看本书示例程序的对应 commit：<a href="https://github.com/greyli/watchlist/commit/6c60b7d552921cb758e716de567e76f3a1ea578e" spellcheck="false">6c60b7d</a>。
</p>
<h2>
	进阶提示<br />
</h2>
<ul>
<li>
		访问&nbsp;<a href="https://github.com/maxcountryman/flask-login">Flask-Login 文档</a>了解更多细节和用法。
	</li>
<li>
		<a href="http://helloflask.com/book/" rel="nofollow">《Flask Web 开发实战》</a>第 2 章通过一个示例介绍了用户认证的实现方式；第 8 章包含对 Flask-Login 更详细的介绍。
	</li>
<li>
		本书主页 &amp; 相关资源索引：<a data-za-detail-view-id="1043" href="http://helloflask.com/tutorial" rel="nofollow noreferrer" target="_blank">http://helloflask.com/tutorial</a>。
	</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>https://greyli.com/flask-tutorial-chapter-8-user-authentication/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
