# 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行:加入註冊用戶超連結
版面非常的弱,請不要介意,在下毫無美術天份,如下:

:::
### 密碼遺失申請表單
我們需要建置申請密碼的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`
* 測試說明:點擊剛才設置的忘記密碼申請超連結

* 測試說明:故意輸入一個不存在的電子郵件,確認驗證失敗。

:::
### 調整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
* 測試
* 測試說明:下面為收到的驗證郵件

* 測試說明:點擊超連結,引導我們到了設置密碼的頁面。

* 測試說明:輸入新設定的密碼,重新登入確認正常。

:::
## 總結
我們完成了使用者登入的需求功能,並且在幾次的設置中了解到一個流程,就是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)