iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Software Development

都是 P 開頭的程式語言,你是在 py 啥啦系列 第 13

[13] [燒瓶裡的部落格] 03. Blueprints 和 View function

面對龐大架構,官方建議使用 Blueprints 將程式碼拆分成不同的模組(modules)

Blueprint 是一種把關聯程式和 view 組織起來的方式
和直過把 view 和其他程式直接注冊到應用的方式不同,直接是把它們注冊到Blueprints
然後在工廠函數中把 Blueprint 註冊到應用中

在我們的練習 Flaskr 中有兩個 Blueprint,一個用於認證功能,另一個用於部落格文章管理
每個 Blueprint 的程式都在一個獨立的的 module 中

使用部落格首先需要認證,因此我們先寫認證的 Blueprint

建立第一個 Blueprint

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
和應用物件一樣,Blueprint 需要知道是在哪裡定義的,因此把 __name__ 作為函數的第二個參數
url_prefix 會添加到所有與該 Blueprint 關聯的 URL 前面

flaskr/init.py
修改檔案,使用 app.register_blueprint() 導入並注冊剛剛建立的 auth.bp Blueprint
把新的程式放在工廠函數的尾部回傳應用之前!

def create_app():
    app = ...
    # existing code omitted

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

    return app 

認證的 Blueprint 將包括新用戶註冊、登入和登出的 view

註冊

當訪問/auth/register URL 時,register view 會回傳註冊表單的 HTML 頁面
當用戶提交表單時,view 會驗證表單內容,接著根據註冊結果
註冊成功則建立新用戶並顯示登錄頁面,否則顯示表單並顯示一個錯誤訊息

底下是 view function 的程式內容

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 view function做了以下的事情

路由註冊

@bp.route 關聯了 URL /register 和 register 的 view function
當 Flask 收到一個指向 /auth/register 的 request 時會呼叫 register view function
並把其返回值作為 response

提交表單

如果使用者送出表單,那麼 request.method 將會是POST
這個情況下會進行輸入內容的驗證

驗證表單

request.form 是一個特殊類型的 dict,其對應了提交表單的鍵和值
表單中會求使用者會輸入usernamepassword,所以要驗證usernamepassword不為空

寫入資料

如果表單驗證通過,則寫入新的使用者資料

db.execute 使用了帶有「?」佔位符的 SQL 查詢語句
佔位符可以代替後面的元組參數中相應的值
使用佔位符的好處是會自動幫你轉譯輸入值,以防止 SQL 注入攻擊

為了安全原因,不能把密碼明文儲存在資料庫中
使用 generate_password_hash() 將輸入的密碼進行 hash
而 db.execute 只會執行 SQL 指令
要將指令提交至 SQL,要使用 db.commit() 才會真的執行前面的 SQL query

如果username已經存在,造成無法寫入的情況下
會發生sqlite3.IntegrityError,這時候會建立一條錯誤提示訊息

頁面跳轉

使用者資料建立後會跳轉到登入頁面,透過 url_for() 產生對應路由方法函式的 URL
比起直接寫死 URL,這麼做的好處是如果之後需要修改對應的 URL,則不需要一個一個找出來改
使用 redirect() 直接跳轉到指定的 URL,也就是登入頁

如果驗證失敗,透過flash() 可以在渲染的模塊時候向使用者顯示一個錯誤訊息

使用者一開始打開auth/register時,或是註冊出錯時應該顯示一個註冊的表單
呼叫 render_template() 會渲染一個包含 HTML 的模板,在下一節會學習如何寫這個模板

登入

這個 view 和上面註冊的模式一樣

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有一點點不同之處

查詢使用者資料

fetchone() 會據查詢回傳一筆紀錄,如果查詢沒有結果,則回傳None
後面會用到 fetchall() 則會回傳所有查詢結果的列表

檢查密碼

check_password_hash() 會以相同的方式對密碼進行 hash 密碼並比較查詢結果是否正確

儲存登入狀態

session 是一個 dict,用於儲存橫跨請求的值
當登入驗證成功後,使用者的id被儲存於一個新的 session 中
資料被儲存到一個向瀏覽器發送的 cookie 中,而瀏覽器在後繼發送的請求中會帶上該 cookie
Flask 會對資料進行簽章,以防數據被篡改

現在使用者 id 已被儲存在 session 中,可以在後續的 request 中使用
如果使用者已經登入,那麼他的使用者資料應該被載入並且在其他 view 裡被使用

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() 註冊一個在 view function 之前運行的函數
不論 URL 是什麼,load_logged_in_user都會檢查使用者 id 是否已經儲存在 session 中
並從資料庫中取得使用者資料,然後儲存在 g.user 中,並且會持續存在
如果沒有使用者 id ,或者 id 查詢結果不存在,那g.user將會是None

登出

登出的時候需要把使用者 id 從 session 中移除
然後 load_logged_in_user 就不會在後續的請求中載入使用者資料了

flaskr/auth.py

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

在其他 View 中驗證登入狀態

因為要登入以後才能建立、編輯和刪除文章
在每個 view 中可以使用裝飾器來完成這個工作

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

裝飾器回傳一個新的 view,包含了傳遞給裝飾器的原始 view
新的函數檢查使用者是否登入
如果已經登入,那麼就繼續正常執行原本的 view
否則就跳轉到登入頁!之後會在部落格的 view 中使用這個裝飾器

Endpoints 和 URL

函數 url_for() 根據 view 的名稱產生 URL
和 view 相關聯的名稱亦稱為 endpoint
預設情況下,endpoint 名稱與 view 的函數名稱相同

For example, the hello() view that was added to the app factory earlier in the tutorial has the name 'hello' and can be linked to with url_for('hello'). If it took an argument, which you’ll see later, it would be linked to using url_for('hello', who='World').

例如,之前被加入應用工廠hello()的 view 為'hello',可以使用url_for('hello')來連接
之後會遇到 view 有參數,那麼可使用url_for('hello', who='World') 連接

When using a blueprint, the name of the blueprint is prepended to the name of the function, so the endpoint for the login function you wrote above is 'auth.login' because you added it to the 'auth' blueprint.

當使用 blueprint 的時候,blueprint 的名稱會添加到函數名稱的前面
上面寫的 login 函數 endpoint 為'auth.login',因為你把他加在 'auth' 的 blueprint 中

今天這篇是真的長,主委沒拆成多篇水一天很有誠意的吧


上一篇
[12] [燒瓶裡的部落格] 02. 定義和使用資料庫 - 使用 SQLite
下一篇
[14] [燒瓶裡的部落格] 04. Templates 模板
系列文
都是 P 開頭的程式語言,你是在 py 啥啦30

尚未有邦友留言

立即登入留言