# Flask實作_使用者登入功能_07_忘記密碼申請 ###### tags: `python` `flask` ## 說明 **『神仙打鼓有時錯,密碼忘掉誰人無』** 忘了某個網站的密碼這件事是非常正常的,這時候就需要透過申請並且驗證身份之後來重新設置一個密碼,不同於變更密碼可以用舊密碼來驗證身份,密碼遺失申請的話需要發送郵件到使用者的註冊Email,再做後續的邏輯處理,否則隨便路人申請都可以的話那就沒隱私了。 驗證的方法除了電子郵件之外,像google會有綁定手機並發送驗證碼到手機訊息,或是銀行會問你所就讀的國小,我們所實作的只是最簡單的。 作業流程如下: ```flow st=>start: 開始 e=>end: 結束 cond1=>condition: Email檢驗 op0=>operation: 點擊密碼遺失 op1=>operation: 點擊驗證連結 io1=>inputoutput: 輸入註冊帳號使用的電子郵件 io2=>inputoutput: 重設密碼 st->op0->io1->op1->io2->e io1->cond1 cond1(no)->e cond1(yes)->op1 ``` ## 作業說明 ### 增加超連結 首先在登入的頁面上加入密碼遺失申請以及帳號註冊的超連結,如下: :::success * 文件:`templates\author\login.html` * 說明:加入超連結 * 密碼遺失申請 * 帳號註冊 ```htmlmixed= {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Login Page{% endblock %} {% block page_content %} <div class="page-header"> <h1>Login</h1> <h2>Welcome To My Blog</h2> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} <p>FORGET PASSWORD..QQ <a href="{{ url_for('reset_password') }}">Click Here To Reset Password</a> </p> <p>JOIN US!.. <a href="{{ url_for('register') }}">Register Account</a> </p> </div> {% endblock %} ``` 第13行:加入忘記密碼超連結 第16行:加入註冊用戶超連結 版面非常的弱,請不要介意,在下毫無美術天份,如下: ![](https://i.imgur.com/CjNgWpL.png) ::: ### 密碼遺失申請表單 我們需要建置申請密碼的Form、Html與View function以及讓使用者驗證身份的郵件,如下: :::success * 文件:`app_blog\author\form.py` * 說明:增加申請密碼的表單 ```python= class FormResetPasswordMail(FlaskForm): """應用於密碼遺失申請時輸入郵件使用""" email = EmailField('Email', validators=[ validators.DataRequired(), validators.Length(5, 30), validators.Email() ]) submit = SubmitField('Send Confirm EMAIL') def validate_email(self, field): """ 驗證是否有相關的EMAIL在資料庫內,若沒有就不寄信 """ if not UserRegister.query.filter_by(email=field.data).first(): raise ValidationError('No Such EMAIL, Please Check!') ``` 第10行:驗證輸入的郵件是否存在資料庫內 ::: ### 密碼遺失申請頁面 :::success * 文件:`templates\author\resetpasswordemail.html` * 說明:建置密碼遺失申請的Html ```htmlmixed= {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Reset Password{% endblock %} {% block page_content %} <div class="page-header"> <h1>Forget Your PASSWORD?</h1> <h2>Input Your Email!</h2> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} </div> {% endblock %} ``` ::: ### 密碼遺失申請View Function :::success * 文件:`app_blog\author\view.py` * 說明:加入密碼遺失申請的View Function,先簡單建置路由測試 ```python= # 記得追加import剛才設置的form from app_blog.author.form import FormResetPasswordMail ..中略 @app.route('/resetpassword', methods=['GET', 'POST']) def reset_password(): form = FormResetPasswordMail() if form.validate_on_submit(): return 'RESET' return render_template('author/resetpasswordemail.html', form=form) ``` ::: #### 快速測試 :::warning * 測試 * 測試說明:記得在`before_request`加入例外endpoint * 測試連結:`http://127.0.0.1/login` * 測試說明:點擊剛才設置的忘記密碼申請超連結 ![](https://i.imgur.com/jUIe0d7.png) * 測試說明:故意輸入一個不存在的電子郵件,確認驗證失敗。 ![](https://i.imgur.com/ERT2xPw.png) ::: ### 調整Model-UserRegister 我們在使用者輸入驗證郵件之後會寄出驗證通知信,並且裡面要有一個超連結讓使用者連結到相對應的頁面。這跟啟動帳號一樣,對照如下: * 使用者註冊新帳號 * 使用者輸入郵件申請密碼遺失 * 寄出一個帶有時間限制的token * 寄出身份驗證郵件並且帶有token * 使用者點擊之後連回伺服器並且驗證token,沒問題就啟動帳號 * 使用者點擊之後連回伺服器並且驗證token,沒問題就設置新密碼 :::success * 文件:`app_blog\author\model.py` * 說明:使用者類別中加入新的method產生token ```python= class UserRegister(UserMixin, db.Model): #...中略...# def create_reset_token(self, expires_in=3600): """ 提供申請遺失密碼認證使用的token :param expires_in: 有效時間(秒) :return:token """ s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'], expires_in=expires_in) return s.dumps({'reset_id': self.id}) ``` 第3行:新增method生成遺失密碼認證token ::: ### 續調整View Function`reset_password` :::success * 文件:`app_blog\author\view.py` * 說明:加入寄送信件通知 ```python= @app.route('/resetpassword', methods=['GET', 'POST']) def reset_password(): # 只允許未登入的匿名帳號可以申請遺失密碼 if not current_user.is_anonymous: return redirect(url_for('index')) form = FormResetPasswordMail() if form.validate_on_submit(): # 取得使用者資料 user = UserRegister.query.filter_by(email=form.email.data).first() if user: # 產生一個token token = user.create_reset_token() # 寄出通知信 send_mail(sender='Yourmail@hotmail.com', # 嫌麻煩就直接透過參數來做預設 recipients=[user.email], subject='Reset Your Password', template='author/mail/resetmail', mailtype='html', user=current_user, token=token) flash('Please Check Your Email. Then Click link to Reset Password') # 寄出之後將使用者導回login,並且送出flash message return render_template(url_for('login')) return render_template('author/resetpasswordemail.html', form=form) ``` 程式碼說明皆寫於註解,雖然看起來很多,但只是單純的利用我們稍早建立的method來產生token,夾帶該token在驗證信件上。 ::: ### 建置驗證信件的版面 :::success * 文件:`templates\author\mail\resetmail.html` * 說明:新增寄出驗證信件的版面 ```htmlmixed= <h1>Hello {{ user.username }}, How are you!</h1> <h2>Please click the link to reset your password.</h2> hyperlink=>{{ url_for('reset_password_recive', token=token, _external=True) }} <h2>If you do not apply to the request , just delete it, but maybe somebody want try your account.</h2> ``` 第3行:記得加入參數`_external=True` ::: ### 建置使用者設置新密碼的表單 使用者點擊信件超連結之後會回到系統輸入新設置的密碼,設置完畢之後引導使用者到login重新登入。 Form、Html與View function,這是我們不變的SOP。 :::success * 文件:`app_blog\author\form.py` * 說明:建置使用者設置新密碼的表單 ```python= # 加入一個form來提供使用者輸入新的密碼 class FormResetPassword(FlaskForm): """使用者申請遺失密碼""" password = PasswordField('PassWord', validators=[ validators.DataRequired(), validators.Length(5, 10), validators.EqualTo('password_confirm', message='PASSWORD NEED MATCH') ]) password_confirm = PasswordField('Confirm PassWord', validators=[ validators.DataRequired() ]) submit = SubmitField('Reset Password') ``` 簡單的表單,兩個欄位並且加入驗證,確認密碼輸入正確 ::: ### 建置使用者設置新密碼的頁面 :::success * 文件:`templates\author\resetpassword.html` * 說明:使用者設置新密碼的頁面 ```htmlmixed= {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Reset Password{% endblock %} {% block page_content %} <div class="page-header"> <h1>Write Your Password</h1> <h2>Reset Password</h2> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} </div> {% endblock %} ``` ::: ### 建置使用者設置新密碼的View Function :::success * 文件:`app_blog\author\view.py` * 說明:使用者設置新密碼的View Function ```python= # 該得追加import剛才加入的form from app_blog.author.form import FormResetPassword #...中略...# @app.route('/resetpassword/<token>', methods=['GET', 'POST']) def reset_password_recive(token): """使用者透過申請連結進來之後,輸入新的密碼設置,接著要驗證token是否過期以及是否確實有其user存在 這邊使用者並沒有登入,所以記得不要很順手的使用current_user了。 """ if not current_user.is_anonymous: return redirect(url_for('index')) form = FormResetPassword() if form.validate_on_submit(): user = UserRegister() data = user.validate_confirm_token(token) if data: # 如果未來有需求的話,還要確認使用者是否被停權了。 # 如果是被停權的使用者,應該要先申請復權。 # 下面注意,複製過來的話記得改一下id的取得是reset_id,不是user_id user = UserRegister.query.filter_by(id=data.get('reset_id')).first() # 再驗證一次是否確實的取得使用者資料 if user: user.password = form.password.data db.session.commit() flash('Sucess Reset Your Password, Please Login') return redirect(url_for('login')) else: flash('No such user, i am so sorry') return redirect(url_for('login')) else: flash('Worng token, maybe it is over 24 hour, please apply again') return redirect(url_for('login')) return render_template('author/resetpassword.html', form=form) ``` 第9行:避免登入的使用者誤入這個Route,因此加入判斷,如果不是匿名就直接引導使用者離開 ::: ### 測試 :::warning * 測試 * 測試說明:下面為收到的驗證郵件 ![](https://i.imgur.com/1TtdpTv.png) * 測試說明:點擊超連結,引導我們到了設置密碼的頁面。 ![](https://i.imgur.com/Ysz4GrJ.png) * 測試說明:輸入新設定的密碼,重新登入確認正常。 ![](https://i.imgur.com/pSd8CUx.png) ::: ## 總結 我們完成了使用者登入的需求功能,並且在幾次的設置中了解到一個流程,就是Model、Form、Html與View function是開發新功能的SOP,當然如果沒有新增資料表需求的話就不需特別的再處理Model。 另外在用語上可能會對路由與View Function(視圖函數)有點混亂,View Function所指為裝飾器`@app.route`所裝飾的函數,而路由所指即為`@app.route`後所定義的路由,見下範例: ```python @app.route('/我是路由/') def 我是View Function(): pass ``` 接著,我們將進入頁面內容設置的說明,也代表專案會新增一個模組,為了管理上的便利,將會先導入Blueprint,後續繼續努力了。 **上一話:**[Flask實作_使用者登入功能_06_密碼變更](https://hackmd.io/s/BJYUlmudM) **下一話:**[Flask實作_開始建置頁面內容_00_開始建置頁面內容導論](https://hackmd.io/s/rJgkf2NKz)