第5天下篇:在Flask應用中使用用戶認證—Flask_Login

原文: http://www.catonlinepy.tech/
聲明: 原創不易,未經許可,不得轉載

接第5天上篇的內容,我們接着介紹用戶註冊功能的實現

3. 用戶的註冊

3.1 註冊表單及前端渲染

根據經驗,當我們在登錄某個應用時,如果我們沒有在平臺註冊過賬號,平臺會提醒我們註冊一個賬號,註冊成功後方可進行登陸。與登錄頁面的實現類似,前端的註冊頁面也需要後臺傳入註冊表單,然後Jinjia2將表單渲染成前端瀏覽器能解析的form表單。由於註冊頁面通常要求用戶輸入電子郵箱,用戶名,密碼以及確認密碼,下面我們首先在forms.py文件中增加註冊表單類registerForm:

# forms.py文件中新增的內容
# ..
# 在wtforms中還需要導入StringField,ValidationError字段
from wtforms import PasswordField, BooleanField, SubmitField, StringField,ValidationError

#  在validators驗證器中還需要導入EqualTo, InputRequired驗證器
from wtforms.validators import DataRequired, Length, EqualTo, InputRequired

# 在def兩個函數中使用到User模型,需要導入
from userauth_demo.models import User

# ..

class registerForm(FlaskForm):
    username = StringField(u'用戶名',
                             validators=[DataRequired(), Length(min=2, max=20)])
    email = EmailField(u"郵箱",
                         validators=[DataRequired(), Length(min=2, max=20)])
    password = PasswordField(u'密碼',
                              validators=[InputRequired(), EqualTo(u"confirm_password", message="兩次輸入的密碼不一致!")])
    confirm_password = PasswordField(u'確認密碼',
                                      validators=[DataRequired()])
    submit = SubmitField(u'註冊')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('該用戶名已被註冊!')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('該郵箱已被註冊!')

在上面的註冊表單類registerForm中,爲了減少輸入錯誤的風險,使用到了password和confirm_password,password密碼中用到了EqualTo驗證器,它需要與confirm_password中輸入的密碼一致才通過驗證。這個註冊表單中還使用到了兩個自定義函數,它們以validate開頭,後面接上_<fieldname>,這裏的filedname都是WTForms提供的自定義字段,當前端用戶點提交按鍵時,就會觸發這兩個函數的調用。在這裏,當用戶提供了註冊信息後,會在數據庫中的user表中進行查詢,如果用戶名和郵箱不存在,則可順利的完成註冊,否則,就會執行if語句內的錯誤提醒程序,這些錯誤提醒會在瀏覽器頁面渲染出來。

註冊表單寫好後,需要在前端HTML模板中渲染表單,它與登陸功能的模板類似,在register.html文件中輸入如下內容:

<!-- register.html 文件中的內容-->

{% extends "layout.html" %}

{% block content %}
    <h2>註冊</h2>
    <form action="" method="post">
            <!--防跨站僞造請求-->
            {{html_form.hidden_tag() }}
        <div>
            {{ html_form.username.label }}<br />
            {{ html_form.username() }}
            {% if html_form.username.errors %}
                <div >
                    {% for error in html_form.username.errors %}
                        <span>{{ error }}</span>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        <div>
            {{ html_form.email.label }}<br />
            {{ html_form.email() }}
            {% if html_form.email.errors %}
                <div >
                    {% for error in html_form.email.errors %}
                        <span>{{ error }}</span>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
        <div>
            {{ html_form.password.label }}<br />
            {{ html_form.password() }}
        </div>
        <div>
            {{ html_form.confirm_password.label }}<br />
            {{ html_form.confirm_password() }}
        </div>
        <div>
            {{ html_form.submit() }}
        </div>
    </form>
    <!-- 添加跳轉到登陸頁面的超鏈接 -->
    <p>
        已經有賬戶? <a  href="{{ url_for('login')}}">登陸</a>
    </p>
{% endblock content %}

3.2 爲註冊表單編寫路由函數

在編寫路由函數之前,先來講講密碼哈希的重要性。因爲在註冊頁面中,用戶需要輸入密碼,而密碼最終需要存入到數據庫中的,但是如果把明文密碼存到數據庫,這是一件很危險的事兒。假如數據庫被黑客獲取後(脫褲),用戶的個人賬號、密碼就完全泄漏給了黑客。這時,就需要有工具將密碼加密後再寫到數據庫。flask_bcrypt就是幹這個活兒的插件,負責對明文密碼進行加密操作,下面先在虛擬環境中安裝這個插件:

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day5$ pip install Flask_Bcrypt

安裝完成後,我們先在python shell中練練手,熟悉熟悉這個插件的基本用法:

(miao_venv) maojie@Thinkpad:~/flask-plan/flask-course-primary/day5$ python
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from flask_bcrypt import Bcrypt                                      # 從flask_bcrypt中導入Bcrypt類
>>> bcrypt = Bcrypt()                                                    # 對Bcrypt()進行實例
>>> bcrypt.generate_password_hash('miaojie')                             # 產生哈希密碼
b'$2b$12$4D2eChUFV45DkC/VMsc0.u3JSjkSPvSZHHtokDHmrhD7tnKldFf/a'
>>> bcrypt.generate_password_hash('miaojie').decode('utf-8')             # 對哈希密碼進行解碼
'$2b$12$Q8.Io9ld5mwXiCKc2TiWI.8c7FPDwh440zo80NkwUwvtebXy.8xoO'
>>> bcrypt.generate_password_hash('miaojie').decode('utf-8')             # 每次產生的密碼會不一樣,這使得得通過哈希過的密碼無法查看它原始的密碼
'$2b$12$YXKa1TSyROPvpprL7UfjOOoWdq16z/ZH2n8dpn8tCc8nDTwNoB.Jm'
>>> hashed_password = bcrypt.generate_password_hash('miaojie').decode('utf-8')  
>>> bcrypt.check_password_hash(hashed_password,'password')                # 哈希過的密碼和明文密碼進行對比,匹配則返回True,不匹配則返回False
False
>>> bcrypt.check_password_hash(hashed_password,'miaojie')
True
>>> 

熟悉了flask_bcrypt插件後,我們就可以使用它了,在__init__.py文件中添加bcrypt對象,如下所示:

# 在__init__.py文件中新增的內容
# ..
# 從flask_bcrypt中導入Bcrypt類
from flask_bcrypt import Bcrypt
# Bcrypt()的實例
bcrypt = Bcrypt()
# ..

在routes.py文件中新增註冊表單的路由函數register,如下所示:

# ..
from userauth_demo.forms import registerForm
from userauth_demo import db,bcrypt
from userauth_demo.models import User

# ..

@app.route("/register", methods=["GET", "POST"])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = registerForm()
    # 發送post請求
    if form.validate_on_submit():
        hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
        user = User(username=form.username.data, email=form.email.data, password=hashed_password)
        db.session.add(user)
        db.session.commit()
        flash('該用戶名已註冊,請登陸!', 'success')
        return redirect(url_for('login'))
    return render_template('register.html', title="第五天", html_form=form)

當用戶填寫完註冊信息,點擊註冊按鍵後,就會向後臺發送post請求,後臺接收到post請求後,會將用戶名、郵箱以及哈希後的密碼保存到數據庫中。註冊成功後,提示flash消息,並重定向到登陸頁面。如果是發送get請求,則直接會對模板進行渲染,顯示出註冊頁面的表單。

通過python run.py將web程序拉起,開始驗證結果,在瀏覽器中輸入http://127.0.0.1:5005/register,效果圖如下所示:

圖片描述

如圖,在註冊表單中輸入用戶名、郵箱及密碼,點擊註冊按鈕後,如果註冊成功,就會跳轉到下圖所示的登陸頁面。

圖片描述

4. 真正的登錄過程

上面我們在註冊頁面的最下方,添加了跳轉到登陸頁面的超鏈接,這裏也需要在登陸頁面的最下方添加跳轉到註冊頁面的超鏈接,方便未註冊的用戶快速找到註冊頁面。這裏在login.html文件的<form></form>標籤下方添加註冊超鏈接:

<! --login.html文件中添加註冊鏈接-->
# ..

<p>
       沒有賬號? <a  href="{{ url_for('register')}}">註冊</a>
</p>
# ..

效果如下所示:

圖片描述

4.1 登陸用戶

在第5天上篇的課程中,我們完成了登陸表單路由函數的實現。其中,當用戶發送post請求時,後臺直接使用了假的郵箱和密碼(不是從數據庫中讀取出來的用戶信息)進行驗證。但是真實的登錄過程中,後臺程序通過用戶輸入的郵箱,從數據庫中讀取出用戶的密碼信息,然後將數據庫中的密碼與用戶在前端輸入的密碼進行比較,如果用戶輸入的密碼是正確的,就將該用戶標記爲登錄狀態,否則就提示用戶密碼輸入錯誤。修改routes.py文件的內容,添加用戶登錄認證的代碼:

# routes.py文件中修改登陸表單路由函數
# ..
# 從userauth_demo中導入bcrypt實例
from userauth_demo import db, bcrypt

# 從flask_login中導入login_user函數
from flask_login import current_user, login_user

# 從userauth_demo.models中導入User模型
from userauth_demo.models import User

#.. 

@app.route('/login', methods=['GET','POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    # 對類LoginForm的實例
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user and bcrypt.check_password_hash(user.password, form.password.data):
            login_user(user, remember=form.remember.data)
        else:
            flash('登陸不成功,請檢查郵箱和密碼!', 'danger')
    return render_template('login.html', title='第五天', html_form=form)

# ..

我們對上面代碼的工作流程進行簡單的講解:首先,用戶發送post請求後,後臺接收到該請求後,通過用戶輸入的郵箱從數據庫中找到該用戶,再使用first()方法返回第一個匹配的用戶。接下來的if語句判斷該用戶是否存在,並且使用check_password_hash函數驗證用戶輸入的密碼是否和數據庫中的密碼一致,如果條件都滿足則會執行login_user()函數,並將用戶登陸狀態記爲已登陸,否則,會執行else語句,flash消息提示登陸不成功,請檢查郵箱和密碼。

4.2 頁面訪問權限控制

web開發過程中,經常會碰到網頁權限控制的問題,即有些頁面需要登錄狀態才能訪問,Flask_Login直接爲我們提供了該功能:如果用戶訪問的頁面需要登錄權限,而此時用戶不是處於登錄狀態,則會強制用戶跳轉到登錄頁面。爲了實現這個功能,Flask_Login需要知道哪個視圖函數用於處理登陸機制,在__init__.py文件中的添加如下代碼:

# 在__init__.py文件中添加內容

login_manager = LoginManager(app)
login_manager.login_view = 'login'   #login是登錄頁面視圖函數的函數名

Flask_Login使用@login_required裝飾器讓已通過認證的用戶進行訪問,如果將該裝飾器放到@app.route裝飾器下的視圖函數上面時,則該路由函數受到保護,不會讓匿名(未登錄)的用戶進行訪問,Flask-Login 會攔截請求,把用戶發往登錄頁面。對routes.py文件進行修改,如下所示:

# routes.py文件中添加內容
# ..
# 從flask_login中導入login_required類
from flask_login import current_user, login_user, login_required
# .. 
@app.route('/')
# 在主頁的視圖函數上面加上@login_required裝飾器
@login_required
def index():
    return render_template('index.html', title='第五天')

此時,我們在未登錄狀態下訪問主頁/,則會跳轉到如下圖所示的登錄頁面:

圖片描述

login_required裝飾器攔截請求並將主頁重定向到登錄頁面,此時URL中還包含其它的信息,如/login?next=%2F。next參數可以幫助用戶在登錄成功後,直接跳轉到之前沒有權限訪問的頁面。下面這段代碼,展示瞭如何查詢和處理next參數,在routes.py中進行修改:

# routes.py文件中添加內容
# 從flask中導入request
from flask import render_template, redirect, url_for, flash, request
# ..
@app.route('/login', methods=['GET','POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user and bcrypt.check_password_hash(user.password, form.password.data):
            login_user(user, remember=form.remember.data)
            # 查詢next參數
            nextpage = request.args.get('next')
            # 對next參數進行處理
            if nextpage:
                return redirect(nextpage)
            else:
                url = url_for('index')
                return redirect(url)
        else:
            flash('登陸不成功,請檢查郵箱和密碼!', 'danger')
    return render_template('login.html', title='第五天', html_form=form)
# ..

request.args.get可以得到next的參數賦值給nextpage,下面的if語句判斷nextpage是否存在,如果存在,則直接重定向到nextpage所在頁面,如果不存在,則會重定向到主頁。

下面,我們在登錄頁面輸入正確的用戶郵箱和密碼,點擊登陸按鈕後頁面將重定向到主頁,效果如下圖所示:

圖片描述

4.3 用戶註銷

通常當用戶登錄後,想要退出登錄狀態,那麼註銷功能也是必須的,借用Flask_Login的logout_user()函數,可以輕鬆的實現用戶的註銷功能。在routes.py文件中添註銷功能的代碼,如下:

# routes.py文件中添加的內容
# ..
# 從flask_login中導入logout_user類
from flask_login import current_user, login_user, login_required, logout_user

# ..

# 添加註銷功能的視圖函數
@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))

此外,還需要在基模板中添加註銷的前端代碼,修改layout.html文件中的內容:

<! -- layout.html文件中添加內容 -->
# ..
<header>
        <div>
            <a href="{{ url_for('index') }}">主頁</a>
            {% if current_user.is_authenticated() %}
                <a href="{{ url_for('logout') }}">註銷</a>
            {% else %}
                <a href="{{ url_for('login') }}">登陸</a>
            {% endif %}
        </div>
</header>

#..

添加完以上代碼後,我們只需刷新一下主頁,就會在主頁看到新增的註銷按鈕,效果圖如下所示:

圖片描述

5. 總結

學習完今天的內容,我們掌握瞭如下技能:

  1. 學習了Flask_Login插件的基本使用方法
  2. 學習瞭如何對明文密碼進行加密、比對
  3. 學習瞭如何利用數據庫完成用戶註冊功能
  4. 學習瞭如何實現用戶的登陸及頁面權限管理
  5. 學習瞭如何控制用戶的登錄狀態和註銷狀態

下一課的教程,貓姐將帶領大家一起學習個人主頁的實現及用戶頭像功能的管理。今天的內容就到這裏,喜歡的同學們可以在下面點贊留言,或是訪問我的博客地址:http://www.catonlinepy.tech/ 加入我們的QQ羣進一步交流學習!

6. 代碼的獲取

大家可以到github上獲取今天教程的所有代碼:https://github.com/miaojie19/...

具體下載代碼的命令如下:

# 使用git命令下載flask-course-primary倉庫所有的代碼
git clone https://github.com/miaojie19/flask-course-primary.git

# 下載完成後,進入day5目錄下面,即可看到今天的代碼
cd flask-course-primary
cd day5

圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章