Ch12-1 : Forgot Passowrbd

[Forgot Passowrd]
忘記密碼部分,可以切分成三個階段。

Stage 1 : 在忘記密碼頁面,輸入註冊帳號,寄送忘記密碼重設信件。
Stage 2 : 使用者從信件點選重設密碼信,檢查信件連結是否正確。
Stage 3 : 使用者進行密碼重設。

以上就是我們忘記密碼時,會進行的三階段動作,下面我會分成三個階段,進行講解。


Stage 1 : 在忘記密碼頁面,輸入註冊帳號,寄送忘記密碼重設信件

就跟之前一樣,我們需要先準備pageroutescontroller以及相關的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.htmlsignup.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_mainpassword欄位

新增member_main_log記錄檔


[Final]
這個小節,使用者忘記密碼的功能就算完成了,下一個小節會是登入之後,進行密碼修改的部分,那我們就往下個章節前進吧。

results matching ""

    No results matching ""