Day 23 实作 user_bp (1)

前言

今天要进入 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 18:专案03 - PTT 八卦版爬虫03 | 文章标题、作者、时间

>>:  Day08 Flutter 和 Native 通讯的原理 02

Day 3:AWS是什麽?30天从动漫/影视作品看AWS服务应用 -《Vivy -Fluorite Eye's Song》Part 3

Vivy几个着名事件中,与IoT最为相关的事件即是 研究员达也因为无法忘怀自己小时候的护理师AI葛蕾...

[DAY12]测试SSH连线

接续之前做容器,希望可以用code连到另台电脑的container,做了ssh的一些测试 先做了一个...

Day 30: 遗漏的章节

「目前为止,所有建议无疑将帮助你设计出更好的软件,这些软件是由具有明确边界、职责、依赖关系受控的元...

Day13. UX/UI 设计流程之三: UI Flow (并使用Axure RP 实作)

UI Flow 的全名是 User Interface Flow,望文生意,也就是使用者点某个按钮会...

Program Loader

之前写了一个很简单的 Program Loader, 现在就来真正的实作它,让它能够把编译好的程序放...