會話技術之記住密碼與免登陸的實現

在前幾篇文章中,我們詳細的介紹了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這一專欄會是一個系列博客,喜歡的話可以持續關注,如果本文對你有所幫助,還請還請點贊、評論加關注。

​有任何疑問,可以評論區留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章