文章目錄
- 使用flask處理表單
- flash閃現的使用
- Flask實現文件上傳
- Session的使用
- 為什么session比cookie安全?
- 設置session
- 獲取session的值
- 刪除session的值或清空session所有內容
使用flask處理表單
傳統的前端通用表單,需要前后端共同完成操作,前端需要使用form
標簽來定義表單,而后端則需要使用request.form
來獲取post
請求中的表單數據:
# 判斷請求方式
if request.method == 'POST':# 獲取表單中name為username的文本域提交的數據name = request.form.get('username')# 獲取表單中name為password的文本域提交的數據password = request.form.get('password')return name+" "+password
- 上述的方法既沒有為表單提供保護措施,也不利于前后端分離的改進需求,固我們引入第三方擴展包:
flask-wtf
與wtforms
,來實現由后端單獨完成的表單操作:
wtforms
安裝:pip install wtforms
flask-wtf
安裝:pip install Flask-WTF
或pip install flask-wtf
wtforms
依照功能類別來說wtforms
分別由以下幾個類別:
Forms
: 主要用于表單驗證、字段定義、HTML
生成,并把各種驗證流程聚集在一起進行驗證。Fields
: 包含各種類型的字段,主要負責渲染(生成HTML
文本域)和數據轉換。Validator
:主要用于驗證用戶輸入的數據的合法性。比如Length
驗證器可以用于驗證輸入數據的長度。Widgets
:html
插件,允許使用者在字段中通過該字典自定義html
小部件。Meta
:用于使用者自定義wtforms
功能(配置),例如csrf
功能開啟。Extensions
:豐富的擴展庫,可以與其他框架結合使用,例如django
。
Flask-WTF
其實是對wtforms
的簡單集成,也能通過添加動態token
令牌的方式,為所有Form
表單提供免受CSRF
(Cross-site request forgery
——跨站請求偽造)攻擊的技術支持
我們可以采用以下方法來啟用CSRF
保護:
- 定義配置文件,再將配置文件中的配置語句通過
app.config.from_object
(<配置對象>)或app.config.from_pyfile
(<‘配置文件名’>)導入到flask
對象app
中,這個配置對象可以是配置模塊也可以是配置類:
# config.py
CSRF_ENABLED = TRUE # 用于開啟CSRF保護,但默認狀態下都是開啟的
SECRET_KEY = 'X1X2X3X4X5' # 用于生成動態令牌的秘鑰
- 其中
SECRET_KEY
用于建立加密令牌token
,在我們編寫程序時可以盡量定義的復雜一些;
from flask import Flask
from flask_wtf.csrf import CSRFProtect # 導入CSRFProtect模塊
import config # 導入配置文件app = Flask(__name__)
# 導入配置模塊中的配置
app.config.from_object(config)
# 為當前應用程序啟用WTF_CSRF保護,并返回一個CSRFProtect對象
csrf = CSRFProtect(app)
- 直接通過鍵值對的方式新增配置,即
app.config
[‘<配置名稱>’]=值添加配置到flask
對象app
中:
from flask import Flask
from flask_wtf.csrf import CSRFProtect # 導入CSRFProtect模塊app = Flask(__name__)
app.config['SECRET_KEY'] = 'ADJLAJDLA' # 用于生成動態令牌的秘鑰
app.config['CSRF_ENABLED'] = True # 用于開啟CSRF保護,但默認狀態下都是開啟的
# 為當前應用程序啟用WTF_CSRF保護,并返回一個CSRFProtect對象
csrf = CSRFProtect(app)
- 除了使用上述方法來配置
CSRF
保護,我們還需要用到flask_wtf
與wtfroms
來定義一個支持CSRF
保護的后端表單,我們一般將其定義在一個類當中;
該類需要繼承基類:flask_wtf.FlaskForm
或flask_wtf.Form
,二者完全相同,但Form
即將被FlaskForm
替換,推薦使用前者!
from flask import Flask,render_template,request
from flask_wtf.csrf import CSRFProtect
# 導入表單基類FlaskForm
from flask_wtf import FlaskForm
# 導入FlaskForm父類的表單字段組件(字符串文本域,密碼文本域,提交按鈕)
from wtforms import StringField,PasswordField,SubmitField
# 導入FlaskForm父類的表單驗證組件(數據不為空,數據是否相同,數據長度)
from wtforms.validators import DataRequired,EqualTo,Lengthapp = Flask(__name__)
# 配置加密匙,后端為了保護網站加入的驗證機制
# 不加會報錯:RuntimeError: A secret key is required to use CSRF.
app.config['SECRET_KEY'] = 'ADJLAJDLA'
# app.config['CSRF_ENABLED'] = True # 可以省略
csrf = CSRFProtect(app)# 定義表單模型類,繼承FlaskForm
class Register(FlaskForm):# 定義表單中的元素,類似于html的form中定義input標簽下的內容# label 用于點擊后跳轉到某一個指定的field框# validators 用于接收一個驗證操作列表# render_kw 用于給表單字段添加屬性,各屬性以鍵值對的形式設置user_name = StringField(label='用戶名:',validators=[DataRequired(message=u'用戶名不能為空'),Length(6,16,message='長度位于6~16之間')],render_kw={'placeholder':'輸入用戶名'})# message中存放判斷為錯誤時要返回的信息,EqualTo中第一個參數是要比較的field組件password = PasswordField(label='密碼:',validators=[DataRequired(message=u'密碼不能為空'),EqualTo('password2',message=u'兩次輸入需相同'),Length(6,16,message='長度位于6~16之間')],render_kw={'placeholder':'輸入密碼'})password2 = PasswordField(label='再次輸入密碼:', validators=[DataRequired(message=u'密碼不能為空'),Length(6,16,message='長度位于6~16之間')],render_kw={'placeholder':'再次輸入密碼'})submit = SubmitField(label='提交')@app.route('/',methods=['GET','POST'])
def register():# 實例化表單對象form = Register()if request.method == 'GET':# 表單對象發送至前端return render_template('register.html',form=form)elif request.method == 'POST':# form.validate_on_submit() 等價于:request.method=='post' and form.validate()# form.validate() 用于驗證表單的每個字段(控件),都滿足時返回值為Trueif form.validate_on_submit():username = form.user_name.datapassword = form.password.datapassword2 = form.password2.datareturn 'login success'else:# flask的form使用一個字典來儲存各控件的errors列表# print(type(form.errors))# 輸出密碼字段導致validate_on_submit為false的錯誤原因(兩種方式)print(form.errors['password'])print(form.password.errors)return render_template('register.html',form=form)if __name__ == '__main__':app.run()
- 前端中使用后端定義的表單,同樣也需要使用
jinja2
模板引擎,在{{ }}
中調用我們傳入的form
對象,且表單開頭需要使用form.csrf_token
或form.hidden_tag()
語句添加動態令牌(用戶不可見也不可編輯,用于驗證)
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>Flask_WTF</title><style type="text/css">.div1 {height:450px;width:400px;border:1px solid #8A8989;margin:0 auto;padding: 10px;}.input {display: block;width: 350px;height: 40px;margin: 10px auto;}.button{background: #2066C5;color: white;font-size: 18px;font-weight: bold;height: 50px;border-radius: 4px;}
</style>
</head>
<body>
<div class="div1"><form action="" method="post"><!-- 啟用CSRF驗證,將token令牌置于表單內 --><!-- 不添加該字段,后端驗證會一直為False -->{{ form.csrf_token }}{{ form.username.label }}<!-- 可以在變量后添加括號,并在括號內設置變量的屬性 -->{{ form.username(class='input',id='name',size=16) }}<!-- 錯誤展示 -->{% for e in form.username.errors %}<span style="color: red">{{ e }}</span>{% endfor %}<br>{{ form.password.label }}{{ form.password(class='input',id='pwd',size=16) }}<!-- 錯誤展示 -->{% for e in form.password.errors %}<span style="color: red">{{ e }}</span>{% endfor %}<br>{{ form.password2.label }}{{ form.password2(class='input',id='pwd2',size=16) }}<!-- 錯誤展示 -->{% for e in form.password2.errors %}<span style="color: red">{{ e }}</span>{% endfor %}<br>{{ form.submit(class='button') }}</form>
</div>
</body>
</html>
flash閃現的使用
導入:from flask import flash
后端的使用:flash("message")
,message
為消息內容;
前端通過遍歷get_flashed_messages()
獲取flash
消息內容;
示例代碼(部分):
# --------------視圖函數------------------
@app.route('/login/', methods=['GET', 'POST'])
def login():if request.method == 'GET':return render_template("flash.html")else:username = request.form.get('username')password = request.form.get('password')# user = User.query.filter(User.username == username, User.password == password).first()user = User.query.filter(User.username == username).first()if user and user.check_password(password):session['user_id'] = user.idsession['user_name'] = user.usernamesession.permanent = Truereturn redirect(url_for("index"))else:flash('用戶名或密碼不正確,請檢查!')return render_template('flash.html')# ---------------前端使用-----------------
<div class="warning">{% for message in get_flashed_messages() %}<div class="alert alert-warning alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button><strong>Warning!</strong> {{ message }}</div>{% endfor %}
</div>
Flask實現文件上傳
文件上傳指的是客戶端將文件上傳(post
請求發送)到服務器,服務器端進行保存的操作;
這一部分涉及到的庫和拓展知識過多,將解釋放在代碼注釋中,直接上代碼再做簡單的流程分析;
對于單文件的上傳,主要用到flask_wtf.file
庫下的上傳字段類:FileField
,以及檢驗組件:FileRequired
和 FileAllowed
;
后端應用程序文件"flask_file.py
":
from flask import Flask,render_template,redirect,url_for
from flask import send_from_directory,session,flash
from flask_wtf import FlaskForm
from wtforms import SubmitField
from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask_wtf.csrf import CSRFProtectapp = Flask(__name__)
app.config['SECRET_KEY'] = 'XASXA#@216.WDFAW'
csrf = CSRFProtect(app)# 自定義表單類
class UploadForm(FlaskForm):# flask_WTF中提供的上傳字段,label仍是字段的標簽# validators接收的驗證列表中:# FileRequired()用于驗證是否包含文件對象,可以設置參數message# FileAllowed()用于驗證文件的類型(后綴名),參數一為允許的文件后綴名的列表,參數二為可選的messagephoto = FileField(label='Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])])# 采用了wtforms提供的提交字段,flask_WTF中似乎不包含該字段submit = SubmitField()import os
# 驗證文件大小,可以通過修改配置,設置請求報文的最大長度
# 接收到超過長度的文件,服務器會中斷連接,并返回413錯誤響應
app.config['MAX_CONTENT_LENGTH'] = 1*1024*1024
# root_path獲取當前程序文件所處的目錄,使用path.join將其與uploads連接形成上傳路徑
# 將形成的路徑寫入到flask程序的配置當中,上傳到服務器的文件都會保存在當前目錄下的uploads目錄(需要手動預先創建)中
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')# 導入 通用唯一識別碼 庫
import uuid
# 自定義文件重命名方法
def random_filename(filename):# os.path.splitext將文件路徑與擴展名(文件類型標識)分開# 這里ext獲取了文件擴展名用于拼接生成新的文件名ext = os.path.splitext(filename)[1]# 將生成的隨機uuid與擴展名結合,形成新的文件名new_filename = uuid.uuid4().hex + ext# 返回新的文件名return new_filename# 文件展示
@app.route('/uploaded-images')
def show_images():# 響應顯示文件的模板return render_template('upload_file/uploaded.html')# 獲取文件路徑
@app.route('/uploads/<path:filename>')
def get_file(filename):# send_from_directory可以生成對應文件的下載鏈接# 參數一是所有文件的存儲目錄(即uploads目錄),參數二是該目錄下要下載的文件名return send_from_directory(app.config['UPLOAD_PATH'], filename)# 主程序
@app.route('/upload', methods=['GET', 'POST'])
def upload():# 實例化我們自定義的表單類UploadFormform = UploadForm()if form.validate_on_submit():# 使用表單對象form.photo的data屬性即可獲取到上傳的文件f = form.photo.data# 處理文件名,這里采用自定義的random_filename方法來實現filename =random_filename(f.filename)# 服務器端使用save方法來保存接收到的文件# 讀取配置中上傳文件夾的路徑,與文件名拼接后形成完整存儲路徑f.save(os.path.join(app.config['UPLOAD_PATH'], filename))# 使用flash通知用戶文件上傳成功flash('Upload success.')# 保存文件名到session,采用列表是為了后續拓展為多文件上傳session['filenames'] = [filename]# 上傳成功后顯示圖片,重定向到對應視圖函數return redirect(url_for('show_images'))# 響應上傳文件的模板,并把表單對象作為參數傳遞return render_template('upload_file/upload.html', form = form)
Session的使用
session
是基于cookie
實現的,也就是說二者之間是存在緊密聯系的
session
和cookie
都是由服務器生成的,都是用來存儲特定的值(鍵值對應);
session
是存儲在服務器的,而cookie
是會返回給客戶端的。
返回形式:置于響應信息頭 ——set-cookie
- 客戶端(瀏覽器)在發送請求的時候,會自動將存活、可用的
cookie
封裝在請求頭中和請求一起發送。- 發送形式:置于請求信息頭 ——
Cookie
cookie
和session
都是有其生命周期的;
cookie
的生命周期,一般來說,cookie
的生命周期受到兩個因素的影響cookie
自身的存活時間,是服務器生成cookie
時設定的;- 客戶端是否保留了
cookie
。客戶端是否保留cookie
,只對客戶端自身有影響,對其它封包工具是沒有影響的。session
的生命周期,一般來說,session
的生命周期也是受到兩個因素的影響:
服務器對于session
對象的保存的最大時間的設置。- 客戶端進程是否關閉。客戶端進程關閉,只對客戶端自身有影響,對其它封包工具是沒有影響的。
cookie
和session
都是有其作用域的。
為什么session比cookie安全?
cooke
是存儲在客戶端的,是(用戶)可見的,是(用戶)可以改變的;session
是存儲在服務器端的,是(用戶)不可見的,是(用戶)不可改變的。
當客戶端第一次請求session
對象時候,服務器會為客戶端創建一個session
,并將通過特殊算法算出一個session
的ID
,用來標識該session
對象;
sessionID
是一次會話的key
,如果客戶端的一次請求沒有攜帶sessionID
,那么服務器端就會為這次會話創建一個session
用于存儲內容,每個session
都有唯一的sessionID
;
設置session
- 服務器端接收到請求會自動創建
session
,我們可以通過session['name']='value'
方法來對session
內的值進行設置; name
即key
,是我們設置的變量名稱;value
則是變量的值;
from flask import Flask,session# 設置session
@app.route('/')
def set_session():# 將 username=zhangsan 存儲在session中# session的存儲與獲取都是字典方式session['username'] = 'zhangsan'return 'Session設置成功!'
獲取session的值
在設置了session
值后,我們有兩種方法來獲取我們設置的值,類似于字典的值的獲取,推薦使用第二種:
① result = session['name']
→ 如果內容不存在會報錯;
② result = session.get('name')
→ 如果內容不存在會返回None
;
刪除session的值或清空session所有內容
也是與字典中的操作類似:
①刪除單條session
值,可以采用session.pop('name')
方法;
②清空整個session
內容,則采用session.clear()
方法;
# 清除session
@app.route('/d')
def del_session():# 刪除session中username的這條記錄session.pop('username')# 清空session# session.clear()return 'Session被刪除!'
- 最終得到一個完整的服務器端
session
操作過程,代碼如下:
from flask import Flask,session
from datetime import timedeltaapp = Flask(__name__)import os
# 使用os庫下的urandom隨機生成秘鑰
app.config['SECRET_KEY'] = os.urandom(24)
# 配置session有效期為7天
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)# 設置session
@app.route('/')
def set_session():# 將 username=zhangsan 存儲在session中# session的存儲與獲取都是字典方式session['username'] = 'zhangsan'# 將有效期設為啟用session.permanent = Truereturn 'Session設置成功!'# 獲取session
@app.route('/g')
def get_session():# 通過字典方式獲取session中的username值username = session.get('username')return username or 'Session為空!'# 清除session
@app.route('/d')
def del_session():# 刪除session中username的這條記錄session.pop('username')# 清空session# session.clear()return 'Session被刪除!'if __name__ == '__main__':app.run(debug=True)