Flask 教程 第五章:用戶登陸

本文翻譯自The Flask Mega-Tutorial Part V: User Loginshtml

這是Flask Mega-Tutorial系列的第五部分,我將告訴你如何建立一個用戶登陸子系統。git

你在第三章中學會了如何建立用戶登陸表單,在第四章中學會了運用數據庫。本章將教你如何結合這兩章的主題來建立一個簡單的用戶登陸系統。github

本章的GitHub連接爲:BrowseZipDiff.shell

密碼哈希

第四章中,用戶模型設置了一個password_hash字段,到目前爲止尚未被使用到。 這個字段的目的是保存用戶密碼的哈希值,並用於驗證用戶在登陸過程當中輸入的密碼。 密碼哈希的實現是一個複雜的話題,應該由安全專家來搞定,不過,已經有數個現成的簡單易用且功能完備加密庫存在了。數據庫

其中一個實現密碼哈希的包是Werkzeug,當安裝Flask時,你可能會在pip的輸出中看到這個包,由於它是Flask的一個核心依賴項。 因此,Werkzeug已經安裝在你的虛擬環境中。 如下Python shell會話演示瞭如何哈希密碼:flask

1 >>> from werkzeug.security import generate_password_hash 2 >>> hash = generate_password_hash('foobar') 3 >>> hash 4 'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'

在這個例子中,經過一系列已知沒有反向操做的加密操做,將密碼foobar轉換成一個長編碼字符串,這意味着得到密碼哈希值的人將沒法使用它逆推出原始密碼。 做爲一個附加手段,屢次哈希相同的密碼,你將獲得不一樣的結果,因此這使得沒法經過查看它們的哈希值來肯定兩個用戶是否具備相同的密碼。瀏覽器

驗證過程使用Werkzeug的第二個函數來完成,以下所示:安全

1 >>> from werkzeug.security import check_password_hash 2 >>> check_password_hash(hash, 'foobar') 3 True 4 >>> check_password_hash(hash, 'barfoo') 5 False
向驗證函數傳入以前生成的密碼哈希值以及用戶在登陸時輸入的密碼,若是用戶提供的密碼執行哈希過程後與存儲的哈希值匹配,則返回,不然返回TrueFalse

整個密碼哈希邏輯能夠在用戶模型中實現爲兩個新的方法:session

 1 from werkzeug.security import generate_password_hash, check_password_hash  2 
 3 # ...
 4 
 5 class User(db.Model):  6     # ...
 7 
 8     def set_password(self, password):  9         self.password_hash = generate_password_hash(password) 10 
11     def check_password(self, password): 12         return check_password_hash(self.password_hash, password)

使用這兩種方法,用戶對象如今能夠在無需持久化存儲原始密碼的條件下執行安全的密碼驗證。 如下是這些新方法的示例用法:app

1 >>> u = User(username='susan', email='susan@example.com') 2 >>> u.set_password('mypassword') 3 >>> u.check_password('anotherpassword') 4 False 5 >>> u.check_password('mypassword') 6 True

Flask-Login簡介

在本章中,我將向你介紹一個很是受歡迎的Flask插件Flask-Login。 該插件管理用戶登陸狀態,以便用戶能夠登陸到應用,而後用戶在導航到該應用的其餘頁面時,應用會「記得」該用戶已經登陸。它還提供了「記住我」的功能,容許用戶在關閉瀏覽器窗口後再次訪問應用時保持登陸狀態。能夠先在你的虛擬環境中安裝Flask-Login來作好準備工做:

(venv) $ pip install flask-login

和其餘插件同樣,Flask-Login須要在app/__init__py中的應用實例以後被建立和初始化。 該插件初始化代碼以下:

1 # ...
2 from flask_login import LoginManager 3 
4 app = Flask(__name__) 5 # ...
6 login = LoginManager(app) 7 
8 # ...

爲Flask-Login準備用戶模型

Flask-Login插件須要在用戶模型上實現某些屬性和方法。這種作法很棒,由於只要將這些必需項添加到模型中,Flask-Login就沒有其餘依賴了,它就能夠與基於任何數據庫系統的用戶模型一塊兒工做。

必須的四項以下:

  • is_authenticated: 一個用來表示用戶是否經過登陸認證的屬性,用TrueFalse表示。
  • is_active: 若是用戶帳戶是活躍的,那麼這個屬性是True,不然就是False(譯者注:活躍用戶的定義是該用戶的登陸狀態是否經過用戶名密碼登陸,經過「記住我」功能保持登陸狀態的用戶是非活躍的)。
  • is_anonymous: 常規用戶的該屬性是False,對特定的匿名用戶是True
  • get_id(): 返回用戶的惟一id的方法,返回值類型是字符串(Python 2下返回unicode字符串).

我能夠很容易地實現這四個屬性或方法,可是因爲它們是至關通用的,所以Flask-Login提供了一個叫作UserMixinmixin類來將它們概括其中。 下面演示瞭如何將mixin類添加到模型中:

1 # ...
2 from flask_login import UserMixin 3 
4 class User(UserMixin, db.Model): 5     # ...

用戶加載函數

用戶會話是Flask分配給每一個鏈接到應用的用戶的存儲空間,Flask-Login經過在用戶會話中存儲其惟一標識符來跟蹤登陸用戶。每當已登陸的用戶導航到新頁面時,Flask-Login將從會話中檢索用戶的ID,而後將該用戶實例加載到內存中。

由於數據庫對Flask-Login透明,因此須要應用來輔助加載用戶。 基於此,插件指望應用配置一個用戶加載函數,能夠調用該函數來加載給定ID的用戶。 該功能能夠添加到app/models.py模塊中:

1 from app import login 2 # ...
3 
4 @login.user_loader 5 def load_user(id): 6     return User.query.get(int(id))

使用Flask-Login的@login.user_loader裝飾器來爲用戶加載功能註冊函數。 Flask-Login將字符串類型的參數id傳入用戶加載函數,所以使用數字ID的數據庫須要如上所示地將字符串轉換爲整數。

用戶登入

讓咱們回顧一下登陸視圖函數,它實現了一個模擬登陸,只發出一個flash()消息。 如今,應用能夠訪問用戶數據,並知道如何生成和驗證密碼哈希值,該視圖函數就能夠完工了。

 1 # ...
 2 from flask_login import current_user, login_user  3 from app.models import User  4 
 5 # ...
 6 
 7 @app.route('/login', methods=['GET', 'POST'])  8 def login():  9     if current_user.is_authenticated: 10         return redirect(url_for('index')) 11     form = LoginForm() 12     if form.validate_on_submit(): 13         user = User.query.filter_by(username=form.username.data).first() 14         if user is None or not user.check_password(form.password.data): 15             flash('Invalid username or password') 16             return redirect(url_for('login')) 17         login_user(user, remember=form.remember_me.data) 18         return redirect(url_for('index')) 19     return render_template('login.html', title='Sign In', form=form)

login()函數中的前兩行處理一個非預期的狀況:假設用戶已經登陸,卻導航到應用的/login URL。 顯然這是一個不可能容許的錯誤場景。 current_user變量來自Flask-Login,能夠在處理過程當中的任什麼時候候調用以獲取用戶對象。 這個變量的值能夠是數據庫中的一個用戶對象(Flask-Login經過我上面提供的用戶加載函數回調讀取),或者若是用戶尚未登陸,則是一個特殊的匿名用戶對象。 還記得那些Flask-Login必須的用戶對象屬性? 其中之一是is_authenticated,它能夠方便地檢查用戶是否登陸。 當用戶已經登陸,我只須要重定向到主頁。

相比以前的調用flash()顯示消息模擬登陸,如今我能夠真實地登陸用戶。 第一步是從數據庫加載用戶。 利用表單提交的username,我能夠查詢數據庫以找到用戶。 爲此,我使用了SQLAlchemy查詢對象的filter_by()方法。 filter_by()的結果是一個只包含具備匹配用戶名的對象的查詢結果集。 由於我知道查詢用戶的結果只多是有或者沒有,因此我經過調用first()來完成查詢,若是存在則返回用戶對象;若是不存在則返回None。 在第四章中,你已經看到當你在查詢中調用all()方法時, 將執行該查詢並得到與該查詢匹配的全部結果的列表。 當你只須要一個結果時,一般使用first()方法。

若是使用提供的用戶名執行查詢併成功匹配,我能夠接下來經過調用上面定義的check_password()方法來檢查表單中隨附的密碼是否有效。 密碼驗證時,將驗證存儲在數據庫中的密碼哈希值與表單中輸入的密碼的哈希值是否匹配。 因此,如今我有兩個可能的錯誤狀況:用戶名多是無效的,或者用戶密碼是錯誤的。 在這兩種狀況下,我都會閃現一條消息,而後重定向到登陸頁面,以便用戶能夠再次嘗試。

若是用戶名和密碼都是正確的,那麼我調用來自Flask-Login的login_user()函數。 該函數會將用戶登陸狀態註冊爲已登陸,這意味着用戶導航到任何將來的頁面時,應用都會將用戶實例賦值給current_user變量。

而後,只需將新登陸的用戶重定向到主頁,我就完成了整個登陸過程。

用戶登出

提供一個用戶登出的途徑也是必須的,我將會經過Flask-Login的logout_user()函數來實現。其視圖函數代碼以下:

1 # ...
2 from flask_login import logout_user 3 
4 # ...
5 
6 @app.route('/logout') 7 def logout(): 8  logout_user() 9     return redirect(url_for('index'))

爲了給用戶暴露登出連接,我會在導航欄上實現當用戶登陸以後,登陸連接自動轉換成登出連接。修改base.html模板的導航欄部分後,代碼以下:

1     <div>
2  Microblog: 3         <a href="{{ url_for('index') }}">Home</a>
4         {% if current_user.is_anonymous %} 5         <a href="{{ url_for('login') }}">Login</a>
6         {% else %} 7         <a href="{{ url_for('logout') }}">Logout</a>
8         {% endif %} 9     </div>

用戶實例的is_anonymous屬性是在其模型繼承UserMixin類後Flask-Login添加的,表達式current_user.is_anonymous僅當用戶未登陸時的值是True

要求用戶登陸

Flask-Login提供了一個很是有用的功能——強制用戶在查看應用的特定頁面以前登陸。 若是未登陸的用戶嘗試查看受保護的頁面,Flask-Login將自動將用戶重定向到登陸表單,而且只有在登陸成功後才重定向到用戶想查看的頁面。

爲了實現這個功能,Flask-Login須要知道哪一個視圖函數用於處理登陸認證。在app/__init__.py中添加代碼以下:

1 # ...
2 login = LoginManager(app) 3 login.login_view = 'login'

上面的'login'值是登陸視圖函數(endpoint)名,換句話說該名稱可用於url_for()函數的參數並返回對應的URL。

Flask-Login使用名爲@login_required的裝飾器來拒絕匿名用戶的訪問以保護某個視圖函數。 當你將此裝飾器添加到位於@app.route裝飾器下面的視圖函數上時,該函數將受到保護,不容許未經身份驗證的用戶訪問。 如下是該裝飾器如何應用於應用的主頁視圖函數的案例:

1 from flask_login import login_required 2 
3 @app.route('/') 4 @app.route('/index') 5 @login_required 6 def index(): 7     # ...

剩下的就是實現登陸成功以後自定重定向回到用戶以前想要訪問的頁面。 當一個沒有登陸的用戶訪問被@login_required裝飾器保護的視圖函數時,裝飾器將重定向到登陸頁面,不過,它將在這個重定向中包含一些額外的信息以便登陸後的迴轉。 例如,若是用戶導航到/index,那麼@login_required裝飾器將攔截請求並以重定向到/login來響應,可是它會添加一個查詢字符串參數來豐富這個URL,如/login?next=/index。 原始URL設置了next查詢字符串參數後,應用就能夠在登陸後使用它來重定向。

下面是一段代碼,展現瞭如何讀取和處理next查詢字符串參數:

 1 from flask import request  2 from werkzeug.urls import url_parse  3 
 4 @app.route('/login', methods=['GET', 'POST'])  5 def login():  6     # ...
 7     if form.validate_on_submit():  8         user = User.query.filter_by(username=form.username.data).first()  9         if user is None or not user.check_password(form.password.data): 10             flash('Invalid username or password') 11             return redirect(url_for('login')) 12         login_user(user, remember=form.remember_me.data) 13         next_page = request.args.get('next') 14         if not next_page or url_parse(next_page).netloc != '': 15             next_page = url_for('index') 16         return redirect(next_page) 17     # ...

 

在用戶經過調用Flask-Login的login_user()函數登陸後,應用獲取了next查詢字符串參數的值。 Flask提供一個request變量,其中包含客戶端隨請求發送的全部信息。 特別是request.args屬性,可用友好的字典格式暴露查詢字符串的內容。 實際上有三種可能的狀況須要考慮,以肯定成功登陸後重定向的位置:

  • 若是登陸URL中不含next參數,那麼將會重定向到本應用的主頁。
  • 若是登陸URL中包含next參數,其值是一個相對路徑(換句話說,該URL不含域名信息),那麼將會重定向到本應用的這個相對路徑。
  • 若是登陸URL中包含next參數,其值是一個包含域名的完整URL,那麼重定向到本應用的主頁。

前兩種狀況很好理解,第三種狀況是爲了使應用更安全。 攻擊者能夠在next參數中插入一個指向惡意站點的URL,所以應用僅在重定向URL是相對路徑時才執行重定向,這可確保重定向與應用保持在同一站點中。 爲了肯定URL是相對的仍是絕對的,我使用Werkzeug的url_parse()函數解析,而後檢查netloc屬性是否被設置。

在模板中顯示已登陸的用戶

你還記得在實現用戶子系統以前的第二章中,我建立了一個模擬的用戶來幫助我設計主頁的事情嗎? 如今,應用實現了真正的用戶,我就能夠刪除模擬用戶了。 取而代之,我會在模板中使用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 %}

而且我能夠在視圖函數傳入渲染模板函數的參數中刪除user了:

1 @app.route('/') 2 @app.route('/index') 3 def index(): 4     # ...
5     return render_template("index.html", title='Home Page', posts=posts)

這正是測試登陸和註銷功能運做機制的好時機。 因爲仍然沒有用戶註冊功能,因此添加用戶到數據庫的惟一方法是經過Python shell執行,因此運行flask shell並輸入如下命令來註冊用戶:

1 >>> u = User(username='susan', email='susan@example.com') 2 >>> u.set_password('cat') 3 >>> db.session.add(u) 4 >>> db.session.commit()

若是啓動應用並嘗試訪問http://localhost:5000/http://localhost:5000/index,會當即重定向到登陸頁面。在使用以前添加到數據庫的憑據登陸後,就會跳轉回到以前訪問的頁面,並看到其中的個性化歡迎。

用戶註冊

本章要構建的最後一項功能是註冊表單,以便用戶能夠經過Web表單進行註冊。 讓咱們在app/forms.py中建立Web表單類來開始吧:

 1 from flask_wtf import FlaskForm  2 from wtforms import StringField, PasswordField, BooleanField, SubmitField  3 from wtforms.validators import ValidationError, DataRequired, Email, EqualTo  4 from app.models import User  5 
 6 # ...
 7 
 8 class RegistrationForm(FlaskForm):  9     username = StringField('Username', validators=[DataRequired()]) 10     email = StringField('Email', validators=[DataRequired(), Email()]) 11     password = PasswordField('Password', validators=[DataRequired()]) 12     password2 = PasswordField( 13         'Repeat Password', validators=[DataRequired(), EqualTo('password')]) 14     submit = SubmitField('Register') 15 
16     def validate_username(self, username): 17         user = User.query.filter_by(username=username.data).first() 18         if user is not None: 19             raise ValidationError('Please use a different username.') 20 
21     def validate_email(self, email): 22         user = User.query.filter_by(email=email.data).first() 23         if user is not None: 24             raise ValidationError('Please use a different email address.')

代碼中與驗證相關的幾處至關有趣。首先,對於email字段,我在DataRequired以後添加了第二個驗證器,名爲Email。 這個來自WTForms的另外一個驗證器將確保用戶在此字段中鍵入的內容與電子郵件地址的結構相匹配。

因爲這是一個註冊表單,習慣上要求用戶輸入密碼兩次,以減小輸入錯誤的風險。 出於這個緣由,我提供了passwordpassword2字段。 第二個password字段使用另外一個名爲EqualTo的驗證器,它將確保其值與第一個password字段的值相同。

我還爲這個類添加了兩個方法,名爲validate_username()validate_email()。 當添加任何匹配模式validate_ <field_name>的方法時,WTForms將這些方法做爲自定義驗證器,並在已設置驗證器以後調用它們。 本處,我想確保用戶輸入的username和email不會與數據庫中已存在的數據衝突,因此這兩個方法執行數據庫查詢,並指望結果集爲空。 不然,則經過ValidationError觸發驗證錯誤。 異常中做爲參數的消息將會在對應字段旁邊顯示,以供用戶查看。

我須要一個HTML模板以便在網頁上顯示這個表單,我其存儲在app/templates/register.html文件中。 這個模板的構造與登陸表單相似:

 1 {% extends "base.html" %}  2 
 3 {% block content %}  4     <h1>Register</h1>
 5     <form action="" method="post">
 6  {{ form.hidden_tag() }}  7         <p>
 8             {{ form.username.label }}<br>
 9             {{ form.username(size=32) }}<br>
10             {% for error in form.username.errors %} 11             <span style="color: red;">[{{ error }}]</span>
12             {% endfor %} 13         </p>
14         <p>
15             {{ form.email.label }}<br>
16             {{ form.email(size=64) }}<br>
17             {% for error in form.email.errors %} 18             <span style="color: red;">[{{ error }}]</span>
19             {% endfor %} 20         </p>
21         <p>
22             {{ form.password.label }}<br>
23             {{ form.password(size=32) }}<br>
24             {% for error in form.password.errors %} 25             <span style="color: red;">[{{ error }}]</span>
26             {% endfor %} 27         </p>
28         <p>
29             {{ form.password2.label }}<br>
30             {{ form.password2(size=32) }}<br>
31             {% for error in form.password2.errors %} 32             <span style="color: red;">[{{ error }}]</span>
33             {% endfor %} 34         </p>
35         <p>{{ form.submit() }}</p>
36     </form>
37 {% endblock %}

登陸表單模板須要在其表單之下添加一個連接來將未註冊的用戶引導到註冊頁面:

1     <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>

最後,我來實現處理用戶註冊的視圖函數,存儲在app/routes.py中,代碼以下:

 1 from app import db  2 from app.forms import RegistrationForm  3 
 4 # ...
 5 
 6 @app.route('/register', methods=['GET', 'POST'])  7 def register():  8     if current_user.is_authenticated:  9         return redirect(url_for('index')) 10     form = RegistrationForm() 11     if form.validate_on_submit(): 12         user = User(username=form.username.data, email=form.email.data) 13  user.set_password(form.password.data) 14  db.session.add(user) 15  db.session.commit() 16         flash('Congratulations, you are now a registered user!') 17         return redirect(url_for('login')) 18     return render_template('register.html', title='Register', form=form)

這個視圖函數的邏輯也是一目瞭然,我首先確保調用這個路由的用戶沒有登陸。表單的處理方式和登陸的方式同樣。在if validate_on_submit()條件塊下,完成的邏輯以下:使用獲取自表單的username、email和password建立一個新用戶,將其寫入數據庫,而後重定向到登陸頁面以便用戶登陸。

註冊表單

精雕細琢以後,用戶已經可以在此應用上註冊賬戶,並進行登陸和註銷。 請確保你嘗試了我在註冊表單中添加的全部驗證功能,以便更好地瞭解其工做原理。 我將在將來的章節中再次更新用戶認證子系統,以增長額外的功能,好比容許用戶在忘記密碼的狀況下重置密碼。 不過對於目前的應用來說,這已經無礙於繼續構建了。

相關文章
相關標籤/搜索