Flask Web 開發 使用者認證_6
握草,終於進入使用者認證的最終章節了,覺得作者不錯,到了這裡,已經開始讓你嘗試自己寫程式碼了
雖然在github上面 Miguelgrinberg 也放上了程式碼,不過還是儘量自己寫吧
首先是對於已經註冊認證的使用者,他們有時候想修改密碼,那我們肯定要為使用者專門放一個頁面,用來修改密碼
那無非是做一個表單和頁面,通過表單來連結資料庫修改最後的密碼
以我們的經驗,一般這樣的表單有3行,老密碼,新密碼,確認新密碼
所以如下(本來form和路由都自己編名字了。。。。後來發現到後面和書對照起來太麻煩了。。。還是老實點先按照書來做吧。。。。)
class ChangePasswordForm(Form):
oldpassword=PasswordField('Oldpassword',validators=[Required()])
newpassword=PasswordField('Newpassowrd',validators=[Required(),EqualTo('newpassword2',message='Password must match.')])
newpassword2=PasswordField('Confirm password',validators=[Required()])
submit = SubmitField('Register')
而對應的路由設定如下:
@auth.route('/change_password',methods=['GET','POST']) #修改密碼頁面
@login_required #保護路由,說明要求是在登入狀態才能操作
def change_password():
form = ChangePasswordForm()
if form.validate_on_submit():
if current_user.verify_password(form.oldpassword.data): #在表單提交有效的情況下,如果當前使用者表單內輸入的老密碼驗證返回結果是True
current_user.password=form.newpassword.data #則當前使用者的密碼更新為表單裡面的newpassword(這裡用到的是password.setter裝飾器)
db.session(current_user) #提交更新
db.session.commit()
return redirect(url_for('main.index'))
else:
flash('Your oldpassword is wrong') #不然的話,出現提示訊息,老密碼錯誤
return render_template('auth/change_password.html'
這裡需要注意的是:current_user實際上可以作為object來直接使用的,db.session.add(current_user)就可以顯示這個作用
後端邏輯做完了,那前段頁面也要做一個
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky-Change Password{% endblock %}
{% block page_content %}
<div class="page-header">
<h3>Please reset your password as below</h3>
</div>
{{wtf.quick_form(form)}} #快速裝飾表單
{% endblock %}
效果圖如下:
需要注意的是,我們這裡添加了一個change password的按鈕,這個超連結最好還是放在base.html裡面
這樣我們的密碼修改功能就完成了
--------------------------------------------------------------------------功能分割線-----------------------------------------------------------------------------------------------
但是有時候你忘記了密碼,那你連改密碼都改不了
所以就要用到忘記密碼功能了,這個功能的作用基本上就是------>通過郵箱傳送給你一個連結------->點選連結進入修改密碼頁面--------->設定新密碼
對於我們平時的經驗來說,一般是通過郵箱先認證一下,再修改密碼
那這樣,等於是要有2個頁面產生,一個是讓你輸入郵箱併發送郵件的頁面,第二個是你郵箱點選連結返回過來的頁面,可以修改密碼
而且,需要做2個表單,一個是輸入郵箱併發送的表單,另外一個是修改密碼的表單
先來看要輸入email地址的表單類
class PasswordResetRequestForm(Form):
email=StringField('Email Address',validators=[Required(),Length(1,64),Email()])
submit = SubmitField('Send out') #提交表單以傳送EMAIL
再來看修改密碼的表單類
class PasswordResetForm(Form):
email = StringField('Email Address',validators=[Required(),Email()]) #這一行特別注意,如果沒有這一行的話,你到最後路由裡面,沒有辦法定位你的具體賬號的。
newpassword=PasswordField('Newpassowrd',validators=[Required(),EqualTo('newpassword2',message='Password must match.')])
newpassword2=PasswordField('Confirm password',validators=[Required()])
submit = SubmitField('Save Change')
def validate_email(self, field): #這裡前面學過的東西差點又忘記了,以validate_開頭的函式,會和普通驗證函式一起被呼叫
if User.query.filter_by(email=field.data).first() is None:
raise ValidationError('Unknown email address.')
有幾個新表單,那就有幾個新頁面,同時也要有幾個新路由
我們接著看新路由
@auth.route('/reset_password',methods=['GET','POST']) #密碼忘記通過郵件申請頁面
def password_reset_request(): #名字這樣取,容易記,是輸入email地址的頁面page
if not current_user.is_anonymous:
return redirect(url_for('main.index'))
form = SendResetEmail()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first() #通過表單上的email地址,查詢資料庫並把使用者資訊賦值給user
if user is None: #但是,不排除沒有這個email地址的可能,所以,如果user返回的結果是None的話
flash('This email does not exist , please check again !') #出現提示訊息,這個email不存在,請重新確認
else:
token = user.generate_reset_password_token() #如果使用者存在,則生成一個修改密碼的token,作用和前面的confirmation的token類似
send_email(user.email,'Reset Password','auth/email/rest_password',user=user,token=token,next=request.args.get('next'))
#EMAIL內容使用模板'auth/email/rest_password.html'來進行渲染,但是,最後為什麼要加上next這個引數和內容,始終沒搞懂
flash('Please check the email in your inbox !')
return render_template('auth/reset_password.html',form=form) #而整個輸入email地址的頁面,則通過send_reset_email_page.html來渲染
Email的渲染模板templates/auth/email/reset_password.html的內容,如下:
<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Flasky</b>!</p>
<p>To reset your password please <a href="{{ url_for('auth.reset_password',
token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.reset_password', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
模板內容,渲染PasswordResetRequestForm表單
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky-Reset Password Email{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Send Email to Reset</h1>
</div>
<div class='col-md-4'>
{{wtf.quick_form(form)}}
</div>
{% endblock %}
而上面我們用到一個函式,叫做generate_reset_password_token,這個函式的作用,其實和confirm裡面的token作用是相似的
只是對於重新設定密碼這個功能來說,我們是需要自己新建一個的,因為confirm裡面最後的目標是修改confirmed屬性,而我們這裡不需要,我們最終目標是修改密碼
那增加功能,就要在models裡面修改了,如下:
def generate_reset_password_token(self,expiration=3600):
s=Serializer(current_app.config['SECRET_KEY'],expiration)
return s.dumps({'reset':self.id}) #注意,這裡加密令牌時,key值自己定義為reset吧,這樣邏輯功能比較清晰點。
def reset_password(self,token,new_password):
s=Serializer(current.config['SECRET_KEY'])
try:
data=s.loads(token)
except:
return False
if data.get('reset') !=self.id:
return False
self.password = new_password #這裡,又用到了password.setter的裝飾器,來重新設定密碼
db.session.add(self)
return True
好,表單,模型,路由這些後端邏輯設定好了,接下去就是搞前段了
我覺得首先是和平時我們登入的網站一樣,要有一個按鈕來轉到傳送email這個頁面,就像我們平時看到的 “忘記密碼?”這樣類似
那我決定把他載入login的頁面上
效果圖如下:
雖然程式碼寫得有點ugly......5個<br>,不過位置圖效果出來還是挺滿意的,正好在輸入密碼的邊上,挺人性化
#
#
# 這裡留給收到的EMAIL郵件內容
#
#
收到EMAIL點選連結返回後
還需要我們配置一個路由,以及模板來顯示修改密碼的頁面
這裡貼2個版本,一個是原來我自己寫的,另外一個是作者的原始碼,我覺得原始碼是不是有點重複的地方.......
先貼原始碼
@auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
if not current_user.is_anonymous: #這一句確實有必要,判斷是否是可以登入的使用者
return redirect(url_for('main.index'))
form = PasswordResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None: #其實這個根據EMAIL找不到使用者的功能,我做在了傳送EMAIL的時候,如果找不到user,連EMAIL也不給你發
return redirect(url_for('main.index'))
if user.reset_password(token, form.password.data): #這個我要說一句,本來我的想法是,只要是通過連線返回回來的頁面,你通過passwordsetter裝飾器來修改就是了,
#何必再多寫一個函式來reset引數呢,後來我想想,作者這樣寫,把token帶進來,也許是出於安全考慮
flash('Your password has been updated.')
return redirect(url_for('auth.login'))
else:
return redirect(url_for('main.index'))
return render_template('auth/reset_password.html', form=form)
講到上面多寫的函式,我們需要重新回到models裡面,為User類新增內容
如下:
def reset_password(self,token,newpassword):
s=Serializer(current.config['SECRET_KEY'])
try:
data=s.loads(token)
except:
return False
if data.get('reset') != self.id:
return False
self.password = new_password
db.session.add(self)
db.session.commit()
return True
渲染的模板沒啥好多說,只是修改下title而已
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky-Reset Password{% endblock %}
{% block page_content %}
<div class="page-header">
<h3>Please reset your password</h3>
</div>
{{wtf.quick_form(form)}}
{% endblock %}
#
#
# 這裡留給頁面的截圖,回頭補
#
#
----------------------------------------------------------------功能分割線----------------------------------------------------------------------------------------------------------------------
終於來到這一章節的最後一點了,申請更換註冊的郵箱!
這和我們平時註冊網站時候碰到的更換繫結的郵箱的功能是一樣的
這章節的說明要求有點複雜,先看看原始碼是如何操作的,來分析下
首先,老規矩,要建立一個修改email地址的表單
class ChangeEmailForm(Form):
email = StringField('New email',validators=[Required(),Email()])
password = PasswordField('Password',validators=[Required(),Email()])
submit = SubmitField('Submit')
def validate_email(self,field): #同樣的作用,以validate_開頭的,會和一般的驗證函式一起作用,這裡主要是測試新email是否和原來一樣
if User.query.filter_by(email=form.email.data).first():
raise ValidationError('Email already registered !') #一樣的話則丟擲一個報錯,email已經被註冊
隨後,我認為是需要在models裡面新增函式,來生成修改email用的token了
def generate_email_change_token(self,new_email,expiration=3600): #這裡注意一下,都了一個new_mail的引數,還不知道作用,先往下看
s = Serializer(current_app.config['SECRET_KEY'],expiration)
return s.dumps({'change_email':self.id,'new_email':new_email})
def change_email(self,token): #這個更改繫結郵箱的確認函式,非常重要!因為和前面的都不一樣,用到了很多判斷語句,並且用到了新的模組hashlib
s=Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('change_email') != self.id: #如果在data中沒有找到change_email,則return False 我個人理解使用者身份驗證
return False
new_email = data.get('new_email') #如果在data裡面沒有找到new_email,這個key,則return False.......這個有點自相矛盾啊。。。
if new_email is None: #如果使用者沒有輸入new_email,則return False
return False
if self.query.filter_by(email = new_email).first() is not None: #如果以new_email來查詢,和使用者目前的email屬性相同的話,則return False,意思重複了
return False
self.email = new_email #如果以上情況均通過,則更新 email屬性的值,為new_email,即重新設定成功
self.avatar_hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
db.session.add(self)
db.session.commit()
return True
上面的程式碼最後部分,用到了hashlib的模組,我覺得廖雪峰老師的Python教程裡面關於這部分的講解非常詳細了,還講到了關於破解和反破解一方面的知識。
所以,我們在models裡面還需要import hashlib併為User類加入avatar_hash的類屬性
avatar_hash的值就等於通過hashlib的md5函式,將email值變成摘要資訊的內容.
模型部分的修改完成了,接著我們就要做最後一步,新增路由了
@auth.route('/change-email',methods=['GET','POST'])
@login_required
def change_email_request:()
form = ChangeEmailForm()
if form.validate_on_submit():
if current_user.verify_password(form.password.data): #如果使用者在表單上輸入的密碼和資料庫的匹配
new_mail = form.email.data #則將這個email地址賦值給new_email
token = current_user.generate_email_change_token(new_mail) #生成token
send_email(new_mail,'Confirm your email address','auth/email/change_email',user = current_user,token = token) #傳送email,包含new_mail
return redirect('main.index')
else:
flash ('invalid password or email!')
return render_template('auth/change_email.html',form = form)
傳送郵件的路由部分做完了以後,需要做模板來渲染
模板auth/change_email.html如下,沒啥好多說的,改個title而已
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky-Change Email{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change your email address</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
如下是你收到的email的內容的渲染模板templates/auth/email/change_email.html
#
# 這裡留給被渲染後的輸入email和密碼的表單畫面
# 這裡留給收到的email的內容截圖
#
#
點選email的連接回來以後,就顯示頁面,後臺邏輯檢測是否修改成功,但是我個人認為,這樣的功能應該放在傳送郵件前就昨晚
而修改繫結email的返回頁面則簡單的多,因為邏輯上的修改只需要用current_user.change_email來執行,只要是True,就返回flash訊息提示成功
@auth.route('/change-email/<token>',methods=['GET','POST'])
@login_required
def change_email(token):
if current_user.change_email(token):
flash('Your email address has been updated !')
else:
flash('Invalid request.')
return redirect(url_for('main.index'))
#
#
# 這裡留給點選email以後返回的畫面截圖
#
#
終於.....................第八章結束了,太漫長了...............不過知識點很多,需要鞏固,有些需要後期補看一下原始碼才能理解。
-------------------------------------------------------------分割線:額外的知識點-----------------------------------------------------------------------------
這裡碰到一個情況引發思考
如下圖,當你在未登入的情況下,嘗試點選change password進入此頁面時候,他會有一個flash訊息提示你,請登入後才有許可權訪問頁面
但是,我查看了所有的路由函式,並沒有找到設定這個flash訊息的地方
那這個訊息肯定是在哪個地方預設設定的咯?百度了一下,發現,他是在Flask-Login的login_view裡面設定的,所以我回過頭去看前面章節的設定
我們來看Flask-Login的官方文件,下面的紅框部分
首先第一點:LoginManager.login_message:這裡設定的就是預設的flash訊息,而這個訊息是用在user.login(我的例子裡是auth.login)頁面的。
如果需要修改的話,需要修改login_manager.login_message這個內容
第二點:在未登入狀態下,嘗試訪問一個需要登入狀態才能訪問的頁面時,login_view會放把嘗試訪問的頁面的地址
最後,看一下我們書中的例子,書裡面並沒有很詳細地講到這一點,所以這個只是點被忽略了
-----------------------------------------------------------------------分割線:知識點結束----------------------------------------------------------------------------