【问题标题】:Implementing forgot password functionality in Java在 Java 中实现忘记密码功能
【发布时间】:2015-02-03 11:21:43
【问题描述】:

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

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

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

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

【问题讨论】:

标签: java password-recovery forgot-password change-password


【解决方案1】:

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

  1. 当用户单击“向我发送带有重置说明的电子邮件”时,您会在 DB 中创建一条包含以下字段的记录:emailtokenexpirationdate
  2. 用户收到带有 yourwwebsite.com/token 的电子邮件并点击它
  3. 通过Url中的token,服务器可以identify the user,检查请求是否由于过期日期未过期,将正确的电子邮件放入框中,并要求更新密码。用户输入新密码,您必须将令牌(表单中的hidden field)+ 密码提供给服务器。服务器不关心电子邮件的文本框,因为with the token, user is identified strongly
  4. 然后服务器使用expirationdate(再次)检查令牌是否仍然有效,检查password match 是否有效,如果一切正常,保存新密码!服务器可以再次发送消息以通知用户由于请求而更改了密码。

这真的很安全。请使用较短的时间让expirationdate 提高安全性(例如 5 分钟对我来说是正确的)并使用强令牌(作为 GUID,请参阅 cmets)

【讨论】:

  • 但用户仍然可以在地址栏中更改令牌。
  • @user3892439 但不知道另一个不会得到任何东西的令牌。这就是为什么你需要像UUIDs 这样的强令牌,所以他们不能只为其他用户获取它。
  • 令牌确实比用户的密码更强大,因为他的复杂性以及令牌在按钮单击后例如 5 分钟过期的事实。当我说令牌时,我没有说 0 到 10 之间的随机,我的意思是那种令牌: nWc^5:lh6[xM(@2t795j?bDZ40vEjT 。这个令牌可能在 5 分钟内有效,所以黑客的唯一方法是破解这是为了破解您的邮箱/网络......,而不是应用程序。
【解决方案2】:

如果您必须自己实现忘记密码功能,我同意@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 的 password reset documentation

如果您可以选择不这样做,您不必自己实现和维护功能。

注意:Stormpath 已加入Okta

【讨论】:

  • 非常感谢。我期待将来使用它
  • 当然,没问题。乐于助人!
【解决方案3】:

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

您可以向uniq random string or token 提供重置链接和 单击重置密码链接或用户提交重置密码请求后,通过检查电子邮件地址和请求中的令牌字符串以及数据库中的电子邮件地址和令牌字符串来检查电子邮件地址和令牌组合。

如果电子邮件地址存在于您的数据库中,则表示电子邮件地址有效,如果不是,则表明电子邮件地址在您的用户记录中不存在。

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

【讨论】:

    【解决方案4】:

    这个问题已经在这个答案之前 3 年发布了......但我认为它可能对其他人有帮助。

    简而言之:我完全同意您的流程。看起来很安全,你唯一的开放端也很有意义——你如何确保没有人更改用户名,并且可以为他设置新密码。

    我不太喜欢临时存储东西的想法是数据库(正如公认的答案所暗示的那样)。

    我的想法是对发送给用户的链接中的数据进行签名。然后,当用户点击链接,服务器接收到调用时,服务器也会得到加密部分,并可以验证数据未被触及。

    顺便说一下(这里有一个“促销”):我已经为这些用例实现了一个 JAVA 项目(还有“创建帐户”、“更改密码”等)。它在 GitHub 上是免费的,开源的。它完美地回答了您的问题...在 Java 中实现,在 Spring Security 之上。

    对所有事情都有解释(如果缺少某些东西 - 让我知道...)

    看看:https://github.com/OhadR/oAuth2-sample/tree/master/authentication-flows

    查看Demo here

    还有一个使用 auth-flows 的客户端网络应用程序,其中包含所有解释的 README:https://github.com/OhadR/Authentication-Flows

    【讨论】:

    • 非常感谢。你建议的另一种方式看起来非常令人印象深刻
    【解决方案5】:

    如果您正在寻找实现忘记密码的完整代码,我在这里分享我的代码。 把链接放在你需要的地方。

    <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加密技术,如果你想怎么做,请点击链接How to Use MD5 Hash for securing Login passwords in JSP with Javascript?

    【讨论】:

      【解决方案6】:

      有两种常见的解决方案:

      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,例如 “重置/?id=2ae755640s15cd3si8c8i6s2cib9e14a1ae552b”。

      【讨论】:

        猜你喜欢
        • 2017-07-06
        • 2010-10-06
        • 2018-11-11
        • 2013-12-09
        • 2011-10-19
        • 2011-09-28
        • 1970-01-01
        • 2022-01-27
        • 2012-02-22
        相关资源
        最近更新 更多