iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Modern Web

Flask系列 第 23

Day 23 實作 user_bp (1)

  • 分享至 

  • xImage
  •  

前言

今天要進入 user_bp,但因為他路徑太多太複雜,所以我們必須分段處理,而今天要處理的是驗證的部分。

user_helper

在開始寫路徑之前,我們要先稍微處理一下 login_manager,接下來這個檔案要放在 app/ 裡面。

user_helper.py

from flask_login import LoginManager, UserMixin
from .database.models import Users


class User(UserMixin):
    pass


login_manager = LoginManager()


@login_manager.user_loader
def load(user_id):
    user_id = int(user_id)
    user = Users.query.filter_by(id=user_id).first()
    if user:
        sessionUser = User()
        sessionUser.id = user.id
        sessionUser.is_admin = user.is_admin
        return sessionUser
    else:
        return None

在這裡面我們看到一個新的 login_manager,我們要用這個來做事,所以在 __init__.py 的那個就可以刪掉了,直接 from .user_helper import login_manager

在這邊我們做了很多事情,有點複雜,所以我不會完整解釋。我們先從最前面定義 User 開始。他繼承自從 Flask-Login 來的 UserMixin,功用是當一個 Flask-Login 需要用的 user,他會在下面的 load 被用到。

接著就看到下面的 load,他加上了一個 login_manager.user_loader 的裝飾器。在這個函式裡面,我們去資料庫找到這個使用者,然後把它包裝一下變成剛剛定義的 User 的形式,這樣 Flask-Login 才看得懂,而如果這個使用者不存在的話,那就回傳 None,這是官方文件指示的。這個函式基本上是在我們登入後,可能會想要找這個 user 是誰,那就需要這個 callback function 了,他是一個很核心的函式,沒有他基本上就動不了。

這個檔案不會這麼快就結束,之後我們還會修改他,可以敬請期待。

/login

在開始寫路徑之前,我們要先加入一些函式。

def login_auth(username, password):
    if user := Users.query.filter_by(username=username).first():
        if user.check_password(password):
            sessionUser = User()
            sessionUser.id = user.id
            return sessionUser
    return False

他要放在 app/database/helper.py 裡面,是用來驗證登入有沒有成功的工具。

在這邊就要說一下 Flask-SQLAlchemy 要怎麼抓資料。我們先看 Users.query,前面的部分是這個 table 的 model,後面則是一個 query 物件,他也可以寫成 db.session.query(Users),但我喜歡前者的寫法。接下來他有一個 filter_by,他就是一個過濾器,username=username 代表他要過濾出 username 欄位的值等於我想要的 username 的整個物件。例如說我現在要使用者名稱是 siriuskoan 的人,那就寫成 filter_by(username="siriuskoan") 即可。這樣還沒結束,我們可以繼續加更多 filter_by,就這樣一直連著下去。直到最後,我們要把那些物件抓出來,這時候就可以使用 all() 或是 first(),此處使用者名稱不會重複,所以直接用後者沒有問題。

check_password 則是我們之前在寫資料庫的時候就寫好的東西,如果通過的話,就會建立一個剛剛寫好的 User (繼承 UserMixin),然後設定好 id 並且送回來,等等在寫路徑的時候就會用到他的這個回傳。

接下來加入 HTML,一樣直接繼承自 base.html

login.html

{% extends "base.html" %}

{% block title %}Login{% endblock %}

{% block content %}
<form action="/login" method="post">
    {{ form.csrf_token }}
    {{ form.username }}
    {{ form.password }}
    {{ form.submit }}
</form>
{% endblock %}

它裡面有一個 form,等等我們會用 render_template 傳入,他就是我們的 LoginForm。後面三個很明顯就是我們在 forms.py 寫好的欄位,比較特別的是第一個。在好幾天前說 Flask-WTF 的時候有提到他可以防止 CSRF,就是利用這個 csrf_token,如果沒有他的話,Flask-WTF 會噴錯誤,除非在設定加上 WTF_CSRF_ENABLED = False,像是測試的設定那個樣子。

最後就可以進入路徑本身了,我們會用到很多函式,之前提過的 flask 函式就不會再說了,請自行引入。

views.py

@user_bp.route("/login", methods=["GET", "POST"])
def login_page():
    if current_user.is_active:
        flash("You have logined.", category="info")
        return redirect(url_for("user.dashboard_page"))
    else:
        form = LoginForm()
        if request.method == "GET":
            return render_template("login.html", form=form)
        if request.method == "POST":
            if form.validate_on_submit():
                username = form.username.data
                password = form.password.data
                if user := login_auth(username, password):
                    login_user(user)
                    flash(f"Login as {username}!", category="success")
                    return redirect(url_for("user.dashboard_page"))
                else:
                    flash("Wrong username or password.", category="alert")
                    return redirect(url_for("user.login_page"))
            else:
                for field, errors in form.errors.items():
                    for error in errors:
                        flash(error, category="alert")
                return redirect(url_for("user.login_page"))

在開始解釋之前,應該會注意到有很多之前沒看過的東西,像是 current_userlogin_user,他們都要從 flask_login 引入 (from flask_login import current_user, login_user)。current_user 就是現在的使用者,如果沒登入的話就會是匿名使用者是,他跟 current_app 有點像,就是可以在任意地方抓到現在的東西。而 login_user 就是讓使用者登入用的,等等看到後面會比較清楚。

可以看到在路徑的最一開始我們先做的一個判斷,使用了 current_user.is_active,他代表這個使用者是否 active,基本上就是是否有登入。其他還有一些東西也可以判斷,像是 current_user.is_anonymous 就可以看他是不是匿名使用者。有興趣的話可以打開 UserMixin 的原始碼,會看到他有一些 property 可以用,而在他下面會有一個叫作 AnonymousUserMixin 的物件,如果在沒有登入的狀況下看 current_user 就會看到他,他的 is_anonymous 就是 True。如果這個判斷是成立的話,我們就 flash 一個小訊息,然後把它重新導向到 dashboard;如果不成立 (他還沒登入),那我們會先宣告一個 form,他是 LoginForm 的實體,然後在 request.method == "GET" 的時候把它傳出去,也就是剛剛 HTML 看到的 form

基本上來說 GET 沒什麼好說的,精彩的在 POST。一開始我們先判斷 form.validate_on_submit(),他就是我們之前的一堆驗證,如果不過的話就會有錯誤訊息出來,所以我們先看看沒過的情況,我們要從 form.errors.items() 把錯誤撈出來,他會包含 fielderrors 兩個部分,前者代表錯誤發生的欄位,後者代表錯誤訊息,而這個錯誤訊息可能很多條,所以他是一個 list,要 for 把他一條一條抓出來然後 flash 出去。

接著回到驗證過的情況,我們就從表單裡面取出資料,收進 usernamepassword 裡面,接著用剛剛寫好的 login_auth 來看看有沒有通過,如果是正確的帳號密碼,那就會收到他生出來的使用者,接著用 login_user 把它登入,然後再 然後再 flash 一些東西,最後重新導向到 dashboard,如果驗證失敗的話,那就會繞回來讓使用者重新驗證。

/logout

剛剛看完複雜的登入頁面,現在來看看簡單的登出頁面。

@user_bp.route("/logout", methods=["GET"])
def logout_page():
    logout_user()
    return redirect(url_for("main.index_page"))

就這樣,非常簡短。其中有個 logout 沒有看過,他也是要從 Flask-Login 引入的函式,功能就是登出使用者。他不需要參數,反正他會把 session 清乾淨,然後我們只需要重新導向到首頁就好。

References

Flask-Login How it Works
Flask實作 ext 11 Flask-Login 登入狀態管理


上一篇
Day 22 實作 main_bp
下一篇
Day 24 實作 user_bp (2)
系列文
Flask30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言