蓝图和视图

视图函数是你编写用来回应发送到应用的请求的代码。Flask 使用模式来匹配传入的请求 URL 到应该处理它的视图。视图返回的数据会被 Flask 转换为发出的响应。Flask 也可以重定向到其他视图并根据视图名称和参数生成到某一个视图的 URL。

创建一个蓝图

蓝图(Blueprint)是一种组织一组相关视图和其他代码的方式。视图和其他代码注册到蓝图,而不是直接注册到应用上。然后,在工厂函数中应用可用时,蓝图会被注册到应用。

Flaskr 将会有两个蓝图,一个用于认证相关的函数,一个用于博客帖子相关的函数。每一个蓝图的代码将存放在单独的模块中。因为博客需要使用认证功能,所以你要先编写认证蓝图。

flaskr/auth.py
import functools

from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

这会创建一个名为 'auth'Blueprint。和应用对象类似,蓝图需要知道它在哪里被定义,所以 __name__ 作为第二个参数传入。url_prefix 将会被添加到所有和这个蓝图相关的 URL 前。

导入并在应用工厂里使用 app.register_blueprint() 注册蓝图。把新代码放到工厂函数的末尾,在返回应用实例之前。

flaskr/__init__.py
def create_app():
    app = ...
    # existing code omitted

    from . import auth
    app.register_blueprint(auth.bp)

    return app

认证蓝图将会有视图来注册新用户、登录和登出。

第一个视图:注册

当用户访问 URL /auth/register 时,register 视图会返回 HTML,其中包含一个让他们填写的表单。当他们提交表单时,它会验证他们的输入,并且要么再次显示表单和一个错误消息,要么创建新用户并跳转到登录页面。

现在你只需要编写视图代码。在下一章,你会编写模板来生成 HTML 表单。

flaskr/auth.py
@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None

        if not username:
            error = 'Username is required.'
        elif not password:
            error = 'Password is required.'

        if error is None:
            try:
                db.execute(
                    "INSERT INTO user (username, password) VALUES (?, ?)",
                    (username, generate_password_hash(password)),
                )
                db.commit()
            except db.IntegrityError:
                error = f"User {username} is already registered."
            else:
                return redirect(url_for("auth.login"))

        flash(error)

    return render_template('auth/register.html')

这是 register 视图函数在做的事情:

  1. @bp.route 把 URL /register 关联到 register 视图函数。当 Flask 收到一个发到 /auth/register 的请求,它会调用 register 视图并使用返回值作为响应。

  2. 如果用户提交了表单,request.method 会是 'POST'。在这种情况下,视图开始验证输入的数据。

  3. request.form 是一个特殊类型的 dict,它映射提交的表单键到相应的值。用户将会输入他们的 usernamepassword

  4. 验证 usernamepassword 不为空。

  5. If validation succeeds, insert the new user data into the database.

    • db.execute takes a SQL query with ? placeholders for any user input, and a tuple of values to replace the placeholders with. The database library will take care of escaping the values so you are not vulnerable to a SQL injection attack.

    • For security, passwords should never be stored in the database directly. Instead, generate_password_hash() is used to securely hash the password, and that hash is stored. Since this query modifies data, db.commit() needs to be called afterwards to save the changes.

    • An sqlite3.IntegrityError will occur if the username already exists, which should be shown to the user as another validation error.

  6. 保存用户后,他们被重定向到登录页面。url_for() 根据其名称生成到 login 视图的 URL。和直接写出来 URL 相比,这是更推荐的做法,因为它允许你以后轻松更改 URL 而不用更新所有链接到它的代码。redirect() 生成一个重定向响应到生成的 URL。

  7. 如果验证失败,错误会显示给用户。flash() 会存储消息,这些消息可以在渲染模板时被获取。

  8. 当用户最初导航到 auth/register 时,或是有验证错误时,应该显示一个带有注册表单的 HTML 页面。render_template() 将渲染一个包含该 HTML 页面的模板,你将在教程的下一章编写相关代码。

登录

这个视图和上面的 register 视图遵循相同的模式。

flaskr/auth.py
@bp.route('/login', methods=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):
            error = 'Incorrect password.'

        if error is None:
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

register 视图有一些区别:

  1. 首先查询用户,并存储到一个变量供后面使用。

    fetchone() returns one row from the query. If the query returned no results, it returns None. Later, fetchall() will be used, which returns a list of all results.

  2. check_password_hash() 以存储散列值时相同的方式为提交的密码计算散列值,并对它们进行安全对比。如果匹配,则密码是有效的。

  3. session 是一个 dict,用来存储跨请求的数据。当验证成功,用户的 id 被存储到一个新的 session。数据被存储到一个发送给浏览器的 cookie 中,然后浏览器会在后续的请求中把它发送回来。Flask 对数据进行安全 签名,使其无法被篡改。

现在,用户的 id 被存储到 session 中,它将会在后续的请求中可用。在每一个请求开始时,如果用户已经登录,他们的信息应该被加载并提供给其他视图。

flaskr/auth.py
@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user = None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

bp.before_app_request() 注册一个函数并让它在每一个视图函数之前运行,不管请求发到哪一个 URL。load_logged_in_user 检查用户的 ID 是否存储在 session 中,并从数据库中获取对应用户的数据,将其存储到 g.user 上——它存在于单个请求的生命周期内。如果没有用户 ID,或是 ID 不存在,g.user 将是 None

登出

若要登出,你需要从 session 中移除用户 ID。这样 load_logged_in_user 就不会在后续请求中加载用户。

flaskr/auth.py
@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

在其他视图里要求认证

创建、编辑和删除博客帖子将需要用户登录才能操作。可以使用一个 装饰器 来为每一个使用它的视图检查登录状态。

flaskr/auth.py
def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view

这个装饰器返回一个新的视图函数,它包裹了被装饰的原始视图函数。新的函数会检查是否有用户被加载:如果有用户被加载,则会调用原视图继续正常运行,否则会重定向到登录页面。你会在写博客视图时使用这个装饰器。

端点和 URL

url_for() 函数根据视图名称和参数生成到一个视图的 URL。视图的名称也被叫做 端点(endpoint),默认情况下,它和视图函数的名称相同。

例如,在教程的前面部分,hello() 视图被添加到应用工厂,这个视图的名称是 'hello',可以通过 url_for('hello') 生成指向它的 URL。如果它接受一个参数(后面你会看到),那么生成 URL 的方式则会是 url_for('hello', who='World')

当使用蓝图时,蓝图的名称会被插入到视图函数的名称前,所以你上面写的 login 函数的端点是 'auth.login',因为你把它添加到了 'auth' 蓝图。

继续阅读 模板