iT邦幫忙

2021 iThome 鐵人賽

DAY 26
0
Modern Web

30天肝出購物網站系列 第 26

Day26:26 - 優化 - 後端 & 前端 - 忘記密碼

γεια σας,我是Charlie!

在Day25當中我們完成了Email訂單通知,而今天我們將完成忘記密碼的部分。

================================◉‿◉=================================

我們的忘記密碼是要做成Email驗證的機制,所以必須要在使用者按下忘記密碼後後端發送Email,讓使用者點選Email中的連結後可以重設密碼。

所以我們先建立一個新的APP:resetPWD,並且加上URL連結:

keyboardmarket\urls.py

url('reset',include('resetPWD.urls')),

resetPWD\urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
	url(r'^$',views.resetPWD)
]

接著是views的部分,這裡的話會分成三種:

  1. 用戶按下忘記密碼時,必須產生token並且寄回去
  2. 用戶到忘記密碼頁面時,必須驗證token
  3. 用戶重設之後,必須重設資料庫的密碼

第一種是POST請求,而第二種是GET請求,第三種是PUT請求。

而我們這邊的TOKEN產生使用PyJWT,因為需要有期限、加密,故這邊使用。

首先先建立產生token跟解譯token的方法:

def makeResetToken(user):
	key = "Your random generate key"
	now = datetime.datetime.now()
	expiretime = now + datetime.timedelta(hours = 1)
	username = user.name
	payload = {
		"username":username,
		"exp":expiretime.timestamp()
	}
	return jwt.encode(payload,key,algorithm = 'HS256')


def decodeResetToken(token):
	key = "Your random generate key"
	try:
		res = jwt.decode(token,key,algorithms = ['HS256'])
	except jwt.ExpiredSignatureError:
		return R.badRequest("驗證超時,請重新操作!")
	except Exception as e:
		return R.internalServerError(str(e))
	else:
		return res["username"]

接著我們先建立post裡面的基本程式碼:

if request.method == "POST":
	req = request.body
	data = json.loads(req)
	if "username" not in data:
		return R.badRequest("username does not exist!")
	user = User.objects.filter(name = data["username"])
	if not user:
		return R.badRequest("user not found!")
	token = makeResetToken(user[0])
	return R.ok("reset password request success")

能產生token之後,接著我們要發送token到使用者的email,在templates當中建立reset資料夾,建立resetpassword.html:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>重設密碼</title>
</head>
<body>
	<h1>{{ username }}您好</h1>
	<p>您在keyboardmarket發起了重置密碼的請求</p>
	<p>密碼重置連結為:</p>
	<p>{{ token }}</p>
	<p>請在一小時內完成重置密碼動作,否則失效</p>
	<hr>
	<p>本信件為自動發送,請勿回覆</p>
</body>
</html>

接著在emailClient中新建send_reset_message方法:

def send_reset_message(self,
						username,
						token,
						user_email):
	email_template = render_to_string(
		'reset/resetpassword.html',
		{
			"username":username,
			"token":"http://localhost:8080/#/reset?token=" + token
		}
	)

	email = EmailMessage(
		"鍵盤貿易 - 重設密碼通知信",
		email_template,
		self.EMAIL_HOST_USER,
		[user_email]
	)
	email.content_subtype = 'html'
	email.fail_silently = False
	email.send()

然後到resetPWD views中新增寄送方法:

token = makeResetToken(user[0])
username = user[0].name
user_email = user[0].email
client.send_reset_message(username,token,user_email)
return R.ok("reset password request success")

再來是GET方法,GET方法是驗證token是否合法,所以在GET方法中建立檢查:

if request.method == "GET":
	req = request.GET
	if "token" not in req:
		return R.badRequest("token not found")
	token = req["token"]
	username = decodeResetToken(token)
	if type(username) != str:
		return username
	user = User.objects.filter(name = username)
	if not user:
		return R.badRequest("user not found")
	return R.ok("request is valid")

再來是PUT方法,修改密碼的部分:

if request.method == "PUT":
	req = request.body
	data = json.loads(req)
	if "password" not in data or "token" not in data:
		return R.badRequest("required parameter not found")
	username = decodeResetToken(data["token"])
	if type(username) != str:
		return username
	user = User.objects.filter(name = username)
	if not user:
		return R.badRequest("User not found")
	user = user[0]
	md5 = hashlib.md5()
	passwordString = data["password"] + username
	md5.update(passwordString.encode())
	pwd = md5.hexdigest()
	user.password = pwd
	user.save()
	return R.ok("update success")

再來是前端的部分,先在loginPage裡面加上超連結:

<a href="/#/createreset">忘記密碼?</a>

接著在components當中建立createreset.vue,讓使用者可以輸入用戶名稱以獲取email訊息:

<template>
	<html lang="zh-Hant-TW">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<link rel="icon" type="image/x-con" href="@/assets/favicon.ico">
		<div id="app">
			<headerComponent></headerComponent>
			<div id="resetHeader" style="width:100%;height:100px;background-image: linear-gradient(to right,#C9C9C9,#F2FFFF,#00E6E6);padding-top: 40px;">
				<h3>重置密碼</h3>
			</div>
			<div id="resetForm">
				<b-form @submit="onSubmit" style="width: 100%;">
					<b-form-group
					id="username+group"
					label="用戶名:"
					label-for="username"
					style="margin:10px;"
					>
						<b-form-input
						id="username"
						placeholder="請輸入用戶名"
						v-model="username"
						required
						>
						</b-form-input>
					</b-form-group>
					<b-button type="submit" variant="info" style="width: 80px;margin: 10px;">發送重置連結</b-button>
					<div id="result">
						<p>{{ resetText }}</p>
					</div>
				</b-form>
			</div>
		</div>
	</html>
</template>

接著在apis當中建立reset.js,新增發送重置密碼訊息的方法:

import { host,port } from '@/apis/constant.js'
import axios from 'axios'

export function createReset(username){
  return axios.post(`http://${host()}:${port()/reset`,{
    "username":username
  })
}

然後在createResetPage當中引入,並且新增onSubmit方法:

methods:{
  onSubmit(e){
    e.preventDefault()
    createReset(this.username).then((response) => {
      if(response.data.code == STATUS_OK){
        this.resetText = "重置信發送成功,請至當初登記信箱查收"
      }else{
        this.resetText = response.data.data
      }
    })
  }
}

可以測試看看能不能收的到信:
https://ithelp.ithome.com.tw/upload/images/20211010/20141666G8tMdjUa2A.png

https://ithelp.ithome.com.tw/upload/images/20211010/20141666dPX3MFtK9E.png

再來是reset的部分,先建立reset.vue,並加上路徑:

<template>
	<html lang="zh-Hant-TW">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<link rel="icon" type="image/x-con" href="@/assets/favicon.ico">
		<div id="app">
			<headerComponent></headerComponent>
			<div id="resetHeader" style="width:100%;height:100px;background-image: linear-gradient(to right,#C9C9C9,#F2FFFF,#00E6E6);padding-top: 40px;">
				<h3>重置密碼</h3>
			</div>
			<div id="resetForm" style="width: 50%;height: 500px;">
				<b-form @submit="onSubmit" style="width:100%;">
					<b-form-group
					id="password_group"
					label="密碼:"
					label-for="password"
					style="margin:10px;"
					>
						<b-form-input
						id="password"
						placeholder="請輸入密碼"
						v-model="password"
						required
						>
						</b-form-input>
					</b-form-group>
					<b-form-group
					id="password1_group"
					label="密碼2:"
					label-for="password1"
					style="margin:10px;"
					>
						<b-form-input
						id="password1"
						placeholder="請再次輸入密碼"
						v-model="password1"
						required
						>
						</b-form-input>
					</b-form-group>
					<b-button type="submit" variant="info" style="width: 80px;margin: 10px;">重設</b-button>
				</b-form>
			</div>
		</div>
	</html>
</template>

接著先建立驗證token的API程式碼:

export function resetValidate(token){
  return axios.get(`http://${host()}:${port()}/reset`,{
    params:{
      "token":token
    }
  })
}

然後在vue創立時驗證:

created(){
  var token = this.$route.query.token
  resetValidate(token).then((response) => {
    if(request.data.code == STATUS_OK){
      this.$fire({type:"success",text:"token驗證成功,請進行重置"})
    }else{
      this.$fire({type:"error",text:response.data.data}).then(() => {
        location.href = "/#/index"
      })
    }
  })
}

接著創立重置密碼的API方法,在reset.js中新增resetPassword方法:

export function resetPassword(password,token){
  return axios.put(`http://${host()}:${port()}/reset`,{
    "password":password,
    "token":token
  })
}

並修改onSubmit方法,成功的話則返回login Page:

onSubmit(e){
e.preventDefault()
if(this.password != this.password1){
  this.$fire({type:"error",text:"兩次密碼不一致"}).then(() => {
    return
  })
}
var token = this.$route.query.token
resetPassword(this.password,token).then((response) => {
  if(response.data.code == STATUS_OK){
    this.$fire({type:"success",text:"修改密碼成功,將導回登入頁"}).then(() => {
      location.href = "/#/login"
    })
  }else{
    this.$fire({type:"error",text:response.data.data}).then(() => {
      location.href = "/#/index"
    })
  }
})
}

就可以正常修改密碼了。

================================◉‿◉=================================

Day26結束了!在今天我們完成了修改密碼的前端跟後端的部分,而明天我們將開始實作google recaptcha的部分。


上一篇
Day25:25 - 優化 - 後端 - 訂單Email通知
下一篇
Day27:27 - 優化 - 後端 - recaptcha 驗證機制
系列文
30天肝出購物網站30

尚未有邦友留言

立即登入留言