在Java中实现忘记密码功能

21

我目前正在Java项目中实现忘记密码功能。我的方法是:

  1. 用户点击忘记密码链接。
  2. 在忘记密码页面,系统提示用户输入他/她在系统中注册的电子邮件地址。
  3. 系统发送包含重置密码链接的电子邮件到上述步骤中提供的电子邮件地址。
  4. 用户点击链接并被重定向到一个页面(重置密码),在该页面中,用户可以输入新密码。
  5. 在重置密码页面中,“电子邮件地址”字段自动填写且不能更改。
  6. 然后用户输入新密码,与电子邮件地址相关联的数据库字段将被更新。

虽然我已经限制了重置密码页面中的“电子邮件地址”字段不可编辑(只读字段),但任何人都可以更改浏览器地址栏中的URL以更改电子邮件地址字段。

如何防止每个用户更改重置密码页面中的电子邮件地址?


6个回答

42

在发送邮件之前,您必须使用令牌将其保存在数据库中:

  1. 当用户点击“发送包含重置说明的电子邮件”时,您需要在数据库中创建一条记录,并包含以下字段: email, token, expirationdate
  2. 用户会收到包含 yourwebsite.com/token 的电子邮件并单击它
  3. 借助 URL 中的 token ,服务器可以 识别用户,检查请求是否过期(通过 expirationdate),将正确的电子邮件地址放入框中,并要求用户更新密码。用户输入新密码,并且您必须向服务器提供令牌(在表单中 hidden field)和密码。服务器不关心电子邮件文本框,因为 凭借令牌,用户被严格识别
  4. 然后,服务器再次检查令牌是否仍然有效(使用expirationdate),检查密码是否匹配,如果一切正常,则保存新密码!服务器可以再次发送消息,以通知用户由于请求已更改密码。

这真的很安全。请使用短时间设置 expirationdate(例如,5分钟对我来说是正确的),并使用强令牌(如 GUID,请参见注释)


1
但是用户仍然可以在地址栏中更改令牌。 - vigamage
3
但是如果没有了解其他令牌,他们将得不到任何东西。这就是为令牌需要像UUID这样强大的令牌,以便他们不能轻易获取其他用户的令牌。 - Zhedar
8
令牌比用户密码更强大,这是因为它更加复杂,并且在按钮点击后例如5分钟内会过期。当我说令牌时,我不是指0到10之间的随机数,我指的是这种类型的令牌:nWc^5:lh6[xM(@2t795j?bDZ40vEjT。该令牌可能在5分钟内有效,因此黑客破解的唯一途径是攻击你的电子邮件/网络,而不是应用程序。 - clement

4

如果你必须自己实现忘记密码功能,我同意@clement提供的答案。这听起来是一种合理且安全的实现方式。

然而,作为替代方案,如果你不必自己实现它,我建议使用一个可以为你完成此操作的服务,比如Stormpath

如果你决定使用Stormpath,在Java中触发该功能的代码将如下所示(使用Stormpath的Java SDK):

Account account = application.sendPasswordResetEmail("john.smith@example.com");

您的用户将收到一封带有以下链接的电子邮件:

http://yoursite.com/path/to/reset/page?sptoken=$TOKEN

然后,当用户单击链接时,您将像这样验证并重置密码:

Account account = application.resetPassword("$TOKEN", "newPassword");

如何操作的详细信息可在Stormpath的密码重置文档中找到。

采用这种方法,如果您有选择,则无需自己实现和维护功能。

注意:Stormpath已加入Okta


非常感谢。我期待将来使用它。 - vigamage
当然,没问题。很乐意帮忙! - ecrisostomo

2
如果您正在寻找完整的代码来实现忘记密码功能,那么我在这里分享我的代码。将链接放在需要的位置即可。
<button> <a href="forgotpassword.jsp" style="text-decoration:none;">Forgot 
Password</a></button>

以下是我的forgotpassword.jsp页面。

 <form id="register-form" role="form" class="form" method="post" 
 action="mymail_fp.jsp">
    <h3>Enter Your Email Below</h3>
   <input id="email" name="email" placeholder="Email address" class="form- 
   control"  type="email" required autofocus>
  <input name="recover-submit" class="btn btn-lg btn-primary btn-block" 
   value="Get Password" type="submit">
</form>

邮件提交后,会被重定向到mymail_fp.jsp页面,我会在该页面将邮件发送给用户。 下面是mymail.jsp页面。

<% 
mdjavahash md = new mdjavahash();
String smail =request.getParameter("email");
int profile_id = 0;
if(smail!=null)
{
 try{
// Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");

// Open a connection
Connection conn = 
DriverManager.getConnection("jdbc:mysql://localhost:3306/infoshare", "root", 
"");

Statement stmt = conn.createStatement();

 String sql1;
 sql1="SELECT  email FROM profile WHERE email = '"+smail+"'";

  ResultSet rs1=stmt.executeQuery(sql1);

if(rs1.first())
{
    String sql;
    sql = "SELECT Profile_id FROM profile where email='"+smail+"'";
     ResultSet rs2 = stmt.executeQuery(sql);

    // Extract data from result set
    while(rs2.next()){
       //Retrieve by column name
     profile_id  = rs2.getInt("Profile_id");
    }

    java.sql.Timestamp  intime = new java.sql.Timestamp(new 
    java.util.Date().getTime());
    Calendar cal = Calendar.getInstance();
    cal.setTimeInMillis(intime.getTime());
    cal.add(Calendar.MINUTE, 20);
    java.sql.Timestamp  exptime = new Timestamp(cal.getTime().getTime());

    int rand_num = (int) (Math.random() * 1000000);
    String rand = Integer.toString(rand_num);
    String finale =(rand+""+intime); // 
    String hash = md.getHashPass(finale); //hash code

    String save_hash = "insert into  reset_password (Profile_id, hash_code, 
   exptime, datetime) values("+profile_id+", '"+hash+"', '"+exptime+"', 
   '"+intime+"')";
    int saved = stmt.executeUpdate(save_hash);
    if(saved>0)
    {
  String link = "http://localhost:8080/Infoshare/reset_password.jsp";     
  //bhagawat till here, you have fetch email and verified with the email 
 from 
  datbase and retrived password from the db.
    //-----------------------------------------------
String host="", user="", pass=""; 
host = "smtp.gmail.com"; user = "example@gmail.com"; 
//"email@removed" // email id to send the emails 
pass = "xxxx"; //Your gmail password 
String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory"; 
String to = smail;  
String from = "example@gmail.com";  
String subject = "Password Reset"; 
 String messageText = " Click <a href="+link+"?key="+hash+">Here</a> To 
  Reset 
  your Password. You must reset your password within 20 
  minutes.";//messageString; 
   String fileAttachment = ""; 
   boolean WasEmailSent ; 
  boolean sessionDebug = true; 
  Properties props = System.getProperties(); 
  props.put("mail.host", host); 
  props.put("mail.transport.protocol.", "smtp"); 
  props.put("mail.smtp.auth", "true"); 
  props.put("mail.smtp.", "true"); 
  props.put("mail.smtp.port", "465"); 
  props.put("mail.smtp.socketFactory.fallback", "false"); 
  props.put("mail.smtp.socketFactory.class", SSL_FACTORY); 
  Session mailSession = Session.getDefaultInstance(props, null); 
  mailSession.setDebug(sessionDebug); 
  Message msg = new MimeMessage(mailSession); 
  msg.setFrom(new InternetAddress(from)); 
  InternetAddress[] address = {new InternetAddress(to)}; 
  msg.setRecipients(Message.RecipientType.TO, address); 
  msg.setSubject(subject); 
  msg.setContent(messageText, "text/html");  
  Transport transport = mailSession.getTransport("smtp"); 
  transport.connect(host, user, pass);
    %>
 <div class="alert success" style="padding: 30px; background-color: grey; 
  color: white; opacity: 1; transition: opacity 0.6s; width:50%; margin: 10% 
 5% 
15% 20%;">
 <a href="forgotpassword.jsp"> <span class="closebtn" style="color: white; 
font-weight: bold; float: right; font-size: 40px; line-height: 35px; cursor: 
pointer; transition: 0.3s;">&times;</span> </a> 
 <h1 style="font-size:30px;">&nbsp;&nbsp; <strong>Check Your Email. Link To 
Reset Your Password Is Sent To : <%out.println(" "+smail); %></strong>  
</h1>
 <center><a href="forgotpassword.jsp"><h2><input type="button" value="OK"> 
</h2></a></center>
</div>
<%
try { 
transport.sendMessage(msg, msg.getAllRecipients()); 
WasEmailSent = true; // assume it was sent 
} 
catch (Exception err) { 
WasEmailSent = false; // assume it's a fail 
} 
 transport.close();
    //-----------------------------------------------
 }  
}   

 else{
    %>
    <div class="alert success" style="padding: 30px; background-color: grey; 
 color: white; opacity: 1; transition: opacity 0.6s; width:50%; margin: 10% 
 5% 15% 20%;">
     <a href="forgotpassword.jsp"> <span class="closebtn" style="color: 
 white; font-weight: bold; float: right; font-size: 40px; line-height: 35px; 
 cursor: pointer; transition: 0.3s;">&times;</span> </a> 
     <h1 style="font-size:30px;">&nbsp;&nbsp; <strong>There Is No Email As 
 Such <%out.println(" "+smail); %></strong>Try Again  </h1>
     <center><a href="forgotpassword.jsp"><h2><input type="button" 
 value="OK"></h2></a></center>
    </div>
    <%      
 }  

stmt.close();
rs1.close();
conn.close();
}catch(SQLException se){
//Handle errors for JDBC
se.printStackTrace();
}catch(Exception e){
//Handle errors for Class.forName
e.printStackTrace();
}
}
 else{
    %>
 <div class="alert success" style="padding: 30px; background-color: grey; 
 color: white; opacity: 1; transition: opacity 0.6s; width:50%; margin: 10% 
 5% 15% 20%;">
  <a href="forgotpassword.jsp"> <span class="closebtn" style="color: white; 
  font-weight: bold; float: right; font-size: 40px; line-height: 35px; 
 cursor: 
 pointer; transition: 0.3s;">&times;</span> </a> 
 <h1 style="font-size:30px;">&nbsp;&nbsp; <strong>Please Enter The Valid 
 Email Address</strong>  </h1>
 <center><a href="forgotpassword.jsp"><h2><input type="button" value="OK"> 
 </h2></a></center>
 </div>
  <%    
  }
  %> 

现在我所做的是,在向用户发送电子邮件之前,我会保存发送时间,过期时间,并从0到1000000生成随机数字并将其与发送时间连接起来加密。然后将它作为查询字符串发送到电子邮件中的链接中。因此,电子邮件将被发送,并且密码重置链接将与哈希密钥一起发送。现在当用户单击链接时,他们将被发送到reset_password.jsp页面,以下是reset_password.jsp页面。

<%
String hash = (request.getParameter("key"));

java.sql.Timestamp  curtime = new java.sql.Timestamp(new 
java.util.Date().getTime());

int profile_id = 0;
java.sql.Timestamp exptime;

try{
// Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");

// Open a connection
Connection conn = 
DriverManager.getConnection("jdbc:mysql://localhost:3306/infoshare", "root", 
"");
Statement stmt = conn.createStatement();

 String sql = "select profile_id, exptime from reset_password where 
 hash_code ='"+hash+"'";
 ResultSet rs = stmt.executeQuery(sql);
 if(rs.first()){
 profile_id = rs.getInt("Profile_id");  
 exptime = rs.getTimestamp("exptime");

  //out.println(exptime+"/"+curtime);
  if((curtime).before(exptime)){        
      %>
      <div class="container">
       <form class="form-signin" action="update_reset.jsp" method="Post"> 
      <br/><br/>
         <h4 class="form-signin-heading">Reset Your Password Here</h4>
         <br> 
          <text style="font-size:13px;"><span class="req" 
        style="color:red">* </span>Enter New Password</text>
         <input type="password" id="inputPassword" name="newpassword" 
       class="form-control" placeholder="New Password" required autofocus>
         <br>
          <text style="font-size:13px;"><span class="req" 
         style="color:red">* </span>Enter New Password Again</text>
         <input type="password" id="inputPassword" name="confirmpassword" 
         class="form-control" placeholder="New Password Again" required>

          <input type="hidden" name="profile_id" value=<%=profile_id %>>
        <br>
         <button class="btn btn-lg btn-primary btn-block" 
    type="submit">Reset Password</button>
       </form>
     </div> <!-- /container -->
    <% } 
    else{
        %>
        <div class="alert success" style="padding: 30px; background-color: 
   grey; color: white; opacity: 1; transition: opacity 0.6s; width:50%; 
  margin: 10% 5% 15% 20%;">
             <a href="forgotpassword.jsp"> <span class="closebtn" 
   style="color: white; font-weight: bold; float: right; font-size: 40px; 
   line-height: 35px; cursor: pointer; transition: 0.3s;">&times;</span> 
   </a> 
             <h1 style="font-size:30px;">&nbsp;&nbsp; The Time To Reset 
  Password Has Expired.<br> &nbsp;&nbsp; Try Again </h1>
             <center><a href="forgotpassword.jsp"><h2><input type="button" 
     value="OK"></h2></a></center>
        </div>
       <%       
       }    
     }
   else{
    %>
    <div class="alert success" style="padding: 30px; background-color: grey; 
   color: white; opacity: 1; transition: opacity 0.6s; width:50%; margin: 
    10% 5% 15% 20%;">
         <a href="forgotpassword.jsp"> <span class="closebtn" style="color: 
      white; font-weight: bold; float: right; font-size: 40px; line-height: 
       35px; cursor: pointer; transition: 0.3s;">&times;</span> </a> 
         <h1 style="font-size:30px;">&nbsp;&nbsp; The Hash Key DO Not Match. 
            <br/> &nbsp;&nbsp;&nbsp;Try Again!! </h1>
         <center><a href="forgotpassword.jsp"><h2><input type="button" 
         value="OK"></h2></a></center>
        </div>
    <%
    }
   // Clean-up environment
   rs.close();
   stmt.close();
   conn.close();
  }catch(SQLException se){
  //Handle errors for JDBC
  se.printStackTrace();
 }catch(Exception e){
  e.printStackTrace();
  }
%> 

在这个页面中,我获取哈希键并与数据库哈希键进行比较,如果匹配,则获取过期时间并与当前时间进行比较。如果重置密码的时间尚未过期,则显示重置密码表单,否则抛出错误消息。如果时间未过期,则显示表单,当表单提交时,将重定向到update_reset.jsp,以下是我的update_reset.jsp页面。

 <%  
 mdjavahash md = new mdjavahash();
 String profile_id= request.getParameter("profile_id");
 String np= request.getParameter("newpassword");
 String cp = request.getParameter("confirmpassword");
 //out.println(np +"/"+ cp);

 if( np.equals(" ") || cp.equals(" ")){%>
 <div class="alert success" style="padding: 30px; background-color: grey; 
 color: white; opacity: 1; transition: opacity 0.6s; width:50%; margin: 10% 
 5% 15% 20%;">
     <a href="reset_password?profile_id=<%=profile_id%>"> <span 
  class="closebtn" style="color: white; font-weight: bold; float: right; 
    font-size: 40px; line-height: 35px; cursor: pointer; transition: 
   0.3s;">&times;</span> </a> 
     <h1 style="font-size:30px;">&nbsp;&nbsp; Please Fill Both The Fields 
    </h1>
     <center><a href="reset_password?profile_id=<%=profile_id%>""><h2><input 
    type="button" value="OK"></h2></a></center>
   </div>   
   <% }
   else if(!np.equals(cp)){
    %>
    <div class="alert success" style="padding: 30px; background-color: grey; 
  color: white; opacity: 1; transition: opacity 0.6s; width:50%; margin: 10% 
  5% 15% 20%;">
         <a href="reset_password?profile_id=<%=profile_id%>"> <span 
     class="closebtn" style="color: white; font-weight: bold; float: right; 
        font-size: 40px; line-height: 35px; cursor: pointer; transition: 
             0.3s;">&times;</span> </a> 
         <h1 style="font-size:30px;">&nbsp;&nbsp; The Two Passwords Do Not 
        Match. Try Again </h1>
         <center><a href="reset_password?profile_id=<%=profile_id%>"><h2> 
           <input type="button" value="OK"></h2></a></center>
        </div>
      <%        
     }
    else{   
      try{
        // Register JDBC driver
        Class.forName("com.mysql.jdbc.Driver");

        // Open a connection
        Connection conn = 
        DriverManager.getConnection("jdbc:mysql://localhost:3306/infoshare", 
      "root", "");
        // Execute SQL query
        Statement stmt = conn.createStatement();
        stmt.executeUpdate("update profile set 
       password='"+md.getHashPass(np)+"' where Profile_id="+profile_id+"");
        //response.sendRedirect("mainpage.jsp");
        %>
        <div class="alert success" style="padding: 30px; background-color: 
       grey; color: white; opacity: 1; transition: opacity 0.6s; width:65%; 
      margin: 10% 5% 15% 20%;">
         <a href="login.jsp"> <span class="closebtn" style="color: white; 
        font-weight: bold; float: right; font-size: 40px; line-height: 35px; 
         cursor: pointer; transition: 0.3s;">&times;</span> </a> 
         <h1 style="font-size:30px;">&nbsp;&nbsp; The Password Is 
            Successfully Reset.<br>&nbsp;&nbsp; Try Login With New 
             Password</h1>
         <br><br><center><a href="login.jsp"><p style="font-size:20px"> 
            <input type="button" style="width:40px; height:35px;" 
        value="OK"></p></a> 
        </center>
           </div>                   
          <%
           stmt.close();
           conn.close();
        }catch(SQLException se){
          //Handle errors for JDBC
           se.printStackTrace();
        }catch(Exception e){
        //Handle errors for Class.forName
         e.printStackTrace();
       }    
  } 
%>

在这个页面中,我首先验证字段,然后使用新密码更新数据库。虽然它很长,但它有效。我在这里使用了MD5加密技术,如果您想知道如何操作,请参考链接如何在JSP中使用JavaScript进行安全登录密码的MD5哈希?


2

您不能限制用户更改电子邮件地址。
即使您已经隐藏或将文本框设置为只读,也可以通过在浏览器中编辑源代码轻松更改电子邮件地址。

您可以提供带有重置链接的唯一随机字符串或令牌,并在单击重置密码链接或用户提交重置密码请求后通过检查请求中的电子邮件地址和令牌字符串与数据库中的电子邮件地址和令牌字符串的组合来检查电子邮件地址和令牌组合。

如果电子邮件地址存在于您的数据库中,那么说明电子邮件地址是有效的;否则,请给出消息说明电子邮件地址不存在于您的用户记录中。

注意:
如果您正在使用任何框架或简单的servlet,则最好提供链接,以便在显示重置密码表单之前验证电子邮件和令牌字符串。如果令牌字符串或电子邮件地址无效,则可以防止用户提交重置密码请求并在提交请求后进行验证。这比提交重置密码请求后进行验证更安全。


2
这个问题在本回答发布前已经发布了3年...不过我认为它可能对其他人有帮助。简而言之:我完全同意您的流程。看起来非常安全,您唯一的开放端口也很有道理——您如何确保没有人更改用户名,并因此可以为他设置新密码。
我不太喜欢将临时内容存储在数据库中(正如被接受的答案建议的那样)。
我想到的想法是在发送给用户的链接中签署数据。然后,当用户单击链接并服务器接收调用时,服务器还会获取加密部分并验证数据未被修改。
顺便说一下(这里是“推广”):我对这些用例(还包括“创建帐户”、“更改密码”等)实现了一个JAVA项目。它在GitHub上免费、开源。它完美地解答了您的问题... 在Java上实现,在Spring Security之上。
每件事都有解释(如果缺少某些东西,请告诉我...)
看看:https://github.com/OhadR/oAuth2-sample/tree/master/authentication-flows此处查看演示。
还有一个使用auth-flows的客户机Web应用程序,其中包含所有说明的自述文件:https://github.com/OhadR/Authentication-Flows

非常感谢您。您提出的替代方案看起来非常令人印象深刻。 - vigamage

1

有两种常见的解决方案:

1. Creating a new password on the server and inform user from it.
2. Sending a unique URL to reset password.

第一种解决方案存在很多问题,不适合使用。以下是一些原因:

1. The new password which is created by server should be sent through an insecure channel (such as email, sms, ...) and resides in your inbox. 

2. If somebody know the email address or phone number of a user who has an account at a website then then it is possible to reset user password.

所以,第二种解决方案更好使用。但是,您应该考虑以下问题:
- The reset url should be random, not something guessable and unique to this specific instance of the reset process.

- It should not consist of any external information to the user For example, a reset URL should not simply be a path such as “.../?username=Michael”. 

- We need to ensure that the URL is loaded over HTTPS. No, posting to HTTPS is not enough, that URL with the token must implement transport layer 
  security so that the new password form cannot be MITM’d and the password the user creates is sent back over a secure connection.

- The other thing we want to do with a reset URL is setting token's expiration time so that the reset process must be completed within a certain duration.

- The reset process must run once completely. So, Reset URL can not be appilicable if the reset process is done completely once.

常见的解决方案是生成一个URL来创建一个唯一的令牌,可以将其作为URL参数发送,其中包含一个URL,例如“Reset/?id=2ae755640s15cd3si8c8i6s2cib9e14a1ae552b”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接