1. 程式人生 > >Flask Web 開發 使用者認證_6

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'
,form = form)

這裡需要注意的是: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會放把嘗試訪問的頁面的地址

最後,看一下我們書中的例子,書裡面並沒有很詳細地講到這一點,所以這個只是點被忽略了

-----------------------------------------------------------------------分割線:知識點結束----------------------------------------------------------------------------