会话技术之记住密码1、2、3

在前几篇文章中,我们详细的介绍了Cookie及其他会话相关的概念,我们知道Cookie是一种在客户端保存会话信息的技术,其出现大大简化了程序员的工作。Cookie作为一个简单、便捷、实用的客户端会话技术,其重要性是不言而喻的,今天我们就一起来看下Cookie的几种应用场景。

资源分配图

​Cookie翻译成中文叫曲奇饼、小饼干,还是很贴切的,如上图一样,每个小饼干上可以有不同的花纹,表明存储一下小而重要的信息。

1.记住密码的简单实现

​记住密码这个功能想必大家都应该体验过,比如QQ,在输入一次密码后,很长一段时间我们无需再次输入密码。下面我们来看下应该如何实现吧。

资源分配图

​这部分的工作主要在于前端,下面给出前端代码,首先是Login.jsp。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="./js/jquery.cookie.js"></script>
<script src="./js/login.js"></script>

<title>Insert title here</title>
</head>
<body>
	<form id="loginForm" action="xxx" method="post">
		<input id="username" name="username" type="text"/>
		<br />
		<input id="password" name="password" type="password" />
		<br />
    	<!-- form提交前存储Cookie -->
		<button type="button" οnclick="loginSubmit()">提交</button>
	</form>
</body>
</html>

​其中jquery.cookie.js的下载链接如下:https://github.com/carhartl/jquery-cookie/tree/master/src

​login.js中的代码如下,其中定义了loginSubmit方法和页面初始化动作。

//页面DOM加载完毕后执行
//如果本地存储了账号、密码,则将其填充到输入框中
$(document).ready(function(){
	const cookieName = $.cookie('loginName');
	const cookiePwd = $.cookie('loginPwd');
	console.log(cookieName + "," + cookiePwd);
	if(cookieName != '' && cookieName != null){
		$("#username").val(cookieName);
	}
	if(cookiePwd != '' && cookiePwd != null){
		$("#password").val(cookiePwd);
	}
})

//登录
function loginSubmit(){
	console.log("执行了js中的方法");
	//将账号名和密码存入Cookie中
	const userName = $("#username").val();
	$.cookie('loginName', userName);
	const password = $("#password").val();
	$.cookie('loginPwd', password);
  //阻断form表单提交,测试时使用
  //return false;
	$("#loginForm").submit();
}

​这里我们就不给出测试的截图了,代码已经全部给出,可自行进行测试。对于记住密码功能而言,仅仅是登录时自动填充明显的功能不足,这里因为已经设置了Cookie,每次请求是都会自动携带账号密码信息,因此我们可以在后端增加一个拦截器,即使Session过期了,我们也可以在密码验证通过后再创建一个新的,做到无须重新登录。

2.使用token增强安全性

​在上面的例子中,我们是将密码以明文的方式存储在浏览器端,虽然浏览器会将密码加密后存储,但是在网络传输中,密码还是以明文的方式进行传输的,因此安全性较低。为了解决此问题,一般我们存储时,不会直接将铭文密码存储在客户端,而是通过加密的技术,在客户端存储一个token。

​Token我们可以理解成一个令牌,我们还是以上面的记住密码为例,当用户登录成功后,我们可以将密码等比较隐私的信息通过加密的方式得到一个加密后的字符串存储在客户端,等下次如有需要时(比如Session过期),直接校验token的有效性,而不是直接使用明文的账号密码来进行验证。

​这里为了安全和简单,我们就简单的使用MD5来进行加密(这里推荐对称加密加密算法AES)。加密部分的工作我们也放在后端,在验证用户名密码正确后进行。

​这里我们来看下LoginServlet中的doGet方法的代码:

protected void doGet(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException {
  request.setCharacterEncoding("UTF-8");
  response.setContentType("text/html;charset=utf-8");

  String userName = request.getParameter("username");
  String password = request.getParameter("password");
 
  UserService userService = UserService.getInstance();
  try {
    String result = userService.loginByNameAndPassword(userName, password);
    boolean isSuccess = false;
    // 登录成功
    if ("Success".equals(result)) {
      isSuccess = true;
      // 创建Session
      HttpSession session = request.getSession();
      // 生成一个token,当Session过期时,可以通过此token来验证是否登录过
      // 规则,userName-password 的md5值
      String token = MD5Util.encodeByMD5(userName + "-" + password);
      Cookie loginCookie = new Cookie("loginToken", token);
      Cookie nameCookie = new Cookie("userName", userName);
      // 将Cookie添加到response中
      response.addCookie(loginCookie);
      response.addCookie(nameCookie);
    } else {
      // 登录失败
      System.out.println("登录失败的原因为:" + result);
      // 跳转登录失败页面
      // response.sendRedirect("/FirstProject/index.jsp");
    }

    // 输出页面
    // ...
}

​其中省略的部分和Service的具体实现可以参考上文。在上例中,我们将用户名和密码拼接起来后进行加密,因为MD5加密的不可解密,因此我们还创建一个保存用户名的cookie。在下次需要验证token信息时,只需根据cookie中的loginName,查询到数据库中的密码,拼接后计算MD5加密后的值,与token进行比对即可。简单的示例代码如下:

/**
	 * 根据登录名校验token是否正确
	 * 
	 * @param loginName
	 * @param loginToekn
	 * @return true || false
	 * @throws ClassNotFoundException
	 * @throws SQLException
	 */
public boolean checkToken(String loginName, String loginToekn) throws ClassNotFoundException, SQLException {
  UserDao userDao = new UserDao();
  User user = userDao.findUserByName(loginName);
  String password = user.getPassword();
  String newToken = MD5Util.encodeByMD5(loginName + "-" + password);
  if (newToken.equals(loginToekn)) {
    System.out.println("token检验正确");
    return true;
  }
  System.out.println("token检验错误");
  return false;
}

​MD5Util的代码如下:

package xxx;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {

  /**
	 * 对传入的字符串进行MD5加密
	 * 
	 * @param str
	 * @return 加密后的字符串
	 */
  public static String encodeByMD5(String str) {
    byte[] bytes = null;
    try {
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      bytes = md5.digest(str.getBytes("utf-8"));
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }
    return bytesTo16BString(bytes);
  }

  /**
	 * 将字节数组转换为16进制格式的字符串
	 * 
	 * @param bytes
	 * @return
	 */
  private static String bytesTo16BString(byte[] bytes) {
    StringBuffer res = new StringBuffer();
    int num = 0;
    for (int i = 0; i < bytes.length; i++) {
      num = bytes[i] > 0 ? bytes[i] : 255 + bytes[i];
      String hex = Integer.toHexString(num);
      res.append(hex.length() < 2 ? 0 + hex : hex);
    }
    return res.toString();
  }

}

3.借鉴JWT的思想

JWT(JSON Web Token)技术是一种开放的行业标准RFC 7519方法,是一种紧凑的,URL安全的方法,用于表示要在两方之间转移的声明。JWT中的声明被编码为JSON对象,用作JWS(JSON Web Signature)结构的有效负载或JWE (JSON Web Encryption)结构的纯文本,从而使声明可以进行数字签名或完整性保护 带有消息验证码(MAC)和/或加密的消息。

​ 一句话,JWT就是将敏感信息放入JSON对象中,并添加header、签名等信息,再通过加密的方式将整个对象转为字符串,存储在客户端。本文也参考JWT的思想,简单的手动实现一下。

​ 首先我们来简单的定义一个对象,来存储信息(也可直接new一个JSONObject),代码如下:

public class JwtDataPojo {

	//登录用户
	private String name;
	
	// 其他信息...
	
	//登录时间
	private Date loginDate;

	// Getter、Setter方法
	// ...
}

​我们将LoginServlet中登录成功的代码修改如下:

protected void doGet(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException {
  //...
 
  UserService userService = UserService.getInstance();
  try {
    String result = userService.loginByNameAndPassword(userName, password);
    boolean isSuccess = false;
    // 登录成功
    if ("Success".equals(result)) {
      isSuccess = true;
      JwtDataPojo dataPojo = new JwtDataPojo();
      dataPojo.setName(userName);
      dataPojo.setLoginDate(new Date());
      // 生成加密后的字符串
      String jwtToken = AESUtil.encrypt(JSON.toJSONString(dataPojo), "123456");
      Cookie cookie = new Cookie("JwtToken", jwtToken);
      // 将Cookie添加到response中
      response.addCookie(cookie);
    } else {
      // 登录失败
      System.out.println("登录失败的原因为:" + result);
      // 跳转登录失败页面
      // response.sendRedirect("/FirstProject/index.jsp");
    }

    // 输出页面
    // ...
}

​因为JwtDataPojo中有登录时间,所以我们可以具体来在服务器端验证是否超时。上面代码中的resolveJwtToken方法如下:

/**
	 * 根据传入的字符换和密码,解析为JwtToken对象
	 * 
	 * @param content
	 * @param key
	 * @return JwtDataPojo
	 * @throws Exception
	 */
public JwtDataPojo resolveJwtToken(String content, String key) throws Exception {
  String jsonString = AESUtil.decrypt(content, key);
  if(StringUtils.isNullOrEmpty(jsonString)) {
    throw new Exception("token错误");
  }
  JwtDataPojo dataPojo = JSON.parseObject(jsonString, JwtDataPojo.class);
  // 超过三十分钟过期
  if (System.currentTimeMillis() - dataPojo.getLoginDate().getTime() > 1000 * 60 * 30) {
    throw new Exception("登录过期");
  }
  return dataPojo;
}

​对于上面的超时时间,我们可以自行修改,并且可以采取Session的最大空闲间隔时间的策略,每次拦截器中验证JwtToken通过后,更新LoginDate。

​AESUtil的代码可以参考这篇文章:java 使用AES对数据进行加密和解密

​ 在上面使用的Fastjson,jar包的下载地址

4.总结

​在上一篇中,我们讲解了登录状态的保持,在本文中,我们将cookie的有效期(或者JwtToken的时效)进行修改,可以达到同样的效果,而且,我们通过服务端的加密后,安全性也能得到保障。

​对于JWT,本文中知识借鉴了其一点思想,完成的JWT的功能会更强,大家有兴趣可以自行学习,后面有时间我会单独写一篇关于JWT的文章。

参考阅读:

  1. 会话技术之Cookie详解

  2. 会话技术之Session详解

  3. 会话技术之登录状态的保持

  4. JWT官网介绍

  5. 什么是 JWT – JSON WEB TOKEN


​又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主自己进行整理并结合自身的理解进行总结,如果有什么错误,还请批评指正。

​Java web这一专栏会是一个系列博客,喜欢的话可以持续关注,如果本文对你有所帮助,还请还请点赞、评论加关注。

​有任何疑问,可以评论区留言。

发布了56 篇原创文章 · 获赞 973 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_34666857/article/details/105639767