"忘记密码"功能过程及其实现细节

更多2019年的技术文章,欢迎关注我的微信公众号:码不停蹄的小鼠松(微信号:busy_squirrel),也可扫下方二维码关注获取最新文章哦~

对于忘记密码功能,一般都是通过2种方式找回:一种是通过预留电话号码发送验证码找回,另一个是通过设定邮箱找回。对于具体的找回流程,参见:http://www.yixieshi.com/ucd/9207.html。

这里结合目前做的项目,详细的说明一下密码找回的过程。这里用的是邮箱找回。根据一般的忘记密码的流程来说明。我采用的流程是:

 

 

  • 流程

 

【登录】 --> 【点击忘记密码】 --> 【输入个人邮箱和验证码】 --> 【系统发送邮箱验证】 --> 【用户在限定时间内登录邮箱,点击链接,进入重置密码页面】 --> 【重置密码完毕,点击进入登录界面】。

 

 

  • 登录页面

先给出我做的登录界面,其中包含"忘记密码"功能

 

一般登录界面都有“忘记密码”选项,这里不多说。

 

  • "忘记密码"页面

点击"忘记密码"选项后,这时,页面会跳向一个新页面,即“忘记密码”页面。首先给出"忘记密码"按钮对应的跳转代码片段:

忘记密码?  


在点击后,跳到新页面:

 

这里的界面设计是模仿的网页163邮箱的忘记密码,给出链接:http://reg.163.com/getpasswd/RetakePassword.jsp?from=mail163。

该界面中,有几个关键点:一个是验证码的生成,一个是邮箱发送的功能,还一个就是点击确定后的反馈界面。主要包含这3个方面的内容,这3点以下将会详细介绍。

1、验证码生成功能

参考文章:http://bbs.csdn.net/topics/350211255。

关键是jsp和servlet的交互。上面的链接中,主要使用了作者贴出的serlvet代码片段,即随机生成四位0~9的数字,然后生成一些干扰线,最后将生成的图片发送到jsp页面。

下面给出部分后台servlet代码片段:

 public void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException
    {
        System.out.println("生成验证码");
        // 清空缓冲区
        response.reset();
 
        // 注意这里的MIME类型
        response.setContentType("image/png");
 
        // 设置页面不缓存
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
 
        // 创建一个图像,验证码显示的图片大小
        BufferedImage image = new BufferedImage(width, height,BufferedImage.TYPE_INT_RGB);
 
        // 获取图形上下文
        Graphics g = image.getGraphics();
         
        // 设置背景
        g.setColor(getRandColor(200,250));
        g.fillRect(0, 0, width, height);
 
        for (int i = 0; i < 4; i++)
        {
            drawCode(g, i);
        }
        //添加干扰线
        drawNoise(g, 12);
 
        // 绘制边框
        //g.setColor(Color.gray);
        //g.drawRect(0, 0, width - 1, height - 1);
 
        // 将验证码内容保存进session中,用于验证用户输入是否正确时使用
        HttpSession session = request.getSession(true);
        session.removeAttribute("rand");
        session.setAttribute("rand", codeNumbers);
        // 重设字符串
        codeNumbers = "";
        // 利用ImageIO类的write方法对图像进行编码
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(image, "PNG", sos);
        sos.close();
    }

从上面的代码可以看出,基本步骤是:生成4位0~9的随机数,然后设置数字的大小,颜色等;接着设置干扰线,根数,颜色等,然后创建出一个BufferedImage对象,就是验证码的背景图片大小、颜色等;最后将生成好的验证码传送到前台。

注意:这里要将验证码(即图片)传送到前台的jsp界面,需要三个方面的设置:

①设置传送到前台的内容类型,可以是text/html,也可以是image/png。由于这里传送的是验证码,所以设置为后一种。

 // 注意这里的MIME类型
        response.setContentType("image/png");

②在servlet中,还要考虑用什么方法对图像进行编码。

 // 利用ImageIO类的write方法对图像进行编码
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(image, "PNG", sos);
        sos.close();

 

③在jsp页面部分:

对应的js函数:

function refreshcode(){
	        document.getElementById("code").src="codeMakerServlet?a="+Math.random()+100;
	        return true;
	    }

 

其实就是每次点击,都运行一次对应的servlet,servlet在运行后,都会发送一个验证码图片到前台,也就是所谓的"刷新"。

 

2、填写内容检查

这里用到了一个js库:jquery.validate.min.js。这个只能判断输入的内容是否符合规则。

由于要输入用户的登录邮箱,规则正确只是第一步,还要查看输入的邮箱在数据库中是否存在,如果不存在,是不能允许发送密码重置邮件的。所以对邮箱正确性的检查,需要调用一次$.getJSON();进行验证;

对于验证码的验证,由于在验证码生成的servlet中,已经设置了

// 将验证码内容保存进session中,用于验证用户输入是否正确时使用
        HttpSession session = request.getSession(true);
        session.removeAttribute("rand");
        session.setAttribute("rand", codeNumbers);

所以这时候还要进行一次后台通讯,查看是否验证码输入正确。但是,有个问题我还没来得急了解:这个验证码的作用是什么?这个也是我在后期需要进行追加的内容。

 

3、邮件发送功能

参考文章:http://www.iteye.com/topic/352753。
需要用到的jar包:mail.jar。

在上面的参考文章中,作者详细的给出了3段主要的代码,这里也不多说了,说实话,我还真没怎么看其内部的实现原理,只是把别人写好的代码,直接调用接口使用。

注意:作者在最后给出了几个经验,其中一个就是不能用新申请的邮箱,否则无法发送成功,我试了下,确实是这样,但是这个是不是所有的邮箱都是这样,我也没有试过。

另外,需要特别提出的是,发送内容的方式分为两种,一个是按照文本发送,一个是按照html发送。

先给出示例代码:

public static void main(String[] args){  
        //这个类主要是设置邮件  
     MailSenderInfo mailInfo = new MailSenderInfo();   
     mailInfo.setMailServerHost("smtp.163.com");   
     mailInfo.setMailServerPort("25");   
     mailInfo.setValidate(true);   
     mailInfo.setUserName("[email protected]");   
     mailInfo.setPassword("********");//您的邮箱密码   
     mailInfo.setFromAddress("[email protected]");   
     mailInfo.setToAddress("[email protected]");   
     mailInfo.setSubject("设置邮箱标题,sfsfdfdff");   
     mailInfo.setContent("http://write.blog.csdn.net/postlist");   
        //这个类主要来发送邮件  
     SimpleMailSender sms = new SimpleMailSender();  
         sms.sendTextMail(mailInfo);//发送文体格式   
         SimpleMailSender.sendHtmlMail(mailInfo);//发送html格式  
   }  


在代码的最后,有两种发送方式,一般都会采用第二种方式,如果要给对方邮箱发送一些超链接等,就需要用html格式发送,这样,就能自动识别成超链接。下图是用文本方式发送的上面代码中的内容:

从上图中,我们可以看出,对应的标签是当做文本处理的,下图是用html格式发送的内容:
两种效果还是不同的,由于发送验证邮件需要用到链接,所以项目中采用html格式。

 

4、邮件中的链接

对于这个链接,还有一个重要的部分,那就是链接要链向哪里?需要哪些参数?这个链接怎么保证时效性(每个密码找回链接都是有时效性的)?

咱们首先看一下成熟的例子。学信网的密码找回链接如下:

 

现在能够确定的一点是:链接是指向某个servlet的,但是对应的参数就是一串没有规律的数字。这个key中隐含着什么样的信息?我认为包括两个部分:

①一部分是用户的用户名等可以辨识用户的内容,否则在用户点击链接后无法得知用户是谁;

②还一部分是邮件发送时的时间,因为只有参数中含有时间信息,才能使得该链接具有时效性(具体时效性可以是多 少,指向     的后台servlet中需要设定的部分)。

知道了以上的内容,还剩下最后一个问题,这个key值的乱码是怎么得来的?首先想到的就是加密。对的!就是加密得来的。否则以明文传送,不太安全。

至于这个加密的部分,就放在下面去讲。这里先来大体说一下加密:输入就是一串数,出来的就是一堆乱码,而且,这里的加密必须要是可逆的,否则点击链接后,不能还原成明文还是不顶用啊。既然知道了输入就是一个字符串,那么可以把时间和用户名串起来,这样就形成了一个字符串,然后在后台再解析出来。

 

5、点击确定后的信息回馈

首先来看一下【学信网】是怎么回馈的:

 

用户点击"确定"按钮,当邮件发送成功后,会显示"邮件发送成功",然后下面跟出一些提示性的语言blablabla....但是,若由于某些原因,导致邮件发送失败时,也要给出相应的提醒。

模仿上述反馈界面,我用了如下界面:

 

内部实现原理如下:

①在用户点击"确认"按钮后,后台首先获得当前的系统时间(按照ms计算),然后从前台得到了用户输入的登录邮箱,在两者之间加入一个"@"符连接起来为一个字符串(目的:便于在转变为明文的时候将两者区分开来,因为邮箱的第一个字母是不允许使用"@"符的,所以加入该符号不会引起歧义)。然后将该字符串加密成一串密文,这就是key值。前面再配上某个servlet的固定连接,就是完成了link链接;

②调用邮箱发送接口,实现邮箱转发。本系统中使用的邮箱接口如下:

/**
	 * 发送信息到用户的登录邮箱
	 * 
	 * @param userEmail
	 * @return
	 */
	public Boolean sendMail(String userEmail) {
		Boolean flag = false;
		try {
			// 获取当前系统时间
			Date now = new Date();
			String currentTime = "" + now.getTime();
			
			String urlString = "http://localhost:8080/EVM/forgetPasswordAction?method=resetPassword&key=";
			CodeEncryption cEncryption = new CodeEncryption();
			String encryptedCode = cEncryption.encryptCode(currentTime + "@" + userEmail);
			String link = urlString + encryptedCode;
			String adminMailAddress = "[email protected]";
			String contents = this.setContents(userEmail, link, adminMailAddress);
			//邮箱配置
			String serverHost = "smtp.163.com";
			String serverPort = "25";
			Boolean isValidate = true;
			String userName = "[email protected]";
			String password = "blablabla";
			String toMailAddress = userEmail;
			String subtitle = "科研数据管理平台注册账号密码找回 ";
			this.setMail(serverHost, serverPort, isValidate, userName, password, toMailAddress, subtitle, contents);
			
			flag = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return flag;
	}

 

上面的函数会返回一个布尔值,如果邮箱发送成功,则返回true,否则返回false。该函数是在service层,得到的布尔值将会返回到调用该函数的servlet层。这里面有个setMail函数,里面有很多参数,下面也给出对应的setMail函数:

/**
	 * 邮箱发送前的配置
	 * @param serverHost
	 * @param serverPort
	 * @param isValidate
	 * @param userName
	 * @param password
	 * @param toMailAddress
	 * @param subtitle
	 * @param contents
	 */
	public void setMail(String serverHost, String serverPort,
			Boolean isValidate, String userName, String password,
			String toMailAddress, String subtitle, String contents) {
		// 这个类主要是设置邮件
		MailSenderInfo mailInfo = new MailSenderInfo();
		mailInfo.setMailServerHost(serverHost);
		mailInfo.setMailServerPort(serverPort);
		mailInfo.setValidate(isValidate);
		mailInfo.setUserName(userName);
		mailInfo.setPassword(password);// 您的邮箱密码
		mailInfo.setFromAddress(userName);
		mailInfo.setToAddress(toMailAddress);
		mailInfo.setSubject(subtitle);
		mailInfo.setContent(contents);
		// 这个类主要来发送邮件
		SimpleMailSender sms = new SimpleMailSender();
	//	sms.sendTextMail(mailInfo);// 发送文体格式
		SimpleMailSender.sendHtmlMail(mailInfo);// 发送html格式
	}

 

若邮件发送失败,servlet会接收到false的信息。先来看一下对应的servlet层是怎么做的:

/**
	 * 密码忘记,发送信息到邮箱
	 * @param request
	 * @param response
	 */
	public void passwordForgotten(HttpServletRequest request, HttpServletResponse response) {
		String userEmail = request.getParameter("userEmailInput");
		ForgetPasswordService fPasswordService = new ForgetPasswordService();
		Boolean flag = fPasswordService.sendMail(userEmail);
		
		try {
			if (flag) {
				RequestDispatcher rDispatcher = request.getRequestDispatcher("fpEmailSended.jsp");
				String str1 = userEmail.substring(0, 3);
				String str2 = userEmail.substring(userEmail.indexOf("@"));
				String str3 = str1 + "***" + str2;
				request.setAttribute("userEmail", str3);
				request.setAttribute("flag", "true");
				rDispatcher.forward(request, response);
			} else {
				RequestDispatcher rDispatcher = request.getRequestDispatcher("fpEmailSended.jsp");
				String str1 = userEmail.substring(0, 3);
				String str2 = userEmail.substring(userEmail.indexOf("@"));
				String str3 = str1 + "***" + str2;
				request.setAttribute("userEmail", str3);
				request.setAttribute("flag", false);
				rDispatcher.forward(request, response);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

 

这里,flag出现了2个分支:在正确的情况下,给出"邮箱发送成功"的反馈界面,并由 RequestDispatcher指向一个新的jsp页面,即点击确认的那个页面和信息回馈的页面不是同一个页面。

若flag为true,则走第一个分支,结果如下:

flag为false,则走第二个分支,结果如下:

下面我们来看一下发送成功的情况,这样在邮箱285***@qq.com中就出现了一封邮件:

 

 

6、点击邮箱链接触发的操作

点击链接后,会进入到指定的servlet。从链接中可以看到,进入到的是一个名称叫forgetPasswordAction的servlet中。后面跟着两个参数:method----指定servlet触发什么操作,key----包含有发送时间(ms计算)和用户名信息,在servlet中会转化为明文。其中,"发送时间"用于判断用户点击链接的时候,该链接是否已经过时,"用户名"用于servlet跳转到jsp页面的前端显示。

下面看一下指定的servlet中的相关内容:

/**
	 * 用户在点击邮箱里的链接后,进入该函数
	 * @param request
	 * @param response
	 */
	public void resetPassword(HttpServletRequest request, HttpServletResponse response) {
		String key = request.getParameter("key");
		ForgetPasswordService fService = new ForgetPasswordService();
		List datasList = new ArrayList();
		datasList = fService.getExplicitCode(key);
		String time = datasList.get(0);
		String userEmail = datasList.get(1);
		Boolean timeFlag = fService.judgeOfTime(time);	//判断用户点击链接的时间是否过期
		String flag = "false";
		if (timeFlag) {
			flag = "true";
		}
		List resultList = new ArrayList();
		resultList.add(flag);
		resultList.add(userEmail);
		
		try {
			RequestDispatcher rDispatcher = request.getRequestDispatcher("resetPassword.jsp");
			request.setAttribute("datasList", resultList);
			rDispatcher.forward(request, response);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

7、重置密码页面

首先判断时间是否过期,并将标记发送到jsp中。下面给出resetPassword.jsp中的部分内容:

首先获得后台传过来的参数:

<%
		List datasList = (List) request.getAttribute("datasList");
		String flag = datasList.get(0);	//链接是否过期
	//	flag = "false";
		String userEmail = datasList.get(1);	//用户登录邮箱
	%>

得到flag,将flag设置到隐藏的input中:

在本jsp页面中,有两个大的div,分别对应flag的不同标记。若flag的标记为true,则显示一个div的内容,隐藏另一个div,若flag为false,则反过来。

flag为true时显示的内容:


 

 

对应的页面如下:

若flag为false,即链接已过期,对应的html代码如下;

  重置密码链接已过期

       没有收到重置密码邮件,您可以到邮件垃圾箱里找找。
       或者点击:【重新发送重置密码邮件】


对应的页面如下:

 

jsp中的js控制代码:

	var flag = $("#hiddenInput").val();
	if(flag == "true") {
		$("#failureDivId").hide();
		$("#resetPasswordForm").show();
	} else {
		$("#resetPasswordForm").hide();
		$("#failureDivId").show();
	}

 

 

 

  • 重置界面

进入重置页面,用户页面已自动填充(根据邮箱链接中的key值可得到用户名),用户这时候可进行密码重置。这里需要说明的一点是:一般的找回密码流程,最后都会让用户重新设置密码,不会把原来用户忘记的密码发送给用户。

其实,在重置密码页面,主要需要说明的就是密码的加密过程。

1、加密算法

我这里用到的是3DES密码加密,相关链接可以直接搜索3DES百度百科,里面有示例代码。这里我贴出相关的代码片段:

byte[] encoded = encryptMode(keyBytes, szSrc.getBytes());
 System.out.println("加密后的字符串:" + new String(encoded));
 
 byte[] srcBytes = decryptMode(keyBytes, encoded);
 System.out.println("解密后的字符串:" + (new String(srcBytes)));

 

这里可以看出来,加密之后的密文是一个byte数组,难就难在这个数组上,虽然可以使用new String(),转化为字符串,但是这个字符串是乱码,我试过,无论使用什么编码格式,转化出来的字符串都是乱码,这是一个问题,另一个问题是,转化后的字符串是无法再回退到byte数组的,即这个过程是不可逆的。基于以上两点缺陷,不能将得到的密文byte数组直接转化为字符串。

为了解决这个问题,我想到了另外一种方式,很巧妙的化解了byte数组的显示问题,而且,此种方式,在一定程度上还算是对byte数组的再次加密,方法如下:

/**
	 * 将明文字符串加密成密文,然后以字符串的形式返回.
	 * 说明:在将字符串明文加密后,得到的是一个byte[]数组,这时候如果将byte[]数组转化为字符串,是乱码。解决方式如下:
	 * 步骤1:对于byte[]数组中的每个元素,因为范围都在-128~127之间,所以为了方便表达,统一加上128,都转化为0或者正数;
	 * 步骤2: 利用Integer.toBinaryString()函数,将每个元素转化为16进制,结果若为单个的,用g在右侧补成2位的;
	 * 步骤3:将所有的结果统一串成一个整个的字符串,作为结果返回。
	 * @param explicitPassword
	 * @return
	 */
	public String encryptCode(String explicitString) {
		Security.addProvider(new com.sun.crypto.provider.SunJCE());
		
		String implicitString = "";
		byte[] encoded = encryptMode(keyBytes, explicitString.getBytes());
		
		List codeArrTemp = new ArrayList();
		for (int i = 0; i < encoded.length; i++) {
			codeArrTemp.add(String.valueOf(Integer.toHexString((int) encoded[i] + 128)));
		}
		List codeArr = new ArrayList();
		for (int i = 0; i < codeArrTemp.size(); i++) {
			if (codeArrTemp.get(i).length() == 1) {
				String temp = codeArrTemp.get(i) + "g";
				codeArr.add(temp);
			} else {
				codeArr.add(codeArrTemp.get(i));
			}
		}
		
		for (int i = 0; i < codeArr.size(); i++) {
			implicitString += codeArr.get(i);
		}
		
		return implicitString;
	}

注释中,已经给出了我的转化步骤,下面再给出与上面加密配套的解密代码:

/**
	 * 对密文解密,返回明文
	 * @param implicitString
	 * @return
	 */
	public String deEncryptCode(String implicitString) {
		Security.addProvider(new com.sun.crypto.provider.SunJCE());
		
		String explicitString = null;
		byte[] encoded = null;
		//根据implicitString得到encoded
		List codeArrTemp = new ArrayList();
		for (int i = 0; i < implicitString.length(); i += 2) {
			codeArrTemp.add(implicitString.substring(i, i + 2));
		}
		List codeArr = new ArrayList();
		for (int i = 0; i < codeArrTemp.size(); i++) {
			if (codeArrTemp.get(i).contains("g")) {
				codeArr.add(codeArrTemp.get(i).substring(0, 1));
			} else {
				codeArr.add(codeArrTemp.get(i));
			}
		}
		encoded = new byte[codeArr.size()];
		for (int i = 0; i < codeArr.size(); i++) {
			encoded[i] = (byte) (Integer.parseInt(codeArr.get(i), 16) - 128);
		}
		
		byte[] srcBytes = decryptMode(keyBytes, encoded);
		explicitString = new String(srcBytes);
		return explicitString;
	}

以上,就完成了字符串的加/解密过程,上面的代码可以直接拿过来用。其中,以上两个函数中,用到了原来的加/解密接口,这里顺便也把代码贴出来:

3DES加密:

// DES,DESede,Blowfish
	// keybyte为加密密钥,长度为24字节
	// src为被加密的数据缓冲区(源)
	public static byte[] encryptMode(byte[] keybyte, byte[] src) {
		try {
			// 生成密钥
			SecretKey deskey = new SecretKeySpec(keybyte, Algorithm);
			// 加密
			Cipher c1 = Cipher.getInstance(Algorithm);
			c1.init(Cipher.ENCRYPT_MODE, deskey);
			return c1.doFinal(src);
		} catch (java.security.NoSuchAlgorithmException e1) {
			e1.printStackTrace();
		} catch (javax.crypto.NoSuchPaddingException e2) {
			e2.printStackTrace();
		} catch (java.lang.Exception e3) {
			e3.printStackTrace();
		}
		return null;
	}

3DES解密:

// keybyte为加密密钥,长度为24字节
	// src为加密后的缓冲区
	public static byte[] decryptMode(byte[] keybyte, byte[] src) {
		try {
			// 生成密钥
			SecretKey deskey = new SecretKeySpec(keybyte, Algorithm);
			// 解密
			Cipher c1 = Cipher.getInstance(Algorithm);
			c1.init(Cipher.DECRYPT_MODE, deskey);
			return c1.doFinal(src);
		} catch (java.security.NoSuchAlgorithmException e1) {
			e1.printStackTrace();
		} catch (javax.crypto.NoSuchPaddingException e2) {
			e2.printStackTrace();
		} catch (java.lang.Exception e3) {
			e3.printStackTrace();
		}
		return null;
	}

以上3段代码就是整个加密、解密的过程。在我们数据库中的密码加密,也是用到了以上的方法。

 

2、"重置密码"点击"确定"后的信息反馈页面

这一步类似于前面的发送邮箱的确认,也是分为两支:成功还是失败。这里再次贴出重置密码的页面:

在用户点击"确定"后,后台将密码加密,然后存入数据库,等操作完成后,后台的servlet跳转到一个新页面:resetPassSuccOrFail.jsp,即给出信息反馈,表示是重置成功还是失败。

这里的操作和前面都是类似的,也就不多说了。下面给出两张分支的页面:

当修改密码失败时:

至此,"忘记密码"功能已实现完毕。

更多2019年的技术文章,欢迎关注我的微信公众号:码不停蹄的小鼠松(微信号:busy_squirrel),也可扫下方二维码关注获取最新文章哦~

你可能感兴趣的:(java,web前端,web前端,J2EE,忘记密码功能,加密,发送邮件)