Ch12-1 : Forgot Passowrbd
[Forgot Passowrd]
忘記密碼部分,可以切分成三個階段。
Stage 1 : 在忘記密碼頁面,輸入註冊帳號,寄送忘記密碼重設信件。
Stage 2 : 使用者從信件點選重設密碼信,檢查信件連結是否正確。
Stage 3 : 使用者進行密碼重設。
以上就是我們忘記密碼時,會進行的三階段動作,下面我會分成三個階段,進行講解。
Stage 1 : 在忘記密碼頁面,輸入註冊帳號,寄送忘記密碼重設信件
就跟之前一樣,我們需要先準備page,routes,controller以及相關的Utils程式撰寫。
app.utils.signup.Utils_Signup.java
在開始寫主要功能前,我們需要先把其它小功能完成,寄信給使用者重設密碼時,我們會需要一個加密過的字串當作使用者唯一的連結,確保該連結,只對應到需要重設密碼的使用者。
/**
* 產生忘記密碼sha-256認證字串
* @param Member member 使用者資料
* @return SHA256 String
*/
public String genForgotPasswordTokenString(Member member){
Format formatter = new SimpleDateFormat("yyyyMMddHHmmss");
String time = formatter.format(new Date());
String text = time + member.getMemberNo() + member.getEmail();
return this.genSHA256String(text);
}
/**
* 傳入主要的需要認證字串,進行Sha256加密
* @param text
* @return token
*/
private String genSHA256String(String text){
String token = "";
try{
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(text.getBytes("UTF-8"));
byte[] digest = md.digest();
// %064x 意思是,產生64個字串長的字串
token = String.format("%064x", new java.math.BigInteger(1, digest));
} catch (Exception e) {
e.printStackTrace();
}
return token;
}
app.utils.mail.Utils_Email.java
我們還需要新增一個小功能是,當使用者在使用忘記密碼功能時,填寫完當初的帳號(電子信箱),檢查確認無誤之後。我們需要寄信給使用者,讓他可以點選連結回來,進行重設密碼的動作。
public Email genForgotPasswordEmail(Member member , String forgotPasswordTokenString){
Email email = new Email();
email.setFrom("[email protected]");
email.setTo(member.getEmail());
email.setSubject("[STAR] - 忘記密碼重設");
email.setText("");
email.setContent("<h2>您好 "+ member.getUsername()+",請在24小時內,點選以下重設密碼連結後,進行設定新密碼,謝謝!!</h2> "
+ "<a href='" +"http://127.0.0.1:9000/web/resetPassword?token="+forgotPasswordTokenString+"'>重設密碼連結</a>");
play.Logger.info("forgotPassword email = " + Json.toJson(email));
return email;
}
app.controllers.WebController.java
接下來開始寫忘記密碼的主要邏輯,我們可以看到doForgotPassword的註解。可以知道前面四個驗證步驟,都是為了檢查使用者輸入的帳號(電子信箱)是否能通過檢查,通過檢查後,才會寄送重設密碼信給使用者,進行重設密碼動作。
// 忘記密碼頁面
public Result forgotPassword(){
return ok(forgotPassword.render());
}
/**
* <pre>
* 執行忘記密碼檢查與寄送動作
*
* Step 1 : 確認表單,填寫是信箱
* Step 2 : 確認是否是否存在該會員資料
* Step 3 : 確認是否停權
* Step 4 : 確認是否尚未認證
* OK : 確認會員正常使用中,產生忘記密碼Token連結後,寄送信箱
* </pre>
*/
public Result doForgotPassword(){
// 清除暫存錯誤訊息
flash().clear();
// Step 1
String email = null;
try {
email = formFactory.form().bindFromRequest().get().getData().get("email").toString();
Logger.info("before , new forgotPassword request email = " + email);
} catch (Exception e) {
e.printStackTrace();
flash().put("error", "系統忙碌中,請稍候再嘗試,謝謝。");
return redirect(controllers.routes.WebController.forgotPassword().url());
} finally {
if(email==null){
Logger.error("表單內容非填寫信箱內容");
flash().put("error", "請重新填寫註冊信箱,謝謝。");
return redirect(controllers.routes.WebController.forgotPassword().url());
}
}
// Step 2
Member member = null;
try{
member = webService.findMemberByEmail(email);
} catch(Exception e){
e.printStackTrace();
flash().put("error", "系統忙碌中,請稍候再嘗試,謝謝。");
return ok(forgotPassword.render());
} finally {
if(member == null){
Logger.error("查無註冊資料");
flash().put("error", "查無註冊資料,請確認資料是否填寫正確,謝謝。");
return redirect(controllers.routes.WebController.forgotPassword().url());
}
}
// Step 3
if(MemberStatus.S3.getStatus().equals(member.getStatus())){
flash().put("error", "您的帳號,已被停權使用,無法使用忘記密碼功能,謝謝。");
play.Logger.warn("member = " + Json.toJson(member));
return redirect(controllers.routes.WebController.forgotPassword().url());
}
// Step 4
if(MemberStatus.S1.getStatus().equals(member.getStatus())){
flash().put("error", "您的帳號,尚未認證成功,無法使用忘記密碼功能,謝謝。");
play.Logger.warn("member = " + Json.toJson(member));
return redirect(controllers.routes.WebController.forgotPassword().url());
}
//OK
String forgotPasswordTokenString = "";
try{
forgotPasswordTokenString = new Utils_Signup().genForgotPasswordTokenString(member);
Map<String , String> memberToken = new HashMap<String , String>();
memberToken.put("memberNo", member.getMemberNo());
memberToken.put("tokenString", forgotPasswordTokenString);
memberToken.put("type", MemberTokenType.ForgotPassword.toString());
int isforgotPasswordStringOk = webService.genTokenData(memberToken);
Utils_Email utils_Email = new Utils_Email();
Email forgotPasswordMail = utils_Email.genForgotPasswordEmail(member, forgotPasswordTokenString);
boolean isSeadMailOk = utils_Email.sendMail(forgotPasswordMail);
play.Logger.info("isforgotPasswordStringOk = " + isforgotPasswordStringOk +" , isSeadMailOk = " + isSeadMailOk);
} catch (Exception e){
e.printStackTrace();
flash().put("error", "系統忙碌中,請稍候再嘗試,謝謝。");
play.Logger.info("token = " + forgotPasswordTokenString);
return redirect(controllers.routes.WebController.forgotPassword().url());
}
flash().put("ok", "已發送重設密碼信件至您的信箱,謝謝。");
return redirect(controllers.routes.WebController.forgotPassword().url());
}
app.views.web.loginSignup.forgotPassword.scala.html
忘記密碼頁面,因為這是一個全新功能,也別忘了,記得要修改headerNav.scala.html來新增忘記密碼連結。
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>忘記密碼</title>
@views.html.web.headerLibs()
@views.html.web.loginSignup.loginSignupLibs()
</head>
<body>
<div id="page-wrapper">
<div id="select_nav_signup">@views.html.web.headerNav()</div>
</div>
<div class="form">
<ul class="tab-group">
<li class="tab active"><a>忘記密碼</a></li>
</ul>
<div class="tab-content">
<div id="Form" >
<form action="@controllers.routes.WebController.doForgotPassword.url" method="post" id="forgotForm">
<div class="field-wrap">
<label class="lable-field-wrap">電子信箱</label>
<input type="email" required autocomplete="off" name="email"/>
@if(flash.containsKey("error")) {
<span style="color:red;">@flash.get("error")</span>
}
@if(flash.containsKey("ok")) {
<span style="color:green;">@flash.get("ok")</span>
}
</div>
<ul><li>忘記密碼,請輸入當初申請註冊的電子信箱,我們將會寄送重設密碼信件給您!</li></ul>
<ul>
<li class="home"><a href="@controllers.routes.WebController.index.url">回首頁</a></li>
<li class="forgot"><a href="@controllers.routes.WebController.forgotPassword.url">忘記密碼?</a></li>
</ul>
<a>
<input type="submit" value="SUBMIT" class="button button-block">
</a>
</form>
</div>
</div><!-- tab-content -->
</div> <!-- /form -->
<div id="titleBar"></div>
<script src='@routes.Assets.versioned("javascripts/loginSignup.js")'></script>
<script>
$( document ).ready(function() {
document.getElementById("forgotForm").reset();
});
@if(flash.containsKey("ok")){
document.getElementById("forgotForm").reset();
}
</script>
</body>
</html>
app.views.web.headerNav.scala.html
上方的選單新增加忘記密碼連結。以及要記得修正login.scala.html與signup.scala.html的忘記密碼連結。
<!-- Nav -->
<nav id="nav">
<ul>
<li id="nav_index"><a href="@controllers.routes.WebController.index.url">首頁</a></li>
<li id="nav_user">
<a>我的世界</a>
<ul>
<li><a href="@controllers.routes.WebController.login.url">登入</a></li>
<li><a href="@controllers.routes.WebController.logout.url">登出</a></li>
</ul>
</li>
<li id="nav_signup">
<a href="@controllers.routes.WebController.signup.url">註冊</a>
<ul>
<li><a href="@controllers.routes.WebController.resendAuthEmail.url">重發認證信</a></li>
<li><a href="@controllers.routes.WebController.forgotPassword.url">忘記密碼</a></li>
</ul>
</li>
</ul>
</nav>
<script>
<!-- 來偵測,外面的Div被誰使用到,而去增加class current屬性-->
$(document).ready(function() {
if ( $('#select_nav_index').length ){
$('#nav_index').addClass('current');
}
if ( $('#select_nav_user').length ){
$('#nav_user').addClass('current');
}
if ( $('#select_nav_signup').length ){
$('#nav_signup').addClass('current');
}
});
</script>
<a href="index.html" id="logo">Star</a>
con.routes
相關服務寫好之後,最後就是新增相關服務在routes上。
# http://127.0.0.1:9000/web/forgotPassword
GET /web/forgotPassword controllers.WebController.forgotPassword()
# http://127.0.0.1:9000/web/forgotPassword
POST /web/forgotPassword controllers.WebController.doForgotPassword()
[Stage 1 Test Case]
這部份是要進行第一階段程式測試。
CASE 1 : 表單填寫不是帳號(電子信箱)
CASE 2 : 該帳號並沒有註冊過。
CASE 3 : 該帳號已經被停權,這種情況下,不能使用忘記密碼功能。
CASE 4 : 該會員已經加入會員,但是尚未認證時,也無法使用忘記密碼功能。
OK : 以上都驗證確認無誤之後,就會顯示重設密碼信給使用者。
使用者收到忘記密碼重設信件
寫入一筆資料到member_token,類型是ForgotPassword。
Stage 2 : 使用者從信件點選重設密碼信,檢查信件連結是否正確
當我們成功寄送重設密碼信後,接下來需要驗證連結是否正確。
app.views.web.loginSignup.resetPassword.scala.html
一樣我們需要先設計頁面出來,特別要注意的部分是,因為每一次的驗證,都是POST傳到後端進行檢查,我們需要把連結的Token變成hidden欄位,以免使用者的網址列的重設密碼Token消失。
@(token:String)
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>重設密碼</title>
@views.html.web.headerLibs()
@views.html.web.loginSignup.loginSignupLibs()
</head>
<body>
<div id="page-wrapper">
<div id="select_nav_signup">@views.html.web.headerNav()</div>
</div>
<div class="form">
<ul class="tab-group">
<li class="tab active"><a>重設密碼</a></li>
</ul>
<div class="tab-content">
<div id="Form" >
<form action="@controllers.routes.WebController.resetPassword.url" method="post" id="setPasswordForm">
<div class="field-wrap">
<label class="lable-field-wrap">新密碼</label>
<input type="password" required autocomplete="off" name="password"/>
</div>
<div class="field-wrap">
<label class="lable-field-wrap">確認密碼</label>
<input type="password" required autocomplete="off" name="retypePassword"/>
@if(flash.containsKey("error")) {
<span style="color:red;">@flash.get("error")</span>
}
</div>
<input type="hidden" value="@token" name="token"/>
<a>
<input type="submit" value="SUBMIT" class="button button-block">
</a>
</form>
</div>
</div><!-- tab-content -->
</div> <!-- /form -->
<div id="titleBar"></div>
<script src='@routes.Assets.versioned("javascripts/loginSignup.js")'></script>
<script>
$( document ).ready(function() {
document.getElementById("setPasswordForm").reset();
});
</script>
</body>
</html>
app.controllers.WebController.java
新增重設密碼信件,點選連結後回來的檢查,來確保Token是可以正常且可以使用的。
/**
* <pre>
* 重設密碼信件寄送回來後
* Step 1 : 檢查重設密碼信件連結是否有資料
* Step 2 : 檢查Token,是否可以查詢到會員資料
* Step 3 : 檢查Token,是否使用過了
* Step 4 : 檢查Token,是否逾期
* OK : 檢查通過,可以進行重設密碼動作,並把Token儲存在表單裡
* </pre>
*/
public Result resetPassword(){
// 清除暫存錯誤訊息
flash().clear();
// Step 1
String token = "";
try{
token = request().getQueryString("token");
} catch (Exception e){
e.printStackTrace();
flash().put("error", "重設密碼連結有誤,請確認是否有點選正確,謝謝。");
return ok(resetPassword.render(""));
}
// Step 2
MemberToken memberToken = null ;
try{
token = request().getQueryString("token");
memberToken = webService.getMemberTokenData(token , MemberTokenType.ForgotPassword.toString());
} catch(Exception e){
e.printStackTrace();
flash().put("error", "系統忙碌中,請稍候再嘗試,謝謝。");
return ok(resetPassword.render(""));
} finally {
if(memberToken == null){
flash().put("error", "重設密碼連結有誤,請確認是否有點選正確,謝謝。");
play.Logger.warn("memberToken = " + Json.toJson(memberToken));
return ok(resetPassword.render(""));
}
}
// Step 3
if(memberToken.getIsUse()){
flash().put("error", "該忘記密碼連結已失效,若要重設密碼,請使用忘記密碼功能,謝謝。");
play.Logger.warn("memberToken = " + Json.toJson(memberToken));
return ok(resetPassword.render(""));
}
// Step 4
long dbTime = Long.parseLong(memberToken.getDbTime()); // 資料庫時間
long expiryDate = Long.parseLong(memberToken.getExpiryDate()); // 逾期時間
if(dbTime > expiryDate){
flash().put("authError", "重設密碼連結已經超過24小時,請重新使用忘記密碼功能謝謝。");
play.Logger.warn("dbTime = " + dbTime);
play.Logger.warn("expiryDate = " + expiryDate);
return ok(resetPassword.render(""));
}
// Ok
return ok(resetPassword.render(memberToken.getTokenString()));
}
[Stage 2 Test Case]
CASE 1 : 使用者點選的連結沒有資料,或者是該連結被修改過,無法使用。
CASE 2 : 使用者的重設密碼信連結,已經使用過,所以我們需要提示使用者已經使用過了。
CASE 3 : 使用者的重設密碼信件連結,已經逾期了。
OK : 使用者可以繼續在重設密碼頁面進行重設密碼。
Stage 3 : 使用者進行密碼重設
最後一個階段就是讓使用者進行密碼重設。在這之前,我們需要先把兩個小功能先完成,一個是更新會員密碼與寄送修改密碼成功信。以及最後的修改密碼成功頁面。
app.services.WebService.java
更新會員密碼,需要傳入memberNo與password來修改密碼
/** 更新會員密碼 */
public int updateMemberPassword(@Param("memberNo")String memberNo, @Param("password")String password);
WebService.xml
<!-- 更新會員密碼 -->
<update id="updateMemberPassword">
UPDATE member_main SET password = #{password} , modifyDate = DATE_FORMAT(NOW(), '%Y%m%d%H%i%s') WHERE memberNo = #{memberNo}
</update>
app.utils.mail.Utils_Email.java
新增修改重設密碼成功信。
public Email genResetPasswordOk(Member member){
Format formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String time = formatter.format(new Date());
Email email = new Email();
email.setFrom("[email protected]");
email.setTo(member.getEmail());
email.setSubject("[STAR] - 密碼重設成功");
email.setText("");
email.setContent("<h2>您好 "+ member.getUsername()+"您的密碼已在"+ time +" , 重設成功。</h2> ");
play.Logger.info("genResetPasswordOk email = " + Json.toJson(email));
return email;
}
app.views.web.loginSignup.resetPasswordOk.scala.html
會員成功重設密碼之後,需要導入的頁面。
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>重設密碼成功</title>
@views.html.web.headerLibs()
@views.html.web.loginSignup.loginSignupLibs()
</head>
<body>
<div id="page-wrapper">
<div id="select_nav_signup">@views.html.web.headerNav()</div>
</div>
<div class="form">
<ul class="tab-group">
<li class="tab active"><a href="#">重設密碼成功</a></li>
</ul>
<div class="tab-content">
<div id="signup">
<span>您已成功修改密碼,您可以回到<a href="@controllers.routes.WebController.login.url">登入頁</a>,進行登入,謝謝。</span>
</div>
</div><!-- tab-content -->
</div> <!-- /form -->
<div id="titleBar"></div>
</body>
</html>
app.pojo.web.signup.request.ResetPasswordRequest.java
表單送出之後,為了方便能取得表單的屬性,要新增表單的類別,以便我們後續去取得表單填寫的資料。
package pojo.web.signup.request;
/**
* 忘記密碼表單請求
*/
public class ResetPasswordRequest {
private String token;
private String password;
private String retypePassword;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRetypePassword() {
return retypePassword;
}
public void setRetypePassword(String retypePassword) {
this.retypePassword = retypePassword;
}
}
app.controllers.WebController.java
使用者開始進行重設密碼,前面的檢查邏輯基本上跟前面的Stage 2類似,唯一不同的是需要多驗證兩次密碼的輸入是否正確,而密碼的驗證,在之前註冊章節有講過,可以回去參考Utils_Signup checkPassword方法,再複習一次即可。
/**
* <pre>
* Step 1 : 檢查表單Token是否還存在
* Step 2 : 檢查Token,是否可以查詢到會員資料
* Step 3 : 檢查Token,是否使用過了
* Step 4 : 檢查Token,是否逾期了
* Step 5 : 檢查兩次輸入密碼,是否正確
*
* OK 1 : 確認完畢,進行修改密碼
* OK 2 : 把該會員所有忘記密碼Token,且尚未使用中的Token,全部更新成使用過
* OK 3 : 修改密碼動作,寄信給使用者
* OK 4 : 會員更新動作,都需要記錄下來,寫入member_main_log
* OK 5 : 以上動作完成,顯示修改成功訊息
* </pre>
*/
public Result doResetPassword(){
// 清除暫存錯誤訊息
flash().clear();
// Step 1
ResetPasswordRequest request = null;
try{
request = formFactory.form(ResetPasswordRequest.class).bindFromRequest().get();
} catch (Exception e){
e.printStackTrace();
flash().put("error", "資料錯誤,請重新點選忘記密碼信件連結,謝謝。0x31");
return ok(resetPassword.render(""));
}
// Step 2
MemberToken memberToken = null ;
try{
memberToken = webService.getMemberTokenData(request.getToken() , MemberTokenType.ForgotPassword.toString());
} catch(Exception e){
e.printStackTrace();
flash().put("error", "系統忙碌中,請稍候再嘗試,謝謝。");
return ok(resetPassword.render(request.getToken()));
} finally {
if(memberToken == null){
flash().put("error", "資料錯誤,請重新點選忘記密碼信件連結,謝謝。0x32");
play.Logger.warn("memberToken = " + Json.toJson(memberToken));
return ok(resetPassword.render(""));
}
}
// Step 3
if(memberToken.getIsUse()){
flash().put("error", "該忘記密碼連結已失效,若要重設密碼,請使用忘記密碼功能,謝謝。");
play.Logger.warn("memberToken = " + Json.toJson(memberToken));
return ok(resetPassword.render(""));
}
// Step 4
long dbTime = Long.parseLong(memberToken.getDbTime()); // 資料庫時間
long expiryDate = Long.parseLong(memberToken.getExpiryDate()); // 逾期時間
if(dbTime > expiryDate){
flash().put("authError", "重設密碼連結已經超過24小時,請重新使用忘記密碼功能謝謝。");
play.Logger.warn("dbTime = " + dbTime);
play.Logger.warn("expiryDate = " + expiryDate);
return ok(resetPassword.render(""));
}
// Step 5
try{
VerificFormMessage message = new Utils_Signup().checkPassword(request.getPassword(), request.getRetypePassword());
if(!"200".equals(message.getStatus())){
flash().put("error", message.getStatusDesc());
return ok(resetPassword.render(request.getToken()));
}
}catch(Exception e){
e.printStackTrace();
flash().put("error", "系統忙碌中,請重新再次嘗試,謝謝。");
return ok(resetPassword.render(request.getToken()));
}
// Ok
try{
String password = request.getPassword();
String memberNo = memberToken.getMemberNo();
Member member = this.webService.findMemberByMemberNo(memberNo);
int isGenMemberChangeLogOk = webService.genMemberChangeLog(member);
int updateMemberPassword = this.webService.updateMemberPassword(memberNo , password);
int updateMemberToken = this.webService.updateMemberToken(memberNo, MemberTokenType.ForgotPassword.toString());
Utils_Email utils_Email = new Utils_Email();
Email email = new Utils_Email().genResetPasswordOk(member);
boolean isGenResetPasswordOk = utils_Email.sendMail(email);
play.Logger.info("updateMemberPassword = " + updateMemberPassword +
" , updateMemberToken = " + updateMemberToken +
" , isGenResetPasswordOk = " + isGenResetPasswordOk +
" , isGenMemberChangeLogOk = " + isGenMemberChangeLogOk);
} catch (Exception e){
e.printStackTrace();
flash().put("error", "系統忙碌中,請重新再次嘗試,謝謝。");
return ok(resetPassword.render(request.getToken()));
}
return ok(resetPasswordOk.render());
}
[Stage3 Test Case]
CASE 1 : 這個情形是,有可能使用者修改表單資料,而導致轉換錯誤,我們需要去阻止這種行為,減少不正確的資料。
CASE 2 : 若是使用者修改了表單Token欄位資訊,我們還是需要去驗證是否正確。
CASE 3 : 這種情形較少發生,但是我們還是需要避免,如果使用者開了兩個視窗,其中一個重設密碼已經成功了。而另外一個畫面的重設密碼,如果進行重設密碼的話,我們必須告訴使用者,該連結已經失效,不可以使用即可。
CASE 4 : 使用者的重設密碼信件連結,已經逾期了。
CASE 5 : 以上驗證都通過,最後一個階段的檢查是,需要檢查使用者重設的兩次輸入的密碼是否符合一致,以及是否符合我們的密碼檢查規則。
Ok : 當全部驗證都通過之後,會進行以下流程
1.我們會去更新member_token把所有的該會員尚未使用的重設密碼的token都變成使用過。
2.我們會去更新member_main把該會員密碼進行更新。
3.會員更新時,都必須去記錄下來,寫入member_main_log裡。
4.最後寄信給使用者說明您的密碼已經重設成功了,主要原因有可能非本人進行重設密碼,這部分需要寄信告知。
重設密碼成功頁面。
密碼重設成功信件
更新該會員的member_token,類似是忘記密碼且尚未使用過的token
更新member_main的password欄位
新增member_main_log記錄檔
[Final]
這個小節,使用者忘記密碼的功能就算完成了,下一個小節會是登入之後,進行密碼修改的部分,那我們就往下個章節前進吧。