【百度云搜索,搜各種資料:http://bdy.lqkweb.com】
【搜網(wǎng)盤,搜各種資料:http://www.swpan.cn】
本文翻譯自The Flask Mega-Tutorial Part V: User Logins
這是Flask Mega-Tutorial系列的第五部分,我將告訴你如何創(chuàng)建一個用戶登錄子系統(tǒng)慨绳。
你在第三章中學會了如何創(chuàng)建用戶登錄表單翁狐,在第四章中學會了運用數(shù)據(jù)庫办铡。本章將教你如何結合這兩章的主題來創(chuàng)建一個簡單的用戶登錄系統(tǒng)。
本章的GitHub鏈接為:Browse, Zip, Diff.
密碼哈希
在第四章中百框,用戶模型設置了一個password_hash
字段闲礼,到目前為止還沒有被使用到。 這個字段的目的是保存用戶密碼的哈希值铐维,并用于驗證用戶在登錄過程中輸入的密碼柬泽。 密碼哈希的實現(xiàn)是一個復雜的話題,應該由安全專家來搞定方椎,不過聂抢,已經(jīng)有數(shù)個現(xiàn)成的簡單易用且功能完備加密庫存在了。
其中一個實現(xiàn)密碼哈希的包是Werkzeug棠众,當安裝Flask時琳疏,你可能會在pip的輸出中看到這個包,因為它是Flask的一個核心依賴項闸拿。 所以空盼,Werkzeug已經(jīng)安裝在你的虛擬環(huán)境中。 以下Python shell會話演示了如何哈希密碼:
>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'
在這個例子中新荤,通過一系列已知沒有反向操作的加密操作揽趾,將密碼foobar
轉換成一個長編碼字符串,這意味著獲得密碼哈希值的人將無法使用它逆推出原始密碼苛骨。 作為一個附加手段篱瞎,多次哈希相同的密碼苟呐,你將得到不同的結果,所以這使得無法通過查看它們的哈希值來確定兩個用戶是否具有相同的密碼俐筋。
驗證過程使用Werkzeug的第二個函數(shù)來完成牵素,如下所示:
>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, 'foobar')
True
>>> check_password_hash(hash, 'barfoo')
False
向驗證函數(shù)傳入之前生成的密碼哈希值以及用戶在登錄時輸入的密碼,如果用戶提供的密碼執(zhí)行哈希過程后與存儲的哈希值匹配澄者,則返回True
笆呆,否則返回False
。
整個密碼哈希邏輯可以在用戶模型中實現(xiàn)為兩個新的方法:
from werkzeug.security import generate_password_hash, check_password_hash
# ...
class User(db.Model):
# ...
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
使用這兩種方法粱挡,用戶對象現(xiàn)在可以在無需持久化存儲原始密碼的條件下執(zhí)行安全的密碼驗證赠幕。 以下是這些新方法的示例用法:
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('mypassword')
>>> u.check_password('anotherpassword')
False
>>> u.check_password('mypassword')
True
Flask-Login簡介
在本章中,我將向你介紹一個非常受歡迎的Flask插件Flask-Login询筏。 該插件管理用戶登錄狀態(tài)榕堰,以便用戶可以登錄到應用,然后用戶在導航到該應用的其他頁面時屈留,應用會“記得”該用戶已經(jīng)登錄局冰。它還提供了“記住我”的功能,允許用戶在關閉瀏覽器窗口后再次訪問應用時保持登錄狀態(tài)灌危】刀可以先在你的虛擬環(huán)境中安裝Flask-Login來做好準備工作:
(venv) $ pip install flask-login
和其他插件一樣,F(xiàn)lask-Login需要在app/__init__py
中的應用實例之后被創(chuàng)建和初始化勇蝙。 該插件初始化代碼如下:
# ...
from flask_login import LoginManager
app = Flask(__name__)
# ...
login = LoginManager(app)
# ...
為Flask-Login準備用戶模型
Flask-Login插件需要在用戶模型上實現(xiàn)某些屬性和方法沫勿。這種做法很棒,因為只要將這些必需項添加到模型中味混,F(xiàn)lask-Login就沒有其他依賴了产雹,它就可以與基于任何數(shù)據(jù)庫系統(tǒng)的用戶模型一起工作。
必須的四項如下:
-
is_authenticated
: 一個用來表示用戶是否通過登錄認證的屬性翁锡,用True
和False
表示蔓挖。 -
is_active
: 如果用戶賬戶是活躍的,那么這個屬性是True
馆衔,否則就是False
(譯者注:活躍用戶的定義是該用戶的登錄狀態(tài)是否通過用戶名密碼登錄瘟判,通過“記住我”功能保持登錄狀態(tài)的用戶是非活躍的)。 -
is_anonymous
: 常規(guī)用戶的該屬性是False
角溃,對特定的匿名用戶是True
拷获。 -
get_id()
: 返回用戶的唯一id的方法,返回值類型是字符串(Python 2下返回unicode字符串).
我可以很容易地實現(xiàn)這四個屬性或方法减细,但是由于它們是相當通用的匆瓜,因此Flask-Login提供了一個叫做UserMixin
的mixin類來將它們歸納其中。 下面演示了如何將mixin類添加到模型中:
# ...
from flask_login import UserMixin
class User(UserMixin, db.Model):
# ...
用戶加載函數(shù)
用戶會話是Flask分配給每個連接到應用的用戶的存儲空間,F(xiàn)lask-Login通過在用戶會話中存儲其唯一標識符來跟蹤登錄用戶驮吱。每當已登錄的用戶導航到新頁面時茧妒,F(xiàn)lask-Login將從會話中檢索用戶的ID,然后將該用戶實例加載到內(nèi)存中糠馆。
因為數(shù)據(jù)庫對Flask-Login透明嘶伟,所以需要應用來輔助加載用戶怎憋。 基于此又碌,插件期望應用配置一個用戶加載函數(shù),可以調用該函數(shù)來加載給定ID的用戶绊袋。 該功能可以添加到app/models.py模塊中:
from app import login
# ...
@login.user_loader
def load_user(id):
return User.query.get(int(id))
使用Flask-Login的@login.user_loader
裝飾器來為用戶加載功能注冊函數(shù)毕匀。 Flask-Login將字符串類型的參數(shù)id
傳入用戶加載函數(shù),因此使用數(shù)字ID的數(shù)據(jù)庫需要如上所示地將字符串轉換為整數(shù)癌别。
用戶登入
讓我們回顧一下登錄視圖函數(shù)皂岔,它實現(xiàn)了一個模擬登錄,只發(fā)出一個flash()
消息展姐。 現(xiàn)在躁垛,應用可以訪問用戶數(shù)據(jù),并知道如何生成和驗證密碼哈希值圾笨,該視圖函數(shù)就可以完工了教馆。
# ...
from flask_login import current_user, login_user
from app.models import User
# ...
@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(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
login()
函數(shù)中的前兩行處理一個非預期的情況:假設用戶已經(jīng)登錄,卻導航到應用的/login URL擂达。 顯然這是一個不可能允許的錯誤場景土铺。 current_user
變量來自Flask-Login,可以在處理過程中的任何時候調用以獲取用戶對象板鬓。 這個變量的值可以是數(shù)據(jù)庫中的一個用戶對象(Flask-Login通過我上面提供的用戶加載函數(shù)回調讀缺蟆),或者如果用戶還沒有登錄俭令,則是一個特殊的匿名用戶對象后德。 還記得那些Flask-Login必須的用戶對象屬性? 其中之一是is_authenticated
抄腔,它可以方便地檢查用戶是否登錄瓢湃。 當用戶已經(jīng)登錄,我只需要重定向到主頁妓柜。
相比之前的調用flash()
顯示消息模擬登錄箱季,現(xiàn)在我可以真實地登錄用戶。 第一步是從數(shù)據(jù)庫加載用戶棍掐。 利用表單提交的username藏雏,我可以查詢數(shù)據(jù)庫以找到用戶。 為此,我使用了SQLAlchemy查詢對象的filter_by()
方法掘殴。 filter_by()
的結果是一個只包含具有匹配用戶名的對象的查詢結果集男公。 因為我知道查詢用戶的結果只可能是有或者沒有,所以我通過調用first()
來完成查詢隙畜,如果存在則返回用戶對象;如果不存在則返回None吮播。 在第四章中,你已經(jīng)看到當你在查詢中調用all()
方法時病瞳, 將執(zhí)行該查詢并獲得與該查詢匹配的所有結果的列表揽咕。 當你只需要一個結果時,通常使用first()
方法套菜。
如果使用提供的用戶名執(zhí)行查詢并成功匹配亲善,我可以接下來通過調用上面定義的check_password()
方法來檢查表單中隨附的密碼是否有效。 密碼驗證時逗柴,將驗證存儲在數(shù)據(jù)庫中的密碼哈希值與表單中輸入的密碼的哈希值是否匹配蛹头。 所以,現(xiàn)在我有兩個可能的錯誤情況:用戶名可能是無效的戏溺,或者用戶密碼是錯誤的渣蜗。 在這兩種情況下,我都會閃現(xiàn)一條消息旷祸,然后重定向到登錄頁面耕拷,以便用戶可以再次嘗試。
如果用戶名和密碼都是正確的肋僧,那么我調用來自Flask-Login的login_user()
函數(shù)斑胜。 該函數(shù)會將用戶登錄狀態(tài)注冊為已登錄,這意味著用戶導航到任何未來的頁面時嫌吠,應用都會將用戶實例賦值給current_user
變量止潘。
然后,只需將新登錄的用戶重定向到主頁辫诅,我就完成了整個登錄過程凭戴。
用戶登出
提供一個用戶登出的途徑也是必須的,我將會通過Flask-Login的logout_user()
函數(shù)來實現(xiàn)炕矮。其視圖函數(shù)代碼如下:
# ...
from flask_login import logout_user
# ...
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
為了給用戶暴露登出鏈接么夫,我會在導航欄上實現(xiàn)當用戶登錄之后,登錄鏈接自動轉換成登出鏈接肤视。修改base.html模板的導航欄部分后档痪,代碼如下:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
用戶實例的is_anonymous
屬性是在其模型繼承UserMixin
類后Flask-Login添加的,表達式current_user.is_anonymous
僅當用戶未登錄時的值是True
邢滑。
要求用戶登錄
Flask-Login提供了一個非常有用的功能——強制用戶在查看應用的特定頁面之前登錄腐螟。 如果未登錄的用戶嘗試查看受保護的頁面,F(xiàn)lask-Login將自動將用戶重定向到登錄表單,并且只有在登錄成功后才重定向到用戶想查看的頁面乐纸。
為了實現(xiàn)這個功能衬廷,F(xiàn)lask-Login需要知道哪個視圖函數(shù)用于處理登錄認證。在app/__init__.py
中添加代碼如下:
# ...
login = LoginManager(app)
login.login_view = 'login'
上面的'login'
值是登錄視圖函數(shù)(endpoint)名汽绢,換句話說該名稱可用于url_for()
函數(shù)的參數(shù)并返回對應的URL吗跋。
Flask-Login使用名為@login_required
的裝飾器來拒絕匿名用戶的訪問以保護某個視圖函數(shù)。 當你將此裝飾器添加到位于@app.route
裝飾器下面的視圖函數(shù)上時宁昭,該函數(shù)將受到保護跌宛,不允許未經(jīng)身份驗證的用戶訪問。 以下是該裝飾器如何應用于應用的主頁視圖函數(shù)的案例:
from flask_login import login_required
@app.route('/')
@app.route('/index')
@login_required
def index():
# ...
剩下的就是實現(xiàn)登錄成功之后自定重定向回到用戶之前想要訪問的頁面久窟。 當一個沒有登錄的用戶訪問被@login_required
裝飾器保護的視圖函數(shù)時秩冈,裝飾器將重定向到登錄頁面,不過斥扛,它將在這個重定向中包含一些額外的信息以便登錄后的回轉。 例如丹锹,如果用戶導航到/index稀颁,那么@login_required
裝飾器將攔截請求并以重定向到/login來響應,但是它會添加一個查詢字符串參數(shù)來豐富這個URL楣黍,如/login?next=/index匾灶。 原始URL設置了next
查詢字符串參數(shù)后,應用就可以在登錄后使用它來重定向租漂。
下面是一段代碼阶女,展示了如何讀取和處理next
查詢字符串參數(shù):
from flask import request
from werkzeug.urls import url_parse
@app.route('/login', methods=['GET', 'POST'])
def login():
# ...
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
# ...
在用戶通過調用Flask-Login的login_user()
函數(shù)登錄后,應用獲取了next
查詢字符串參數(shù)的值哩治。 Flask提供一個request
變量秃踩,其中包含客戶端隨請求發(fā)送的所有信息。 特別是request.args
屬性业筏,可用友好的字典格式暴露查詢字符串的內(nèi)容憔杨。 實際上有三種可能的情況需要考慮,以確定成功登錄后重定向的位置:
- 如果登錄URL中不含
next
參數(shù)蒜胖,那么將會重定向到本應用的主頁消别。 - 如果登錄URL中包含
next
參數(shù),其值是一個相對路徑(換句話說台谢,該URL不含域名信息)寻狂,那么將會重定向到本應用的這個相對路徑。 - 如果登錄URL中包含
next
參數(shù)朋沮,其值是一個包含域名的完整URL蛇券,那么重定向到本應用的主頁。
前兩種情況很好理解,第三種情況是為了使應用更安全怀读。 攻擊者可以在next
參數(shù)中插入一個指向惡意站點的URL诉位,因此應用僅在重定向URL是相對路徑時才執(zhí)行重定向,這可確保重定向與應用保持在同一站點中菜枷。 為了確定URL是相對的還是絕對的苍糠,我使用Werkzeug的url_parse()
函數(shù)解析,然后檢查netloc
屬性是否被設置啤誊。
在模板中顯示已登錄的用戶
你還記得在實現(xiàn)用戶子系統(tǒng)之前的第二章中岳瞭,我創(chuàng)建了一個模擬的用戶來幫助我設計主頁的事情嗎? 現(xiàn)在蚊锹,應用實現(xiàn)了真正的用戶瞳筏,我就可以刪除模擬用戶了。 取而代之牡昆,我會在模板中使用Flask-Login的current_user
:
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
并且我可以在視圖函數(shù)傳入渲染模板函數(shù)的參數(shù)中刪除user
了:
@app.route('/')
@app.route('/index')
def index():
# ...
return render_template("index.html", title='Home Page', posts=posts)
這正是測試登錄和注銷功能運作機制的好時機姚炕。 由于仍然沒有用戶注冊功能,所以添加用戶到數(shù)據(jù)庫的唯一方法是通過Python shell執(zhí)行丢烘,所以運行flask shell
并輸入以下命令來注冊用戶:
>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()
如果啟動應用并嘗試訪問http://localhost:5000/或http://localhost:5000/index柱宦,會立即重定向到登錄頁面。在使用之前添加到數(shù)據(jù)庫的憑據(jù)登錄后播瞳,就會跳轉回到之前訪問的頁面掸刊,并看到其中的個性化歡迎。
用戶注冊
本章要構建的最后一項功能是注冊表單赢乓,以便用戶可以通過Web表單進行注冊忧侧。 讓我們在app/forms.py中創(chuàng)建Web表單類來開始吧:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User
# ...
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user is not None:
raise ValidationError('Please use a different email address.')
代碼中與驗證相關的幾處相當有趣。首先牌芋,對于email
字段蚓炬,我在DataRequired
之后添加了第二個驗證器,名為Email
姜贡。 這個來自WTForms的另一個驗證器將確保用戶在此字段中鍵入的內(nèi)容與電子郵件地址的結構相匹配试吁。
由于這是一個注冊表單,習慣上要求用戶輸入密碼兩次楼咳,以減少輸入錯誤的風險熄捍。 出于這個原因,我提供了password
和password2
字段母怜。 第二個password字段使用另一個名為EqualTo
的驗證器余耽,它將確保其值與第一個password字段的值相同。
我還為這個類添加了兩個方法苹熏,名為validate_username()
和validate_email()
碟贾。 當添加任何匹配模式validate_ <field_name>
的方法時币喧,WTForms將這些方法作為自定義驗證器,并在已設置驗證器之后調用它們袱耽。 本處杀餐,我想確保用戶輸入的username和email不會與數(shù)據(jù)庫中已存在的數(shù)據(jù)沖突,所以這兩個方法執(zhí)行數(shù)據(jù)庫查詢朱巨,并期望結果集為空史翘。 否則,則通過ValidationError
觸發(fā)驗證錯誤冀续。 異常中作為參數(shù)的消息將會在對應字段旁邊顯示琼讽,以供用戶查看。
我需要一個HTML模板以便在網(wǎng)頁上顯示這個表單洪唐,我其存儲在app/templates/register.html文件中钻蹬。 這個模板的構造與登錄表單類似:
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
登錄表單模板需要在其表單之下添加一個鏈接來將未注冊的用戶引導到注冊頁面:
<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
最后,我來實現(xiàn)處理用戶注冊的視圖函數(shù)凭需,存儲在app/routes.py中问欠,代碼如下:
from app import db
from app.forms import RegistrationForm
# ...
@app.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
這個視圖函數(shù)的邏輯也是一目了然,我首先確保調用這個路由的用戶沒有登錄功炮。表單的處理方式和登錄的方式一樣溅潜。在if validate_on_submit()
條件塊下,完成的邏輯如下:使用獲取自表單的username薪伏、email和password創(chuàng)建一個新用戶,將其寫入數(shù)據(jù)庫粗仓,然后重定向到登錄頁面以便用戶登錄嫁怀。
精雕細琢之后,用戶已經(jīng)能夠在此應用上注冊帳戶借浊,并進行登錄和注銷塘淑。 請確保你嘗試了我在注冊表單中添加的所有驗證功能,以便更好地了解其工作原理蚂斤。 我將在未來的章節(jié)中再次更新用戶認證子系統(tǒng)存捺,以增加額外的功能,比如允許用戶在忘記密碼的情況下重置密碼曙蒸。 不過對于目前的應用來講捌治,這已經(jīng)無礙于繼續(xù)構建了。