Python Web Flask — Flask-Login登入系統實作

Sean Yeh
Python Everywhere -from Beginner to Advanced
22 min readFeb 4, 2021
Golden Gate Bridge, San Francisco, CA, United States, photo by Sean Yeh

曾經製作網站的朋友應該都知道,如果網站的功能不希望只是單純用來顯示公開訊息的話,都需要一個機制讓有權限的人可以操作或瀏覽到特定的內容,而無權限者則無法看到那些內容。這個機制就是網站的登入與登出的功能。當網站有了這項功能,您的網站就開始朝向「網路應用程式」的階段邁進。

製作網站的登入與登出的功能在不同的程式語言、不同的框架下使用的方式個有差異,在Flask中我們可以使用Flask-Login這個套件來實現網站登入與登出的功能。Flask-Login Library的特色是可以讓我們在Flask Web的開發中使用簡單的方式就達成使用者認證的功能。

接下來,我們就逐步實作一個簡單的使用者登入與登出的網站。透過這個實際的製作,我們可以暸解到製作一個具有登入與登出的網站,最少需要哪些東西。

flask-login使用說明

根據官網的說明:

Flask-Login provides user session management for Flask. It handles the common tasks of logging in, logging out, and remembering your users’ sessions over extended periods of time.

所謂flask-login藉由 user session來提供使用者登入的任務,諸如使用者的登入、登出以及在一定的期間內記住目前的使用者,因此,flask-login可以將使用者的 ID儲存在session中,讓使用者可以方便的登入、登出網站應用服務。

安裝套件

在使用flask-login前,必須透過pip將套件安裝在自己的虛擬環境中:

$ pip install flask-login

匯入套件

安裝完畢之後就可以在Python檔案(例如 app.py)中匯入套件:

from flask_login import LoginManager

使用套件

flask-login Class是使用LoginManager時很重要的一個Class。匯入 flask-login套件後,需要建立LoginManager實體才可以使用。

login_manager = LoginManager()

實體建立後,還需要初始化,並且提供一個登入的view。在這裡我們將view指向login頁面。

login_manager.init_app(app)
login_manager.login_view = 'login'

登入系統實作

大致上暸解了flask_login的安裝與匯入的方式後,我們要開始實作一個登入系統的網路應用程式。首先,請大家看看下面的圖表:

登入系統網 檔案架構

這個圖表說明了本次實作的「登入登出系統網」的檔案架構。主要應用程式資料夾命名為「myproject」,除了啟動這個資料夾所需的 __init__.py 檔案外,裡面包括表單相關函式(forms)、資料庫函式(models)以及templates樣板。網站的路由部分,則放在最外層的app.py裡面。

再來看看整個系統完成時的各個畫面:

首頁

下面的畫面是進入網站時的第一個頁面。這時候使用者尚未進行登入。首頁上的兩個超連結引導使用者進行登入系統或者註冊帳號的後續行動。

首頁

登入系統

若使用者已經註冊過帳號,即可以點選超連結,或者是網頁上方的Menu連結進入系統登入畫面。使用者需要在這個頁面輸入電子郵件與密碼進行登入的行為。

登入畫面

註冊帳號

若使用者尚未註冊過帳號,即可以到註冊帳號的畫面中,填寫電子郵件、使用者名稱與密碼,進行註冊程序。在這階段,我們會檢查密碼等欄位,以確保整個註冊的程序是正確的。

註冊帳號的畫面

會員專區

若使用者從登入頁面,登入系統後,眼前呈現的畫面就是會員專區的畫面。這個畫面只有在登入的狀況下可以看到,尚未登入的使用者是看不到的。此外, Menu選單的頁面連結會從「登入系統」變為「登出系統」,以提供使用者登出會員專區。

登入後的畫面

以上是整個系統的規劃,我們將會依照這個架構與畫面,逐一的建立這個應用程式。然而,由於這篇主要不是在討論HTML與CSS的操作與使用,在Template的部分,我們會省略一些為讓畫面美觀的div標籤(要讓畫面美觀,有時候必須不計成本的在HTML中使用div標籤,當然如果您的CSS功力還不錯的話,可以節省一些div標籤的使用,甚至於可以達到Zen「禪」的境界。)

__init__檔案

這個檔案(__init__.py)的主要作用是用來初始化Python的「myproject」 packages。首先在這個檔案裡面,我們需要匯入Flask套件,以及其他本應用程式所需要的套件。例如os、flask、 flask_sqlalchemy 、flask_migrate 以及 flask_login等套件

import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
# flask_login
from flask_login import LoginManager

路徑設定

設定專案路徑basedir。

basedir = os.path.abspath(os.path.dirname(__file__))

app實體化

依如以往,先建立app實體。

app = Flask(__name__)

然後,再設定 app實體的config:

其中包括SECRET_KEY、SQLALCHEMY_DATABASE_URI 與SQLALCHEMY_TRACK_MODIFICATIONS 等config。

app.config['SECRET_KEY']= 'acretkeyinthisproject'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+os.path.join(basedir,'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

資料庫db

設定資料庫db與Migrate

db = SQLAlchemy(app)
Migrate(app,db)

LoginManager實體

接著要建立LoginManager的實體,並且初始化,使app有login_manager的功能,以及設定login的view名稱為 login。

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

以上的程式碼是__init__的部分。

models檔案

完成了__init__之後,我們接著處理models部分。在models.py裡面我們要處理與資料庫相關的問題。

匯入套件

需要匯入的套件包括剛剛我們在 myproject 裏的__init__ 中建立的db與 login_manager 實體;與密碼加密有關的werkzeug 以及與login有關的flask_login。

from myproject import db, login_managerfrom werkzeug.security import generate_password_hash, check_password_hashfrom flask_login import UserMixin

建立 User Class

繼承資料庫db.Model與UserMixin建立使用者資料表User Class。其中的table名稱為「users」。欄位有四,分別為id、email、username、password_hash 。其中id為主鍵;email與username 資料為獨一且可以被查詢;而 password_hash 儲存的為加密後的密碼,因此在使用者輸入 password 之後,還需要將這輸入值透過 generate_password_hash( password ) 轉換為密碼存入password_hash中。換句話說,我們實際存入的值為加密後的password_hash,而非使用者輸入的password本身。

class User(db.Model, UserMixin):
__tablename__ = 'users'

# columns
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(64),unique=True, index=True)
username = db.Column(db.String(64),unique=True, index=True)
password_hash = db.Column(db.String(128))
def __init__(self, email, username, password):
"""初始化"""
self.email = email
self.username = username
# 實際存入的為password_hash,而非password本身
self.password_hash = generate_password_hash(password)

def check_password(self, password):
"""檢查使用者密碼"""
return check_password_hash(self.password_hash, password)

check_password函式用來檢查使用者登入時輸入的密碼與資料庫中存放的加密後password_hash是否一樣,如果兩者相同,就可以進入網站,反之則否。

關於Password Hash的說明,可以參考下面文章。

login_manager裝飾器

使用login_manager裝飾器,建立load_user函式。load_user函式的參數為user_id。可以依照這個user_id,對應出資料庫中實際的User。

@login_manager.user_loaderdef load_user(user_id):
return User.query.get(user_id)

以上的程式碼是models的部分。

forms檔案

在forms.py裡面我們要處理資料輸入的相關問題。首先我們要匯入套件,包括flask_wtf 與 wtforms。內容包括各種型態的欄位以及欄位的檢查、檢查出錯誤時的對應處理。

from flask_wtf import FlaskFormfrom wtforms import StringField, PasswordField, SubmitFieldfrom wtforms.validators import DataRequired, Email, EqualTo, email_validatorfrom wtforms import ValidationError

在這個應用程式中我們需要兩個表單,分別是登入用的login表單以及註冊會員時的表單,至於登出的時候,不需要另外製作表單。

登入用的login表單我們命名為:LoginForm。註冊用的表單我們命名為:RegistrationForm,兩者都繼承自前面匯入的FlaskForm(源自於 flask_wtf )。

登入表單

登入用的login表單需要三個欄位,分別是email、password與submit(也可以不需要)。email使用字串格式( StringField )、password則使用密碼格式( PasswordField )兩者都進行必填的欄位檢查( validators=[DataRequired()] ),email的欄位則要外加email格式的檢查(Email())。

class LoginForm(FlaskForm):
email = StringField('電子郵件', validators=[DataRequired(), Email()])
password = PasswordField('密碼',validators=[DataRequired()])
submit = SubmitField('登入系統')

註冊表單

註冊用的表單則需要五個欄位,分別是email、username、password、pass_confirm與submit(也可以不需要)。設置方式大致上與上面的登入表單類似。其中值得注意的地方是password 欄位多了一個 EqualTo( ‘pass_confirm’, message=’密碼需要吻合’ ) 的驗證屬性。在這個欄位輸入的值需要與下面欄位的值進行比較,如果輸入的內容一致時,才會符合驗證的需求,否則即無法進行註冊。

class RegistrationForm(FlaskForm):
email = StringField('電子郵件', validators=[DataRequired(), Email()])
username = StringField('使用者', validators=[DataRequired()])
password = PasswordField('密碼', validators=[DataRequired(), EqualTo('pass_confirm', message='密碼需要吻合')])
pass_confirm = PasswordField('確認密碼', validators=[DataRequired()])
submit = SubmitField('註冊')

除了設定欄位之外,RegistrationForm類還加上了檢查email與檢查使用者名稱的方法。這兩個方法會檢查輸入的電子郵件與使用者名稱是否與資料庫中存在的紀錄重複。如果重複了就會產生錯誤。

def check_email(self, field):
"""檢查Email"""
if User.query.filter_by(email=field.data).first():
raise ValidationError('電子郵件已經被註冊過了')
def check_username(self, field):
"""檢查username"""
if User.query.filter_by(username=field.data).first():
raise ValidationError('使用者名稱已經存在')

app檔案

接著,我們來到了app.py檔案。這裡我們會建立這個網站需要的所有路由。在建立之前,我們一樣需要匯入套件。

匯入套件

from flask import render_template, redirect, request, url_for, flash, abortfrom flask_login import login_user, logout_user, login_requiredfrom myproject import app, db
from myproject.models import User
from myproject.forms import LoginForm, RegistrationForm

在這裏增加各個頁面的路由。

首頁

首頁的路由/,設定為導向home的template:

@app.route('/')
def home():
return render_template('home.html')

登入

登入頁的路由/login,設定為導向login的template,由於需要接收form表單的資料,因此除了GET方法外,還需要POST方法。表單對應的是form.py裡面的LoginForm Class:

@app.route('/login',methods=['GET','POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user.check_password(form.password.data) and user is not None:
login_user(user)
flash("您已經成功的登入系統")
next = request.args.get('next')
if next == None or not next[0]=='/':
next = url_for('welcome_user')
return redirect(next)
return render_template('login.html',form=form)

登出

登出雖然沒有頁面,但是仍然需要路由/logout提供給登出使用。@login_required裝飾器用來確認使用者狀態必須是在登入狀態。此外,我們使用flash來呈現登出成功的訊息:

@app.route('/logout')
@login_required
def logout():
logout_user()
flash("您已經登出系統")
return redirect(url_for('home'))

註冊

註冊的路由/register,導向register的template。與前面的login一樣,除了GET方法外,還需要POST方法來接收RegistrationForm表單的資料。此外,在這個階段,我們需要讓經過驗證的使用者註冊資料可以被存在資料庫中,我們需要對db.session進行寫入的步驟:

@app.route('/register',methods=['GET','POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data,
username=form.username.data, password=form.password.data)

# add to db table
db.session.add(user)
db.session.commit()
flash("感謝註冊本系統成為會員")
return redirect(url_for('login'))
return render_template('register.html',form=form)

會員專區

會員專區路由/welcome,導向welcome_user的template:

@app.route('/welcome')
@login_required
def welcome_user():
return render_template('welcome_user.html')

啟動app

最後加上啟動,並且啟動debug模式。

if __name__ == '__main__':
app.run(debug=True)

templates樣板

樣板的部分,我們依照需要建立了下面幾個樣板:

base.html

這個樣板為所有頁面的基礎樣板,我們將網站共用的部分寫在這個樣板中。共用的部分有CSS、JS等檔案的連結位置,網站title標題、放meta tag的head部分。還有網站Header與Menu選單、footer等區塊。其他頁面的畫面則會出現在{% block content %} {% endblock %}以內的地方

值得一提的是,我們在選單的部分做了一個切換開關,如果目前的狀態是使用者已登入時,這時候current_user.is_authenticated 即為True,我們就顯示「登出系統」的連結;反之則顯示「登入系統」與「註冊帳號」的連結。

{% if current_user.is_authenticated %}<li class="nav-item">
<a class="nav-link" href="{{url_for('logout')}}">登出系統</a>
</li>
{% else %}<li class="nav-item">
<a class="nav-link" href="{{url_for('login')}}">登入系統</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{url_for('register')}}">註冊帳號</a>
</li>
{% endif %}

home.html

在這個頁面,我們會透過extends 使用base.html樣板當做基底。並且使用{% block title %}乘載本頁面標題,與{% block content %}乘載本頁面的所有內容。

{% extends "base.html" %}
{% block title %}首頁 |Login Exp{% endblock %}
{% block content %}
...略...
{% endblock %}

值得注意的是,在{% block content %}裡面,我們對於該顯示什麼內容,進行了判斷。判斷的依據是使用者是否登入頁面。如下面程式碼,若current_user.is_authenticated為True,即表示發生登入頁面的狀態,這時候畫面會顯示使用者的名稱(current_user.username),反之則顯示兩個超連結:「登入帳號」與「註冊帳號」。

{% if current_user.is_authenticated %}<h1 class="display-4">{{current_user.username}}</h1>{% else %}<p>請 <a href="{{url_for('login')}}">登入帳號</a> 或 <a href="{{url_for('register')}}">註冊帳號</a>
</p>
{% endif %}

login.html

與前面的home.html一樣,需要extends 使用base.html樣板與使用{% block title %}乘載標題{% block content %}乘載頁面。

在content裡面,放置了form表單,並且利用class_的方式,為input欄位加上css的class。如果需要兩個以上的css class,可以直接使用空一格的方式加在後面。例如下面方式是有效的。

例:class_=”form-control form-control-lg”

<form method="POST">{# This hidden_tag is a CSRF security feature. #} 
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.email.label }}
{{ form.email(class_="form-control form-control-lg") }}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class_="form-control form-control-lg") }}
</div>
{{form.submit(class_="btn btn-primary btn-lg")}}
</form>

register.html

這個頁面大致上跟前面的的login.html一樣。唯一的差異在於這個頁面的欄位比較多。

<form method="POST">{{ form.hidden_tag() }}<div class="form-group">
{{ form.email.label }}
{{ form.email(class_="form-control form-control-lg") }}
</div>
<div class="form-group">
{{form.username.label}}
{{form.username(class_="form-control form-control-lg") }}
</div><div class="form-group">
{{ form.password.label }}
{{ form.password(class_="form-control form-control-lg") }}
</div>
<div class="form-group">
{{ form.pasw_confirm.label }}
{{ form.pasw_confirm(class_="form-control form-control-lg") }}
</div>
{{form.submit(class_="btn btn-primary btn-lg")}}
</form>

welcome_user.html

這個頁面也與前面的樣板差不多,裡面多了一個使用這名稱的顯示。

<h3>{{current_user.username}},歡迎回來</h3>

以上就是所有的製作過程。您可以使用python app.py來啟動網站應用。看看是否可以正常使用。

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。