本章我们将为程序添加用户认证功能。这将把用户分成两类:管理员和访客。管理员可以通过用户名和密码登录程序,并执行与数据相关的操作;而访客只能浏览页面。在开始之前,我们先来了解如何将密码安全地存储到数据库中。

安全储存密码

把密码明文存储在数据库中是极其危险的,假如攻击者窃取了你的数据库,那么用户的账号和密码就会被直接泄露。更保险的方式是对每个密码进行计算生成独一无二的密码散列值,这样即使攻击者拿到了散列值,也几乎无法逆向获取到密码。

Flask 的依赖 Werkzeug 内置了用于生成和验证密码散列值的函数,werkzeug.security.generate_password_hash() 用来为给定的密码生成密码散列值,而 werkzeug.security.check_password_hash() 则用来检查给定的散列值和密码是否对应。使用示例如下所示:

>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> pw_hash = generate_password_hash('dog')  # 为密码 'dog' 生成密码散列值
>>> pw_hash  # 查看密码散列值
'pbkdf2:sha256:50000$mm9UPTRI$ee68ebc71434a4405a28d34ae3f170757fb424663dc0ca15198cb881edc0978f'
>>> check_password_hash(pw_hash, 'dog')  # 检查散列值是否对应密码 'dog'
True
>>> check_password_hash(pw_hash, 'cat')  # 检查散列值是否对应密码 'cat'
False

我们在存储用户信息的 User 模型类添加 username 字段和 password_hash 字段,分别用来存储登录所需的用户名和密码散列值,同时添加两个方法来实现设置密码和验证密码的功能:

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)  # 返回布尔值

因为模型(表结构)发生变化,我们需要重新生成数据库(这会清空数据):

(env) $ flask initdb --drop

生成管理员数据

因为程序只允许一个人使用,没有必要编写一个注册页面。我们可以编写一个命令来创建管理员账户,下面是实现这个功能的 admin() 函数:

import click

@app.cli.command()
@click.option('--username', prompt=True, help='The username used to login.')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='The password used to login.')
def admin(username, password):
    """Create user."""
    db.create_all()

    user = User.query.first()
    if user is not None:
        click.echo('Updating user...')
        user.username = username
        user.set_password(password)  # 设置密码
    else:
        click.echo('Creating user...')
        user = User(username=username, name='Admin')
        user.set_password(password)  # 设置密码
        db.session.add(user)

    db.session.commit()  # 提交数据库会话
    click.echo('Done.')

使用 click.option() 装饰器设置的两个选项分别用来接受输入用户名和密码。执行 flask admin 命令,输入用户名和密码后,即可创建管理员账户。如果执行这个命令时账户已存在,则更新相关信息:

(env) $ flask admin
Username: greyli
Password: 123  # hide_input=True 会让密码输入隐藏
Repeat for confirmation: 123  # confirmation_prompt=True 会要求二次确认输入
Updating user...
Done.

使用 Flask-Login 实现用户认证

Flask-Login 提供了实现用户认证所需的各类功能函数,我们将使用它来实现程序的用户认证。首先需要安装它:

(env) $ pip install flask-login

这个扩展的初始化步骤稍微有些不同,除了实例化扩展类之外,我们还要实现一个“用户加载回调函数”。具体代码如下所示:

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  # 返回用户对象

Flask-Login 提供了一个 current_user 变量。注册这个函数的目的是,当程序运行后,如果用户已登录,current_user 变量的值会是当前用户的用户模型类记录。

另一个步骤是让存储用户的 User 模型类继承 Flask-Login 提供的 UserMixin 类:

from flask_login import UserMixin

class User(db.Model, UserMixin):
    # ...

继承这个类会让 User 类拥有几个用于判断认证状态的属性和方法,其中最常用的是 is_authenticated 属性:如果当前用户已经登录,那么 current_user.is_authenticated 会返回 True,否则返回 False。有了 current_user 变量和这几个验证方法和属性,我们可以很轻松地判断当前用户的认证状态。

登录

用户登录使用 Flask-Login 提供的 login_user() 函数实现,需要传入用户模型类对象作为参数。下面是用于显示登录页面和处理登录表单提交请求的视图函数:

from flask_login import login_user

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        if not username or not password:
            flash('Invalid input.')
            return redirect(url_for('login'))

        user = User.query.first()
        # 验证用户名和密码是否一致
        if username == user.username and user.validate_password(password):
            login_user(user)  # 登入用户
            flash('Login success.')
            return redirect(url_for('index'))  # 重定向到主页

        flash('Invalid username or password.')  # 如果验证失败,显示错误消息
        return redirect(url_for('login'))  # 重定向回登录页面

    return render_template('login.html')

下面是包含登录表单的登录页面模板:

{% extends 'base.html' %}

{% block content %}
<h3>Login</h3>
<form method="post">
    Username<br>
    <input type="text" name="username" required><br><br>
    Password<br>
    <input type="password" name="password" required><br><br>
    <input class="btn" type="submit" name="submit" value="Submit">
</form>
{% endblock %}

在登录视图函数中,我们首先获取用户输入的用户名和密码。然后通过查询数据库获取用户对象,并使用 validate_password() 方法验证用户名和密码是否匹配。如果匹配成功,我们调用 login_user() 函数将用户登录,并重定向到主页;否则,显示错误消息,并重定向回登录页面。

登出

from flask_login import login_required, logout_user

@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('Goodbye.')
    return redirect(url_for('index'))

上述代码定义了一个 /logout 的路由,并使用 @login_required 装饰器进行视图保护,确保只有已登录的用户才能访问该路由。在视图函数中,调用 logout_user() 函数将当前用户登出,并显示登出成功的消息。最后,重定向回首页。

通过以上的代码实现了用户登录和登出功能后,可以将对应这两个视图函数的登录和登出链接放到导航栏上,以提供用户操作的入口。

认证保护

在Web程序中,可以使用认证保护来限制未登录用户访问某些页面或URL,并隐藏某些内容。下面介绍如何进行视图保护和模板内容保护。

视图保护: 对于不允许未登录用户访问的视图,可以使用@login_required装饰器进行保护。例如,在删除条目的视图函数中,可以添加@login_required装饰器,如下所示:

@app.route('/movie/delete/<int:movie_id>', methods=['POST'])
@login_required
def delete(movie_id):
    # 删除操作的代码

添加了@login_required装饰器后,如果未登录的用户访问对应的URL,Flask-Login会将用户重定向到登录页面,并显示错误提示。为了使重定向操作正常执行,还需要将login_manager.login_view的值设置为程序的登录视图端点(函数名),可以在login_manager实例定义下面添加如下代码:

login_manager.login_view = 'login'

模板内容保护: 认证保护的另一种形式是对模板内容的保护。可以根据用户的登录状态决定是否显示某些内容。例如,在首页模板(index.html)中,可以使用{% if ... %}{% endif %}进行条件判断,以控制是否渲染创建新条目表单的HTML代码,如下所示:

{% if current_user.is_authenticated %}
<form method="post">
    <!-- 创建新条目表单的HTML代码 -->
</form>
{% endif %}

在模板渲染时,会根据current_user.is_authenticated的值判断当前用户的登录状态,如果用户没有登录,就不会渲染表单部分的HTML代码。

类似地,可以使用条件判断来决定是否显示编辑和删除按钮等内容。例如:

{% if current_user.is_authenticated %}
    <!-- 编辑按钮和删除按钮的HTML代码 -->
{% endif %}

对于导航栏等需要根据登录状态显示不同内容的地方,可以在基模板(base.html)中使用条件判断。例如,如果用户已登录,则显示设置和登出链接,否则显示登录链接:

{% if current_user.is_authenticated %}
    <!-- 设置和登出链接的HTML代码 -->
{% else %}
    <!-- 登录链接的HTML代码 -->
{% endif %}

通过视图保护和模板内容保护,可以限制未登录用户的访问和隐藏特定内容,以实现认证保护的功能。

标签: flask教程, flask技术, flask学习, flask学习教程, flask下载, flask开发, flask入门教程, flask进阶教程, flask高级教程, flask面试题, flask笔试题, flask编程思想