今天要進入 user_bp
,但因為他路徑太多太複雜,所以我們必須分段處理,而今天要處理的是驗證的部分。
在開始寫路徑之前,我們要先稍微處理一下 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 了,他是一個很核心的函式,沒有他基本上就動不了。
這個檔案不會這麼快就結束,之後我們還會修改他,可以敬請期待。
在開始寫路徑之前,我們要先加入一些函式。
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_user
、login_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()
把錯誤撈出來,他會包含 field
和 errors
兩個部分,前者代表錯誤發生的欄位,後者代表錯誤訊息,而這個錯誤訊息可能很多條,所以他是一個 list,要 for 把他一條一條抓出來然後 flash
出去。
接著回到驗證過的情況,我們就從表單裡面取出資料,收進 username
和 password
裡面,接著用剛剛寫好的 login_auth
來看看有沒有通過,如果是正確的帳號密碼,那就會收到他生出來的使用者,接著用 login_user
把它登入,然後再 然後再 flash
一些東西,最後重新導向到 dashboard,如果驗證失敗的話,那就會繞回來讓使用者重新驗證。
剛剛看完複雜的登入頁面,現在來看看簡單的登出頁面。
@user_bp.route("/logout", methods=["GET"])
def logout_page():
logout_user()
return redirect(url_for("main.index_page"))
就這樣,非常簡短。其中有個 logout
沒有看過,他也是要從 Flask-Login 引入的函式,功能就是登出使用者。他不需要參數,反正他會把 session 清乾淨,然後我們只需要重新導向到首頁就好。
Flask-Login How it Works
Flask實作 ext 11 Flask-Login 登入狀態管理