目前爲止,雖然程序的功能大部分已經實現,但還缺乏一個很是重要的部分——用戶認證保護。頁面上的編輯和刪除按鈕是公開的,全部人均可以看到。假如咱們如今把程序部署到網絡上,那麼任何人均可以執行編輯和刪除條目的操做,這顯然是不合理的。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 提供了實現用戶認證須要的各種功能函數,咱們將使用它來實現程序的用戶認證,首先使用 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。