《Flask 入門教程》第 8 章:用戶認證

目前爲止,雖然程序的功能大部分已經實現,但還缺乏一個很是重要的部分——用戶認證保護。頁面上的編輯和刪除按鈕是公開的,全部人均可以看到。假如咱們如今把程序部署到網絡上,那麼任何人均可以執行編輯和刪除條目的操做,這顯然是不合理的。html

這一章咱們會爲程序添加用戶認證功能,這會把用戶分紅兩類,一類是管理員,經過用戶名和密碼登入程序,能夠執行數據相關的操做;另外一個是訪客,只能瀏覽頁面。在此以前,咱們先來看看密碼應該如何安全的存儲到數據庫中。git

安全存儲密碼

把密碼明文存儲在數據庫中是極其危險的,假如攻擊者竊取了你的數據庫,那麼用戶的帳號和密碼就會被直接泄露。更保險的方式是對每一個密碼進行計算生成獨一無二的密碼散列值,這樣即便攻擊者拿到了散列值,也幾乎沒法逆向獲取到密碼。github

Flask 的依賴 Werkzeug 內置了用於生成和驗證密碼散列值的函數,werkzeug.security.generate_password_hash() 用來爲給定的密碼生成密碼散列值,而 werkzeug.security.check_password_hash() 則用來檢查給定的散列值和密碼是否對應。使用示例以下所示:數據庫

>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> pw_hash = generate_password_hash('dog')  # 爲密碼 dog 生成密碼散列值
>>> pw_hash  # 查看密碼散列值
'pbkdf2:sha256:50000$mm9UPTRI$ee68ebc71434a4405a28d34ae3f170757fb424663dc0ca15198cb881edc0978f'
>>> check_password_hash(pw_hash, 'dog')  # 檢查散列值是否對應密碼 dog
True
>>> check_password_hash(pw_hash, 'cat')  # 檢查散列值是否對應密碼 cat
False複製代碼

咱們在存儲用戶信息的 User 模型類添加 username 字段和 password_hash 字段,分別用來存儲登陸所需的用戶名和密碼散列值,同時添加兩個方法來實現設置密碼和驗證密碼的功能:flask

from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    username = db.Column(db.String(20))  # 用戶名
    password_hash = db.Column(db.String(128))  # 密碼散列值

    def set_password(self, password):  # 用來設置密碼的方法,接受密碼做爲參數
        self.password_hash = generate_password_hash(password)  # 將生成的密碼保持到對應字段

    def validate_password(self, password):  # 用於驗證密碼的方法,接受密碼做爲參數
        return check_password_hash(self.password_hash, password)  # 返回布爾值複製代碼

由於模型(表結構)發生變化,咱們須要從新生成數據庫(這會清空數據):安全

$ flask initdb --drop複製代碼

生成管理員帳戶

由於程序只容許一我的使用,沒有必要編寫一個註冊頁面。咱們能夠編寫一個命令來建立管理員帳戶,下面是實現這個功能的 admin() 函數:網絡

import click

@app.cli.command()
@click.option('--username', prompt=True, help='The username used to login.')
@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='The password used to login.')
def admin(username, password):
    """Create user."""
    db.create_all()

    user = User.query.first()
    if user is not None:
        click.echo('Updating user...')
        user.username = username
        user.set_password(password)  # 設置密碼
    else:
        click.echo('Creating user...')
        user = User(username=username, name='Admin')
        user.set_password(password)  # 設置密碼
        db.session.add(user)

    db.session.commit()  # 提交數據庫會話
    click.echo('Done.')複製代碼

使用 click.option() 裝飾器設置的兩個選項分別用來接受輸入用戶名和密碼。執行 flask admin 命令,輸入用戶名和密碼後,便可建立管理員帳戶。若是執行這個命令時帳戶已存在,則更新相關信息:session

$ flask admin
Username: greyli
Password: 123  # hide_input=True 會讓密碼輸入隱藏
Repeat for confirmation: 123  # confirmation_prompt=True 會要求二次確認輸入
Updating user...
Done.複製代碼

使用 Flask-Login 實現用戶認證

擴展 Flask-Login 提供了實現用戶認證須要的各種功能函數,咱們將使用它來實現程序的用戶認證,首先使用 Pipenv 安裝它:app

$ pipenv install flask-login複製代碼

這個擴展的初始化步驟稍微有些不一樣,除了實例化擴展類以外,咱們還要實現一個「用戶加載回調函數」,具體代碼以下所示:ide

app.py:初始化 Flask-Login

from flask_login import LoginManager

login_manager = LoginManager(app)  # 實例化擴展類


@login_manager.user_loader
def load_user(user_id):  # 建立用戶加載回調函數,接受用戶 ID 做爲參數
    user = User.query.get(int(user_id))  # 用 ID 做爲 User 模型的主鍵查詢對應的用戶
    return user  # 返回用戶對象複製代碼

Flask-Login 提供了一個 current_user 變量,註冊這個函數的目的是,當程序運行後,若是用戶已登陸, current_user 變量的值會是當前用戶的用戶模型類記錄。

另外一個步驟是讓存儲用戶的 User 模型類繼承 Flask-Login 提供的 UserMixin 類:

from flask_login import UserMixin

class User(db.Model, UserMixin):
    # ...複製代碼

繼承這個類會讓 User 類擁有幾個用於判斷認證狀態的屬性和方法,其中最經常使用的是 is_authenticated 屬性:若是當前用戶已經登陸,那麼 current_user.is_authenticated 會返回 True, 不然返回 False。有了 current_user 變量和這幾個驗證方法和屬性,咱們能夠很輕鬆的判斷當前用戶的認證狀態。

登陸

登陸用戶使用 Flask-Login 提供的 login_user() 函數實現,須要傳入用戶模型類對象做爲參數。下面是用於顯示登陸頁面和處理登陸表單提交請求的視圖函數:

app.py:用戶登陸

from flask_login import login_user

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']

        if not username or not password:
            flash('Invalid input.')
            return redirect(url_for('login'))
        
        user = User.query.first()
        # 驗證用戶名和密碼是否一致
        if username == user.username and user.validate_password(password):
            login_user(user)  # 登入用戶
            flash('Login success.')
            return redirect(url_for('index'))  # 重定向到主頁

        flash('Invalid username or password.')  # 若是驗證失敗,顯示錯誤消息
        return redirect(url_for('login'))  # 重定向回登陸頁面
    
    return render_template('login.html')複製代碼

下面是包含登陸表單的登陸頁面模板:

templates/login.html:登陸頁面

{% extends 'base.html' %}

{% block content %}
<h3>Login</h3>
<form method="post">
    Username<br>
    <input type="text" name="username" required><br><br>
    Password<br>
    <!-- 密碼輸入框的 type 屬性使用 password,會將輸入值顯示爲圓點 -->
    <input type="password" name="password" required><br><br>
    <input class="btn" type="submit" name="submit" value="Submit">
</form>
{% endblock %}複製代碼

登出

和登陸相對,登出操做則須要調用 logout_user() 函數,使用下面的視圖函數實現:

from flask_login import login_required, logout_user

# ...

@app.route('/logout')
@login_required  # 用於視圖保護,後面會詳細介紹
def logout():
    logout_user()  # 登出用戶
    flash('Goodbye.')
    return redirect(url_for('index'))  # 重定向回首頁複製代碼

實現了登陸和登出後,咱們先來看看認證保護,最後再把對應這兩個視圖函數的登陸/登出連接放到導航欄上。

認證保護

在 Web 程序中,有些頁面或 URL 不容許未登陸的用戶訪問,而頁面上有些內容則須要對未登錄的用戶隱藏,這就是認證保護。

視圖保護

在視圖保護層面來講,未登陸用戶不能執行下面的操做:

  • 訪問編輯頁面
  • 訪問設置頁面
  • 執行註銷操做
  • 執行刪除操做
  • 執行添加新條目操做

對於不容許未登陸用戶訪問的視圖,只須要爲視圖函數附加一個 login_required 裝飾器就能夠將未登陸用戶拒之門外。以刪除條目視圖爲例:

@app.route('/movie/delete/<int:movie_id>', methods=['POST'])
@login_required  # 登陸保護
def delete(movie_id):
    movie = Movie.query.get_or_404(movie_id)
    db.session.delete(movie)
    db.session.commit()
    flash('Item deleted.')
    return redirect(url_for('index'))複製代碼

添加了這個裝飾器後,若是未登陸的用戶訪問對應的 URL,Flask-Login 會把用戶重定向到登陸頁面,並顯示一個錯誤提示。爲了讓這個重定向操做正確執行,咱們還須要把 login_manager.login_view 的值設爲咱們程序的登陸視圖端點(函數名):

login_manager.login_view = 'login'複製代碼

提示 若是你須要的話,能夠經過設置 login_manager.login_message 來自定義錯誤提示消息。

編輯視圖一樣須要附加這個裝飾器:

@app.route('/movie/edit/<int:movie_id>', methods=['GET', 'POST'])
@login_required
def edit(movie_id):
    # ...複製代碼

建立新條目的操做稍微有些不一樣,由於對應的視圖同時處理顯示頁面的 GET 請求和建立新條目的 POST 請求,咱們僅須要禁止未登陸用戶建立新條目,所以不能使用 login_required,而是在函數內部的 POST 請求處理代碼前進行過濾:

from flask_login import login_required, current_user

# ...

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        if not current_user.is_authenticated:  # 若是當前用戶未認證
            return redirect(url_for('index'))  # 重定向到主頁
        # ...複製代碼

最後,咱們爲程序添加一個設置頁面,支持修改用戶的名字:

app.py:支持設置用戶名字

from flask_login import login_required, current_user

# ...

@app.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
    if request.method == 'POST':
        name = request.form['name']
        
        if not name or len(name) > 20:
            flash('Invalid input.')
            return redirect(url_for('settings'))
        
        current_user.name = name
        # current_user 會返回當前登陸用戶的數據庫記錄對象
        # 等同於下面的用法
        # user = User.query.first()
        # user.name = name
        db.session.commit()
        flash('Settings updated.')
        return redirect(url_for('index'))
    
    return render_template('settings.html')複製代碼

下面是對應的模板:

templates/settings.html:設置頁面模板

{% extends 'base.html' %}

{% block content %}
<h3>Settings</h3>
<form method="post">
    Your Name <input type="text" name="name" autocomplete="off" required value="{{ current_user.name }}">
    <input class="btn" type="submit" name="submit" value="Save">
</form>
{% endblock %}複製代碼

模板內容保護

認證保護的另外一形式是頁面模板內容的保護。好比,不能對未登陸用戶顯示下列內容:

  • 建立新條目表單
  • 編輯按鈕
  • 刪除按鈕

這幾個元素的定義都在首頁模板(index.html)中,以建立新條目表單爲例,咱們在表單外部添加一個 if 判斷:

<!-- 在模板中能夠直接使用 current_user 變量 -->
{% if current_user.is_authenticated %}
<form method="post">
    Name <input type="text" name="title" autocomplete="off" required>
    Year <input type="text" name="year" autocomplete="off" required>
    <input class="btn" type="submit" name="submit" value="Add">
</form>
{% endif %}複製代碼

在模板渲染時,會先判斷當前用戶的登陸狀態(current_user.is_authenticated)。若是用戶沒有登陸(current_user.is_authenticated 返回 False),就不會渲染表單部分的 HTML 代碼,即上面代碼塊中 {% if ... %}{% endif %} 之間的代碼。相似的還有編輯和刪除按鈕:

{% if current_user.is_authenticated %}
	<a class="btn" href="{{ url_for('edit', movie_id=movie.id) }}">Edit</a>
	<form class="inline-form" method="post" action="{{ url_for('.delete', movie_id=movie.id) }}">
		<input class="btn" type="submit" name="delete" value="Delete" onclick="return confirm('Are you sure?')">
	</form>
{% endif %}複製代碼

有些地方則須要根據登陸狀態分別顯示不一樣的內容,好比基模板(base.html)中的導航欄。若是用戶已經登陸,就顯示設置和登出連接,不然顯示登陸連接:

{% if current_user.is_authenticated %}
	<li><a href="{{ url_for('settings') }}">Settings</a></li>
	<li><a href="{{ url_for('logout') }}">Logout</a></li>
{% else %}
	<li><a href="{{ url_for('login') }}">Login</a></li>
{% endif %}複製代碼

如今的程序中,未登陸用戶看到的主頁以下所示:



在登陸頁面,輸入用戶名和密碼登入:



登陸後看到的主頁以下所示:



本章小結

添加用戶認證後,在功能層面,咱們的程序基本算是完成了。結束前,讓咱們提交代碼:

$ git add .
$ git commit -m "User authentication with Flask-Login"
$ git push複製代碼

提示 你能夠在 GitHub 上查看本書示例程序的對應 commit:3944088

進階提示

相關文章
相關標籤/搜索