使用Python的Flask框架表單插件Flask-WTF實(shí)現(xiàn)Web登錄驗(yàn)證
表單是讓用戶與我們的網(wǎng)頁(yè)應(yīng)用程序交互的基本元素。Flask 本身并不會(huì)幫助我們處理表單,但是 Flask-WTF 擴(kuò)展讓我們?cè)谖覀兊?Flask 應(yīng)用程序中使用流行的 WTForms 包。這個(gè)包使得定義表單和處理提交容易一些。
Flask-WTF
我們想要使用 Flask-WTF 做的第一件事情(在安裝它以后,GitHub項(xiàng)目頁(yè):https://github.com/lepture/flask-wtf )就是在 myapp.forms 包中定義一個(gè)表單。
# ourapp/forms.py from flask_wtf import Form from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, Email class EmailPasswordForm(Form): email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()])
在 Flask-WTF 0.9 版本以前,F(xiàn)lask-WTF 提供了針對(duì) WTForms 字段以及驗(yàn)證器的自己的封裝。你可能看到外面一大堆的代碼是從 flask.ext.wtforms 中不是從 wtforms 中導(dǎo)入 TextField,PasswordField。
在 Flask-WTF 0.9 版本以后,我們應(yīng)該直接從 wtforms 中導(dǎo)入這些字段和驗(yàn)證器。
我們定義的表單是一個(gè)用戶登錄表單。我們把它叫做 EmailPasswordForm(),我們可以重用這個(gè)同樣的表單類(lèi)(Form)去做其它的一些事情,像注冊(cè)表單。這里我們沒(méi)有去定義一個(gè)又長(zhǎng)又沒(méi)有用的表單,而是選擇一個(gè)很常用的表單,只是為了給你們介紹使用 Flask-WTF 定義表單的方式。也許以后在正式項(xiàng)目中會(huì)定義一個(gè)特別復(fù)雜表單。對(duì)于表單中包含字段名稱(chēng),我們建議使用一個(gè)清楚的名稱(chēng),并且在一個(gè)表單中保持唯一。不得不說(shuō),對(duì)于一個(gè)長(zhǎng)的表單,我們可能要給出一個(gè)更符合上文的字段名稱(chēng)。
登錄表單可以替我們做一些事情。它能夠保證我們應(yīng)用程序的安全以防止 CSRF 漏洞,驗(yàn)證用戶輸入并且渲染適當(dāng)?shù)臉?biāo)記,這些標(biāo)記是我們?yōu)楸韱味x的字段。
CSRF 保護(hù)和驗(yàn)證
CSRF 表示跨站請(qǐng)求偽造。CSRF 攻擊是指第三方偽造(像一個(gè)表單提交)請(qǐng)求到一個(gè)應(yīng)用程序的服務(wù)器。一個(gè)易受攻擊的服務(wù)器假設(shè)從一個(gè)表單來(lái)的數(shù)據(jù)是來(lái)自它自己的網(wǎng)站并且采取相應(yīng)的操作。
作為一個(gè)例子,比方說(shuō),一個(gè)郵件提供商可以讓你通過(guò)提交一個(gè)表單來(lái)刪除你的賬號(hào)。表單發(fā)送一個(gè) POST 請(qǐng)求到服務(wù)器上的 account_delete 端點(diǎn)并且當(dāng)表單被提交的時(shí)候刪除登錄的賬號(hào)。我們可以在自己的網(wǎng)站上創(chuàng)建一個(gè)表單,該表單發(fā)送一個(gè) POST 請(qǐng)求到同一個(gè) account_delete 端點(diǎn)?,F(xiàn)在,如果我們讓某人點(diǎn)擊我們表單的提交按鈕(或者通過(guò) JavaScript 來(lái)這樣做),郵件提供商提供的登錄賬號(hào)就會(huì)被刪除掉。當(dāng)然郵件提供商還不知道表單提交并不是發(fā)生在他們的網(wǎng)站上。
因此如何才能阻止 POST 請(qǐng)求來(lái)自別的網(wǎng)站?WTForms 通過(guò)在渲染每一個(gè)表單的時(shí)候生成一個(gè)唯一的令牌使得成為可能。生成的令牌會(huì)被傳回到服務(wù)器,伴隨著 POST 請(qǐng)求的數(shù)據(jù),在表單被接受之前令牌必須接受服務(wù)器的驗(yàn)證。關(guān)鍵的是令牌是與存儲(chǔ)在用戶會(huì)話(cookies)的一個(gè)值有關(guān)并且會(huì)在一段時(shí)間后失效(默認(rèn)是 30 分鐘)。這種方式就能夠保證提交一個(gè)有效表單的人就是加載頁(yè)面的人(或者至少是使用同一電腦的人),而且他們只能在加載頁(yè)面 30 分鐘內(nèi)這樣做。
要開(kāi)始使用 Flask-WTF 保護(hù) CSRF,我們需要為我們的登錄頁(yè)定義一個(gè)視圖。
# ourapp/views.py from flask import render_template, redirect, url_for from . import app from .forms import EmailPasswordForm @app.route('/login', methods=["GET", "POST"]) def login(): form = EmailPasswordForm() if form.validate_on_submit(): # Check the password and log the user in # [...] return redirect(url_for('index')) return render_template('login.html', form=form)
如果表單已經(jīng)被提交和驗(yàn)證的話,我們可以繼續(xù)登錄的邏輯。如果它沒(méi)有被提交的話(例如,只是一個(gè) GET 請(qǐng)求),我們就要把表單對(duì)象傳遞給我們的模板,以便它能夠被渲染。下面就是我們使用 CSRF 保護(hù)的時(shí)候模板的樣子。
{# ourapp/templates/login.html #} {% extends "layout.html" %} {% endraw %} <html> <head> <title>Login Page</title> </head> <body> <form action="{{ url_for('login') }}" method="post"> <input type="text" name="email" /> <input type="password" name="password" /> {{ form.csrf_token }} </form> </body> </html>
{% raw %}{{ form.csrf_token }}{% endraw %} 渲染了一個(gè)隱藏的字段,該字段包含那些奇特的 CSRF 令牌,并且當(dāng) WTForms 驗(yàn)證表單的時(shí)候會(huì)尋找這個(gè)字段。我們不用擔(dān)心包含處理令牌的邏輯,WTForms 會(huì)主動(dòng)幫我們?nèi)プ觥:猛郏?/p>
自定義驗(yàn)證
除了由 WTForms 提供的內(nèi)置的表單驗(yàn)證器(例如,Required(),Email() 等等),我們能創(chuàng)建我們自己的驗(yàn)證器。我們將通過(guò)編寫(xiě)一個(gè) Unique() 驗(yàn)證器來(lái)說(shuō)明如何創(chuàng)建自己的驗(yàn)證器,Unique() 驗(yàn)證器是用來(lái)檢查數(shù)據(jù)庫(kù)并且確保用戶提供的值在數(shù)據(jù)庫(kù)中不存在。這能夠用于確保用戶名或者郵箱地址還沒(méi)有使用。沒(méi)有 WTForms 的話,我們可能要在視圖中做這些事情,但是現(xiàn)在我們可以在表單本身做些事情。
現(xiàn)在我們來(lái)定義一個(gè)簡(jiǎn)單的注冊(cè)表單,其實(shí)這個(gè)表單和登錄的表單幾乎一樣。只是會(huì)在后面給它添加一些自定義的驗(yàn)證器。
# ourapp/forms.py from flask_wtf import Form from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, Email class EmailPasswordForm(Form): email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()])
現(xiàn)在我們要添加我們的驗(yàn)證器用來(lái)確保它們提供的郵箱地址不存在數(shù)據(jù)庫(kù)中。我們把這個(gè)驗(yàn)證器放在一個(gè)新的 util 模塊,util.validators。
# ourapp/util/validators.py from wtforms.validators import ValidationError class Unique(object): def __init__(self, model, field, message=u'This element already exists.'): self.model = model self.field = field def __call__(self, form, field): check = self.model.query.filter(self.field == field.data).first() if check: raise ValidationError(self.message)
這個(gè)驗(yàn)證器假設(shè)我們是使用 SQLAlchemy 來(lái)定義我們的模型。WTForms 期待驗(yàn)證器返回某種可調(diào)用的對(duì)象(例如,一個(gè)可調(diào)用的類(lèi))。
在 Unique() 的 \_\_init\_\_ 中我們可以指定哪些參數(shù)傳入到驗(yàn)證器中,在本例中我們要傳入相關(guān)的模型(例如,在我們例子中是傳入 User 模型)以及要檢查的字段。當(dāng)驗(yàn)證器被調(diào)用的時(shí)候,如果定義模型的任何實(shí)例匹配表單中提交的值,它將會(huì)拋出一個(gè) ValidationError。我們也可以添加一個(gè)具有通用默認(rèn)值的消息,它將會(huì)被包含在 ValidationError 中。
現(xiàn)在我們可以修改 EmailPasswordForm,使用我們自定義的 Unique 驗(yàn)證器。
# ourapp/forms.py from flask_wtf import Form from wtforms import StringField, PasswordField from wtforms.validators import DataRequired from .util.validators import Unique from .models import User class EmailPasswordForm(Form): email = StringField('Email', validators=[DataRequired(), Email(), Unique( User, User.email, message='There is already an account with that email.')]) password = PasswordField('Password', validators=[DataRequired()])
渲染表單
WTForms 也能幫助我們?yōu)楸韱武秩境?HTML 表示。WTForms 實(shí)現(xiàn)的 Field 字段能夠渲染成該字段的 HTML 表示,所以為了渲染它們,我們只必須在我們模板中調(diào)用表單的字段。這就像渲染 csrf_token 字段。下面給出了一個(gè)登錄模板的示例,在里面我們使用 WTForms 來(lái)渲染我們的字段。
{# ourapp/templates/login.html #} {% extends "layout.html" %} <html> <head> <title>Login Page</title> </head> <body> <form action="" method="post"> {{ form.email }} {{ form.password }} {{ form.csrf_token }} </form> </body> </html>
我們可以自定義如何渲染字段,通過(guò)傳入字段的屬性作為參數(shù)到調(diào)用中。
<form action="" method="post"> {{ form.email.label }}: {{ form.email(placeholder='yourname@email.com') }} <br> {% raw %}{{ form.password.label }}: {{ form.password }}{% endraw %} <br> {% raw %}{{ form.csrf_token }}{% endraw %} </form>
處理 OpenID 登錄
現(xiàn)實(shí)生活中,我們發(fā)現(xiàn)有很多人都不知道他們擁有一些公共賬號(hào)。一部分大牌的網(wǎng)站或服務(wù)商都會(huì)為他們的會(huì)員提供公共賬號(hào)的認(rèn)證。舉個(gè)栗子,如果你有一個(gè) google 賬號(hào),其實(shí)你就有了一個(gè)公共賬號(hào),類(lèi)似的還有 Yahoo, AOL, Flickr 等。
為了方便我們的用戶能簡(jiǎn)單的使用他們的公共賬號(hào),我們將把這些公共賬號(hào)的鏈接添加到一個(gè)列表,這樣用戶就不用自手工輸入了。
我們要把一些提供給用戶的公共賬號(hào)服務(wù)商定義到一個(gè)列表里面,這個(gè)列表就放到配置文件中吧 (fileconfig.py):
CSRF_ENABLED = True SECRET_KEY = 'you-will-never-guess' OPENID_PROVIDERS = [ { 'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id' }, { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, { 'name': 'AOL', 'url': 'http://openid.aol.com/<username>' }, { 'name': 'Flickr', 'url': 'http://www.flickr.com/<username>' }, { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }]
接下來(lái)就是要在我們的登錄視圖函數(shù)中使用這個(gè)列表了:
@app.route('/login', methods = ['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): flash('Login requested for OpenID="' + form.openid.data + '", remember_me=' + str(form.remember_me.data)) return redirect('/index') return render_template('login.html', title = 'Sign In', form = form, providers = app.config['OPENID_PROVIDERS'])
我們從 app.config 中引入了公共賬號(hào)服務(wù)商的配置列表,然后把它作為一個(gè)參數(shù)通過(guò) render_template 函數(shù)引入到模板。
接下來(lái)要做的我想你也猜得到,我們需要在登錄模板中把這些服務(wù)商鏈接顯示出來(lái)。
<!-- extend base layout --> {% extends "base.html" %} {% block content %} <script type="text/javascript"> function set_openid(openid, pr) { u = openid.search('<username>') if (u != -1) { // openid requires username user = prompt('Enter your ' + pr + ' username:') openid = openid.substr(0, u) + user } form = document.forms['login']; form.elements['openid'].value = openid } </script> <h1>Sign In</h1> <form action="" method="post" name="login"> {{form.hidden_tag()}} <p> Please enter your OpenID, or select one of the providers below:<br> {{form.openid(size=80)}} {% for error in form.errors.openid %} <span style="color: red;">[{{error}}]</span> {% endfor %}<br> |{% for pr in providers %} <a href="javascript:set_openid('{{pr.url}}', '{{pr.name}}');">{{pr.name}}</a> | {% endfor %} </p> <p>{{form.remember_me}} Remember Me</p> <p><input type="submit" value="Sign In"></p> </form> {% endblock %}
這次的模板添加的東西似乎有點(diǎn)多。一些公共賬號(hào)需要提供用戶名,為了解決這個(gè)我們用了點(diǎn) javascript。當(dāng)用戶點(diǎn)擊相關(guān)的公共賬號(hào)鏈接時(shí),需要用戶名的公共賬號(hào)會(huì)提示用戶輸入用戶名, javascript 會(huì)把用戶名處理成可用的公共賬號(hào),最后再插入到 openid 字段的文本框中。
下面這個(gè)是在登錄頁(yè)面點(diǎn)擊 google 鏈接后顯示的截圖:
- Python常用Web框架Django、Flask與Tornado介紹
- Python web框架(django,flask)實(shí)現(xiàn)mysql數(shù)據(jù)庫(kù)讀寫(xiě)分離的示例
- Python flask框架如何顯示圖像到web頁(yè)面
- python web框架Flask實(shí)現(xiàn)圖形驗(yàn)證碼及驗(yàn)證碼的動(dòng)態(tài)刷新實(shí)例
- 使用Python的Flask框架構(gòu)建大型Web應(yīng)用程序的結(jié)構(gòu)示例
- python的簡(jiǎn)單web框架flask快速實(shí)現(xiàn)詳解
相關(guān)文章
Python+Opencv實(shí)現(xiàn)計(jì)算閉合區(qū)域面積
這篇文章主要介紹了利用Python?Opencv計(jì)算閉合區(qū)域的面積的原理以及實(shí)現(xiàn)代碼,文中的講解詳細(xì)易懂,感興趣的小伙伴快跟隨小編一起學(xué)習(xí)一下吧2022-03-03Numpy中關(guān)于arctan和arctan2的區(qū)別
這篇文章主要介紹了Numpy中關(guān)于arctan和arctan2的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Python設(shè)計(jì)模式之策略模式實(shí)例詳解
這篇文章主要介紹了Python設(shè)計(jì)模式之策略模式,結(jié)合實(shí)例形式分析了策略模式的概念、原理并結(jié)合實(shí)例形式分析了Python定義與使用策略模式相關(guān)操作技巧,需要的朋友可以參考下2019-01-01python使用pandas讀取json文件并進(jìn)行刷選導(dǎo)出xlsx文件的方法示例
這篇文章主要介紹了python使用pandas讀取json文件并進(jìn)行刷選導(dǎo)出xlsx文件的方法,結(jié)合實(shí)例形式分析了python調(diào)用pandas模塊針對(duì)json數(shù)據(jù)操作的相關(guān)使用技巧,需要的朋友可以參考下2023-06-06Python利用matplotlib模塊數(shù)據(jù)可視化繪制3D圖
matplotlib是python最著名的繪圖庫(kù),它提供了一整套和matlab相似的命令A(yù)PI,十分適合交互式地行制圖,下面這篇文章主要給大家介紹了關(guān)于Python利用matplotlib模塊數(shù)據(jù)可視化實(shí)現(xiàn)3D圖的相關(guān)資料,需要的朋友可以參考下2022-02-02Python實(shí)現(xiàn)從網(wǎng)絡(luò)攝像頭拉流的方法分享
這篇文章主要為大家詳細(xì)介紹了Python實(shí)現(xiàn)從網(wǎng)絡(luò)攝像頭拉流的幾種方法,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解一下2023-01-01