更多2019年的技术文章,欢迎关注我的微信公众号:码不停蹄的小鼠松(微信号:busy_squirrel),也可扫下方二维码关注获取最新文章哦~
对于忘记密码功能,一般都是通过2种方式找回:一种是通过预留电话号码发送验证码找回,另一个是通过设定邮箱找回。对于具体的找回流程,参见:http://www.yixieshi.com/ucd/9207.html。
这里结合目前做的项目,详细的说明一下密码找回的过程。这里用的是邮箱找回。根据一般的忘记密码的流程来说明。我采用的流程是:
【登录】 --> 【点击忘记密码】 --> 【输入个人邮箱和验证码】 --> 【系统发送邮箱验证】 --> 【用户在限定时间内登录邮箱,点击链接,进入重置密码页面】 --> 【重置密码完毕,点击进入登录界面】。
先给出我做的登录界面,其中包含"忘记密码"功能
一般登录界面都有“忘记密码”选项,这里不多说。
点击"忘记密码"选项后,这时,页面会跳向一个新页面,即“忘记密码”页面。首先给出"忘记密码"按钮对应的跳转代码片段:
忘记密码?
在点击后,跳到新页面:
这里的界面设计是模仿的网页163邮箱的忘记密码,给出链接:http://reg.163.com/getpasswd/RetakePassword.jsp?from=mail163。
该界面中,有几个关键点:一个是验证码的生成,一个是邮箱发送的功能,还一个就是点击确定后的反馈界面。主要包含这3个方面的内容,这3点以下将会详细介绍。
参考文章: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在运行后,都会发送一个验证码图片到前台,也就是所谓的"刷新"。
这里用到了一个js库:jquery.validate.min.js。这个只能判断输入的内容是否符合规则。
由于要输入用户的登录邮箱,规则正确只是第一步,还要查看输入的邮箱在数据库中是否存在,如果不存在,是不能允许发送密码重置邮件的。所以对邮箱正确性的检查,需要调用一次$.getJSON();进行验证;
对于验证码的验证,由于在验证码生成的servlet中,已经设置了
// 将验证码内容保存进session中,用于验证用户输入是否正确时使用
HttpSession session = request.getSession(true);
session.removeAttribute("rand");
session.setAttribute("rand", codeNumbers);
所以这时候还要进行一次后台通讯,查看是否验证码输入正确。但是,有个问题我还没来得急了解:这个验证码的作用是什么?这个也是我在后期需要进行追加的内容。
参考文章: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格式。
对于这个链接,还有一个重要的部分,那就是链接要链向哪里?需要哪些参数?这个链接怎么保证时效性(每个密码找回链接都是有时效性的)?
现在能够确定的一点是:链接是指向某个servlet的,但是对应的参数就是一串没有规律的数字。这个key中隐含着什么样的信息?我认为包括两个部分:
①一部分是用户的用户名等可以辨识用户的内容,否则在用户点击链接后无法得知用户是谁;
②还一部分是邮件发送时的时间,因为只有参数中含有时间信息,才能使得该链接具有时效性(具体时效性可以是多 少,指向 的后台servlet中需要设定的部分)。
知道了以上的内容,还剩下最后一个问题,这个key值的乱码是怎么得来的?首先想到的就是加密。对的!就是加密得来的。否则以明文传送,不太安全。
至于这个加密的部分,就放在下面去讲。这里先来大体说一下加密:输入就是一串数,出来的就是一堆乱码,而且,这里的加密必须要是可逆的,否则点击链接后,不能还原成明文还是不顶用啊。既然知道了输入就是一个字符串,那么可以把时间和用户名串起来,这样就形成了一个字符串,然后在后台再解析出来。
用户点击"确定"按钮,当邮件发送成功后,会显示"邮件发送成功",然后下面跟出一些提示性的语言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页面,即点击确认的那个页面和信息回馈的页面不是同一个页面。
下面我们来看一下发送成功的情况,这样在邮箱285***@qq.com中就出现了一封邮件:
点击链接后,会进入到指定的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();
}
}
首先判断时间是否过期,并将标记发送到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值可得到用户名),用户这时候可进行密码重置。这里需要说明的一点是:一般的找回密码流程,最后都会让用户重新设置密码,不会把原来用户忘记的密码发送给用户。
其实,在重置密码页面,主要需要说明的就是密码的加密过程。
我这里用到的是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段代码就是整个加密、解密的过程。在我们数据库中的密码加密,也是用到了以上的方法。
这一步类似于前面的发送邮箱的确认,也是分为两支:成功还是失败。这里再次贴出重置密码的页面:
在用户点击"确定"后,后台将密码加密,然后存入数据库,等操作完成后,后台的servlet跳转到一个新页面:resetPassSuccOrFail.jsp,即给出信息反馈,表示是重置成功还是失败。
这里的操作和前面都是类似的,也就不多说了。下面给出两张分支的页面:
当修改密码失败时:
至此,"忘记密码"功能已实现完毕。
更多2019年的技术文章,欢迎关注我的微信公众号:码不停蹄的小鼠松(微信号:busy_squirrel),也可扫下方二维码关注获取最新文章哦~