本文為flask-wtf中的幾乎所有常用API實現,併在實例中運用flask-wtf包,同時包含了作者在學習開發階段的一些心得體會。 ...
簡介:簡單的集成flask,WTForms,包括跨站請求偽造(CSRF),文件上傳和驗證碼。
一.安裝(Install)
此文仍然是Windows操作系統下的教程,但是和linux操作系統下的運行環境相差甚微。
使用Python版本3.5.2.
上一篇文章提到Virtualenv環境運行Python,這次仍然建立Python虛擬運行環境以便實現不同數據包的隔離。
- 創建wtfdemo虛擬運行環境
用控制台(管理員運行模式)進入(cd)到想要創建工程的路徑下,創建wtfdemo文件夾。
mkdir wtfdemo
進入(cd)wtfdemo文件夾,創建Python虛擬運行環境。
virtualenv flaskr
出現如下字樣,說明虛擬環境創建成功
PS:本次提供第二種創建Python虛擬運行環境的使用方法
virtualenv -p source_file\python.exe target_file
為什麼提出第二種創建方法,你會發現,當你的Python Web程式開發多了以後,PC上難免安裝了很多版本的Python運行環境。
舉例:當PC上同時安裝了Python2.7和Python3.5,運行virtualenv flaskr後,建立的Python虛擬運行環境是Python2.7版本的,但是我們的開發環境是Python3.5。
在控制臺中輸入一下指令,你就會發現問題。
virtualenv -h
出現下麵圖片顯示,預設的virtualenv安裝路徑是Python2.7,也就是預設的安裝的虛擬環境是Python2.7版本。
所以,在這種情況下,請指定你需要的Python版本,並建立虛擬運行環境。
- 安裝flask-wtf庫文件
進入Python虛擬運行環境(在上一篇博文中寫過),執行以下指令。
pip install flask pip install flask-wtf pip install flask-login
pip install flask-sqlalchemy
出現如下圖所示,說明安裝成功。
flask安裝成功。
flask-wtf安裝成功。
flask-login安裝成功。(本次使用flask-wtf庫時需要輔助以該運行庫)
flask-sqlalchemy安裝成功。
至此,環境配置階段結束。
二.Flask-WTF
flask-wtf實現了簡單集成WTForms,包括CSRF,文件上傳以及Google內嵌的驗證碼。
特性:
①集成了WTForms。
②包含了帶有CSRF的安全表單
③全局的CSRF保護
④驗證碼Recaptcha的支持
⑤支持Flask-Upload的文件上傳
⑥多語言集成
但,成也集成敗也集成,flask-wtf不包含WTForms中許多重要的API,在進行複雜作業時顯得力不從心。
實際上,flask-wtf下整合的WTForm單元已經能基本滿足一些小型網站的建設使用了。
三.在實例中應用
- 文件結構
上一篇博文中,文件結構很是混亂,在實際維護中很不方便,會留下很多隱患,比如後期維護人員無法很快的切人項目中進行解讀代碼。
此處將各個模塊進行分離,抽離出各個不同的模塊。這樣的有點是單個文件只操作自己的功能,代碼可讀性強,邏輯清晰。缺點就是大型項目時文件會有很多碎小的文件,需要經常使用from import語句從不同的文件中引用出不同的功能。
但是相對於優點,缺點所帶來的風險是很小的。實際工作中,這樣的分文件管理項目是極力推薦的,邏輯清晰,可維護性高是每一個優秀的項目應該具有的最基本屬性。
本文采用以下文件結構。
wtfdemo→flaskr
→templates →login.html
→index.html
→uploads
→wtf.py
→model.py
→views.py
→run.py
→config.py
→form.py
本次應用實現一個簡單的用戶登錄功能模塊,文中涵蓋的flask-login包將在下一篇博文中詳細解釋,如果想馬上瞭解,請查看flask-login官方文檔。
- 詳細參數解析
config.py代碼如下所示。
1 import os 2 3 basedir = os.path.abspath(os.path.dirname(__file__)) 4 5 SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'test.db') 6 SQLALCHEMY_TRACK_MODIFICATIONS = False 7 8 CSRF_ENABLED = True 9 SECRET_KEY = 'you will never guess' 10 11 RECAPTCHA_PUBLIC_KEY = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J' 12 RECAPTCHA_PRIVATE_KEY = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu'
一句一句解釋
①OS模塊是Python標準庫中的一個用於訪問操作系統功能的模塊,OS模塊提供了一種可移植的方法使用操作系統的功能。
這裡利用os.path.abspath()函數,取出該文件下的絕對路徑。
②將取出的絕對路徑下存放到basedir中,以便被其他文件引用。
③SQLALCHEMY_DATABASE_URI用於連接資料庫,SQLALCHEMY_TRACK_MODIFICATIONS追蹤對象的修改並且發送信號,這裡在上一篇文章講解過。
④CSRF_ENABLED為是否開啟flask-wtf下的CSRF(跨站請求偽造)保護,True為開啟保護狀態。
⑤SECRET_KEY設置Flask的應用密匙,同時CSRF開啟保護時,同時需要一個密匙,如果不在文件中明確指出,通常與你的 Flask 應用密鑰一致。如果想要使用不同的密匙,請在文件中配置CSRF單獨的密匙。
WTF_CSRF_ENABLED = False
⑥RECAPTCHA_PUBLIC_KEY與RECAPTCHA_PRIVATE_KEY分別為,必需公鑰與必需私鑰。
wtf.py代碼如下。
1 from flask import Flask 2 from flask_sqlalchemy import SQLAlchemy 3 from config import SQLALCHEMY_DATABASE_URI, SQLALCHEMY_TRACK_MODIFICATIONS 4 from flask_login import LoginManager, AnonymousUserMixin 5 6 app = Flask(__name__) 7 app.config.from_object('config') 8 db = SQLAlchemy(app) 9 lm = LoginManager() 10 lm.init_app(app) 11 lm.login_view = 'login' 12 lm.login_message = 'Please log in!' 13 lm.login_message_category = 'info' 14 lm.anonymous_user = AnonymousUserMixin 15 lm.session_protection = 'strong' 16 lm.refresh_view = 'login' 17 lm.needs_refresh_message = 'Please enter your info' 18 lm.needs_refresh_message_category = "refresh_info" 19 20 from model import User 21 import views
①引用包中的函數說明。
Flask函數為flask架構的主函數,一切flask架構的Web開發都是由此函數引申出來的。
SQLAlchemy函數為flask-sqlalchemy包的主函數,由此函數綁定相應的flask架構應用,就可以實現連接資料庫的操作。
SQLALCHEMY_DATABASE_URI與SQLALCHEMY_TRACK_MODIFICATIONS為config.py中配置相應的參數,在此文件中被引用以便使用。
LoginManager函數是flask-login包的主函數,由此將flask架構和flask-login連接成一個整體。
②app.config.from_object函數將config文件下能配置到app的條目,綁定到app上。結果等同於直接綁定,等同結果如下。
app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = SQLALCHEMY_TRACK_MODIFICATIONS
③其他關於flask-login包下的函數,在下篇博文中將詳細的講解,本篇文章重點介紹flask-wtf包中的API使用。
form.py代碼構成如下。
1 from flask_wtf import Form 2 from wtforms import TextField, BooleanField 3 from wtforms.validators import DataRequired 4 from flask_wtf.recaptcha import RecaptchaField 5 6 from werkzeug import secure_filename 7 from flask_wtf.file import FileField 8 9 class LoginForm(Form): 10 username = TextField('username', validators=[DataRequired()]) 11 password = TextField('password', validators=[DataRequired()]) 12 remember_me = BooleanField('remember_me', default=False) 13 recaptcha = RecaptchaField() 14 15 class UploadForm(Form): 16 file = FileField()
①Form為flask-wtf的基礎form類型,一切的網頁表單都是繼承Form父類的屬性,並添加自己獨有的處理
②TextField, BooleanField是WTForm構建的文字域和覆選框域,對應HTML的<input type='text'/>和<input type='checkbox'/>標簽。
③DataRequired驗證TextField是否為空,為空返回一個錯誤信息(error message)
④在將文件保存在文件系統之前,要堅持使用secure_filename函數來確保文件名是安全的。
⑤flask-wtf整合了flask-upload中的部分函數,所以FileField不是從WTForm中引用出來的。FileField顧名思義,對應HTML標簽<input type='file'/>標簽。
⑥分析LoginForm類
1 class LoginForm(Form): 2 username = TextField('username', validators=[DataRequired()]) 3 password = TextField('password', validators=[DataRequired()]) 4 remember_me = BooleanField('remember_me', default=False) 5 recaptcha = RecaptchaField()
類成員構成: 變數名 = xxxField(),其中xxxField中可以選擇傳入<input/>標簽的name,validators屬性判斷如果TextBox中為None則返回錯誤消息
變數名可以在相對應的HTML文件中使用對應的 form.變數名 使用。
解析:此處添加兩個TextBox一個輸入用戶名,一個輸入密碼。一個CheckBox覆選框,實現記住用戶功能。最後一個為登陸時防止惡意登陸的驗證碼。
model.py代碼構成。
1 from wtf import db 2 from flask_login import UserMixin 3 4 class User(db.Model, UserMixin): 5 id = db.Column(db.Integer, primary_key=True) 6 username = db.Column(db.String(80), unique=True) 7 password = db.Column(db.String(80), unique=True) 8 9 def __init__(self, username, password): 10 self.username = username 11 self.password = password 12 13 def __repr__(self): 14 return '<User %r>' % self.username
①將wtf中模塊中的db參數引用到model.py中,以創建ORM資料庫映射關係
②其中UserMixin為flask-login中的用戶類,其中包含了一些可以在實現用戶登錄時驗證時需要的屬性。
③User類中定義id,用戶名及密碼屬性。__init__為User類的構造函數,__repr__方便在調試中輸出數據時使用。
views.py代碼構成。(下邊的代碼包含了Flask框架下的一些簡單應用,請在有一定Flask架構基礎的情況下,可以快速的切入下段代碼。其中flask-login在本文只一筆帶過,將在下篇博文中詳細解析flask-login包的API使用實例教程)
1 from flask import render_template, flash, redirect, session, url_for, request, g 2 from flask_login import login_user, logout_user, current_user, login_required, AnonymousUserMixin, fresh_login_required, login_fresh 3 from wtf import app, db, lm, User 4 from form import LoginForm, UploadForm 5 6 @app.before_request 7 def before_request(): 8 g.user = current_user 9 10 @lm.user_loader 11 def load_user(id): 12 user = User.query.filter_by(id=id).first() 13 return user 14 15 @app.route('/login', methods=['GET', 'POST']) 16 def login(): 17 if g.user is not None and g.user.is_authenticated: 18 login_fresh() 19 session['_fresh'] = False 20 return redirect(url_for('index')) 21 if g.user.is_active == True: 22 flash('active is True') 23 else: 24 flash('active is False') 25 form = LoginForm() 26 if form.validate_on_submit(): 27 user = User.query.filter_by(username=form.username.data, password=form.password.data).first() 28 if(user is not None): 29 login_user(user,remember=form.remember_me.data) 30 return redirect(request.args.get("next") or url_for('index')) 31 return render_template('login.html', form=form) 32 33 @app.route('/', methods = ['GET', 'POST']) 34 @app.route('/index', methods=['GET', 'POST']) 35 @login_required 36 def index(): 37 form = UploadForm() 38 39 if form.validate_on_submit(): 40 filename = secure_filename(form.file.data.filename) 41 form.file.data.save('uploads/' + filename) 42 return redirect(url_for('upload')) 43 44 return render_template('upload.html', form=form) 45 46 @app.route('/logout') 47 @login_required 48 def logout(): 49 logout_user() 50 return redirect(url_for('login'))
①from form import LoginForm, UploadForm, InfoForm
將form.py中定義的表單類引入到該文件中。
②before_request函數,定義為flask-login綁定用戶時使用,在發起請求之前首先調用此函數。
③load_user函數,定義為根據ID取出用戶實體。
④login函數,詳細分析:
@app.route('/login', methods=['GET', 'POST']) #接受前段POST和GET的表單信息,綁定到發佈/login頁面上 def login(): if g.user is not None and g.user.is_authenticated:#判斷用戶是否登錄過系統,或者是否使用過記住用戶這個功能
return redirect(url_for('index')) #如果滿足條件,則跳轉到/index界面
form = LoginForm() #實例化LoginForm() if form.validate_on_submit(): #validate_on_submit函數判斷form表單中是否有提交動作,如果有,則獲取該動作 user = User.query.filter_by(username=form.username.data, password=form.password.data).first()#獲取是否存在用戶(這種未加密登錄方法不推薦在實際項目中使用) if(user is not None): #判斷取出的資料庫用戶實體是否為空 login_user(user,remember=form.remember_me.data)#將用戶實體,記住用戶選項,綁定到flask-login中 return redirect(request.args.get("next") or url_for('index'))#重定向到next頁面或是index頁面 return render_template('login.html', form=form) #使用模板函數,將form綁定到login.html頁面上
⑤index函數分析
@app.route('/', methods = ['GET', 'POST']) #接受前段POST和GET的表單信息,綁定到發佈跟目錄上 @app.route('/index', methods=['GET', 'POST']) #接受前段POST和GET的表單信息,綁定到發佈/index頁面上 @login_required #驗證用戶是否登錄 def index():
form = UploadForm() #實例化UploadForm() if form.validate_on_submit(): #判斷表單中是否存在提交動作 filename = secure_filename(form.file.data.filename)#secure_filename函數確保文件名是安全,並賦值到filename中 form.file.data.save('uploads/' + filename) #將文件保存到工程目錄的位置 return redirect(url_for('index')) #上傳完文件後將頁面重定向到index.html return render_template('index.html', form=form) #使用模板函數
⑥logout函數分析
@app.route('/logout') #將函數綁定到/logout鏈接上 @login_required #驗證用戶是否登錄 def logout(): logout_user() #清空session中的緩存 return redirect(url_for('login'))#重定向到login.html頁面上
login.html代碼構成
<html> <head> Login-Demo </head> <body> <form method="POST" action=""> {% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }} </li> {% endfor %} </ul> <hr/> {% endif %} {% endwith %} {{form.csrf_token}} {{form.username(size=80)}} {% for error in form.username.errors %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> {{ form.password(size=80) }} {% for error in form.password.errors %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> <p>{{ form.remember_me }} Remember Me</p> <p></p> <p><input type="submit" value="Sign In"></p> <a href="{{ url_for('logout') }}">Logout</a> </form> </body> </html>
一下講解表單中各個語句的含義。
{% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }} </li> {% endfor %} </ul> <hr/> {% endif %} {% endwith %}
①get_flashed_messages函數獲取flask後端發動的flash消息。
②判斷如果是消息類型的話迴圈輸出
{{form.csrf_token}} {{form.username(size=80)}} {% for error in form.username.errors %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> {{ form.password(size=80) }} {% for error in form.password.errors %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> <p>{{ form.remember_me }} Remember Me</p> <p><input type="submit" value="Sign In"></p> <a href="{{ url_for('logout') }}">Logout</a>
①form.csrf_token設置開啟CSRF保護
②form.username(size=80),創建一個長度為80px的TextBox,對應前面提到的LoginForm創建的成員變數username
③for error in form.username.errors,此處抓取validators=[DataRequired()]返回的錯誤信息(error message)
④form.password(size=80),創建一個長度為80px的TextBox,對應前面提到的LoginForm創建的成員變數password
⑤form.remember_me,創建一個CheckBox,對應前面提到的LoginForm創建的成員變數remember_me
⑥<input type="submit" value="Sign In">添加一個按鈕,點擊後觸發form.validate_on_submit()函數
⑦<a href="{{ url_for('logout') }}">前段調用後端函數的一種方法,調用後端函數view.py中的logout函數
index.html代碼構成
<html> <head> {% if filedata %} <h3>{{ filedata.filename }}</h3> {% endif %} </head> <body> {% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }} </li> {% endfor %} </ul> <hr/> {% endif %} {% endwith %} <a href="{{ url_for('logout') }}">Logout</a> <form method="post" enctype="multipart/form-data"> {{ form.hidden_tag() }} {{ form.file }} <input type="submit"> </form> </body> </html>
將UploadForm類中的file呈現在頁面上。
最後,我們為了運行程式,添加run.py文件。
from wtf import app app.run(debug=True)
運行如下代碼,執行此次實常式序。
四.運行實常式序
打開瀏覽器輸入 http://127.0.0.1:5000/login,進入登錄界面。
關於資料庫的操作已經在上一篇博文里寫的很清楚了,在資料庫中已經添加了用戶(username=john,password=1)。
①當直接點擊Sign In按鈕之後,會出現如下圖顯示的效果。
出現上面[This field is required.]是from.py文件中validators=[DataRequired()]的功勞,這段代碼幫你驗證了提交表單中TextBox是否為空。
②當輸入用戶名和密碼之後,你會發現,點擊Sign In按鈕之後,並沒有跳轉到預期的index.html界面。
Q:為什麼沒有跳轉。
A:這和我們之前的一個疏忽造成的,之前的代碼我已經留下了一個位置修改這個問題。
下麵我們來分析,問題是點擊按鈕無法執行代碼中的跳轉,說明後端代碼綁定控制項的時候出現問題。
那是什麼問題呢,flask-wtf已經為你提供了一個可以查出問題的方法,使用form.errors檢索出在提交表單之後發生的問題。
在views.py文件的login函數中添加如下代碼(在實例化LoginForm之後添加)
flash(form.errors)
重新運行程式,輸入用戶名和密碼,點擊Sign In按鈕。
看到{'recaptcha': ['The response parameter is missing.']}你發現問題了麽,對的,就是因為筆者的一個不小心,沒有在HTML文件實例化recaptcha,由於驗證碼和按鈕有一個相互作用的關係,試想當你登錄一個系統,不輸入驗證碼直接點擊登錄,會發生一樣的問題。
解決辦法:
在login.html添加如下代碼。
<p>{{ form.remember_me }} Remember Me</p> <p>{{ form.recaptcha }}</p>#添加的代碼 <p><input type="submit" value="Sign In"/><p>
再次重新啟動程式,這回我們就會看到我們需要的樣子。
輸入用戶名和密碼,按驗證碼要求點擊相應的圖片,點擊Sign In按鈕,登錄成功!
點擊Choose File,讓我們上傳一個文件試試看,點擊upload按鈕之後,你會在項目根目錄下的Uploads文件夾找到你上傳的文件。
PS:本地運行並上傳文件,不會出現問題。如果你是發佈在網路中的時候,請註意一下幾個問題。
①圖片同名問題。
②上傳文件夾許可權問題。(筆者之前在項目中處理的辦法是將讀寫許可權賦予給everyone用戶,但是這樣會大大降低伺服器的安全性)
關於flask-wtf中文件上傳的一些解析,將在以後講解flask-upload的時候一起講解。
關於本文中的flask-login包,我們將在下一篇文章中解析。
以上就是基本的flask-wtf使用實例說明,歡迎探討轉載及引用參考,你們的回應是我的動力。
如果文中存在錯誤或是某個地方說的不夠精確,勞煩批評指出。
以上,辛苦了。
參考:
[1] github源碼:https://github.com/lepture/flask-wtf
[2] flask-wtf官方文檔:https://flask-wtf.readthedocs.io/en/latest/
*本文為Alima原創,如果轉載請聯繫筆者,轉載註明格式[轉載][博客園][Alima][關於flask-wtf中API使用實例教程],併在文首註明本文鏈接,多謝合作。
*非法轉載及非法抄襲博文將依照網路著作權流程辦理,請尊重作者勞動成果,最終解釋權歸Alima與博客園共同所有,感謝合作