文為小編原創文章,首發于Java識堂,一個高原創,高收藏,有干貨的微信公眾號,一起成長,一起進步,歡迎關注
原來分享過一篇文章,Java自定義注解及應用,當時為了能突出重點,直接在url中傳了用戶的所屬角色,并寫了一般的做法。加上最近看了一些人的簡歷,發現神奇的相似,都有類似商城的項目,為了不至于問些特別Low的問題,便總結了一下登錄這個模塊所涉及的東西
Http協議使用的是無狀態連接,這樣會造成什么問題呢?看如下Demo
測試
HttpServletRequest對象代表客戶端的請求,當客戶端通過HTTP協議訪問服務器時,HTTP請求頭中的所有信息都封裝在這個對象中,當在一個請求中時HttpServletRequest中的信息可以共享,而在不同的請求中HttpServletRequest并不能共享,這樣就會造成用戶確實進行過登錄操作,但是跳到購物車頁面時發現并沒有東西,因為應用并不知道訪問這個頁面的用戶是誰
我們可以用一個HttpSession對象保存跨多個請求的會話狀態,上面的例子就是保存用戶名,看下圖理解為什么HttpSession可以跨請求保存狀態
對客戶的第一個請求,容器會生成一個唯一的會話ID,并通過響應把它返回給客戶。客戶再在以后的每一個請求中發回這個會話ID。容器看到ID后,就會找到匹配的會話,并把這個會話與請求關聯
將上面代碼改成如下,再測試
果然能保存會話狀態了,客戶和容器如何交換會話ID信息呢?其實是通過cookie實現的
看上面能保存會話的代碼,我們并沒有對cookie進行操作啊,其實是容器幾乎會做cookie的所有工作,從最開始的Servlet開始講這些操作是如何實現的,先看一下Servlet執行過程
容器使用部署描述文件把URL映射到Servlet ,一個Servlet可以有3個名字,(1)用戶知道的URL名,(2)部署人員知道的內部名,(3)實際的文件名
加入使用Spring MVC時要在web.xml中配置如下內容
根據url-pattern->servlet-name->servlet-class的三級映射關系,容器即可根據用戶輸入的URL找到對應的Servlet
從這個就可以看出其實Spring MVC框架其實在Servlet上面封裝了一層,當我們自己用Servlet編寫程序時,可以從HttpServletRequest中獲取HttpSession,如下
public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } }
HttpSession session = req.getSession();
我們只需要寫上述一行代碼即可,來看看容器幫我們做了哪些事情
HttpSession session = req.getSession();
與響應生成會話ID和cookie時用的方法一樣
if (請求包含一個會話ID cookie) { 找到與該ID匹配的會話 } else if (沒有會話Id cookie OR 沒有與此會話ID匹配的當前會話) { 創建一個新會話 }
如上面用的方法,我們并沒有直接從HttpServletRequest 中獲取HttpSession
public String login(HttpSession session, @RequestParam("username") String username)
能直接獲取到HttpSession,其實是框架幫我們執行了HttpSession session = req.getSession(),然后設置進來的。我們可以設置session的過期時間,以保證用戶登錄后長期不操作需要重新登錄
當整個服務是分布式的該怎么處理呢?用戶在服務器A上登錄,結果在服務器B上查看購物車信息,因為在A上登錄,HttpSession存在A服務器上,當訪問B服務器上的購物車信息因為獲取不到用戶登錄的HttpSession,就會認為用戶沒有登錄,這種情況該怎么處理呢?
實現分布式Session有多種方式,這里就介紹一下用Redis實現分布式Session,其實Spring Session項目就使用Redis實現Session共享的
理解了單機Session,分布式Session也不難理解,主要步驟如下
當用戶登出時只要刪除key為token的hash,并且將cookie的最長時間設置為0,重新放回HttpServletResponse即可,鑒于篇幅限制,就不寫具體代碼了
直接存儲
以前系統存儲密碼時都是類似如下形式
假如用戶信息泄露,用戶的賬號安全將受到威脅,參考CSDN密碼泄露事件
加密存儲
既然明文存儲會有安全問題,那就加密存儲,一般常用的加密算法是MD5和SHA,當用戶注冊時,數據庫中保存的密碼是加密后的密碼,當用戶登錄時先對登錄的密碼進行MD5,然后和數據庫中的密碼比對,正確則登錄成功,失敗則登錄失敗
以為這樣就足夠安全了?其實遠遠不夠,有的人將各種密碼的MD5值都算出來,做成一個字典,前面說的泄露的CSDN的密碼就是一個很好的素材,這樣就可以通過
泄露密碼的MD5值->MD5字典->原始的字符串的映射關系,得到泄露的密碼,針對這種情況,有2種做法,一種是將密碼多次進行MD5,即對加密后的MD5值再次進行MD5,另一種就是加鹽
加鹽存儲
由于鹽值時隨機生成的,我們算一下破解一個用戶的密碼需要多長時間,假如數據庫中密碼是如此生成的MD5(明文密碼+Salt),MD5的方式也被壞人知道了,假如壞人有600w個字典,得先對這些字典加Salt做一次MD5再匹配,而且還有可能匹配不出來,破解一個賬號的成本就這么高,而且鹽值和密碼的方式進行MD5的方式也多種多樣啊,Salt可以插中間,Salt倒序再進行MD5。當然還可以這樣啊MD5(Salt[0] + 明文密碼 + Salt[5])。如果還覺得不夠安全,還可以對加鹽生成的MD5值再次MD5啊,次數由你定,這樣幾乎是破解不了
用session做一個驗證碼登錄的案例
轉發:實現服務器的跳轉
重定向是瀏覽器的跳轉
1. 問:什么時候使用轉發,什么時候使用重定向?
如果要保留請求域中的數據,使用轉發,否則使用重定向。
以后訪問數據庫,增刪改使用重定向,查詢使用轉發。
2. 問:轉發或重定向后續的代碼是否還會運行?
無論轉發或重定向后續的代碼都會執行
https://ww.lanzous.com/b015g184b 密碼:b53v
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>login</title>
<script>
window.onload = function(){
document.getElementById("img").onclick = function(){
this.src="/Project1/checkCodeServlet?time="+new Date().getTime();
}
}
</script>
<style>
div{
color: red;
}
</style>
</head>
<body>
<form action="/Project1/loginServlet" method="post">
<table>
<tr>
<td>用戶名</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密碼</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>驗證碼</td>
<td><input type="text" name="checkCode"></td>
</tr>
<tr>
<td colspan="2"><img id="img" src="/Project1/checkCodeServlet"></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="登錄"></td>
</tr>
</table>
</form>
<div><%=request.getAttribute("cc_error") == null ? "" : request.getAttribute("cc_error")%></div>
<div><%=request.getAttribute("login_error") == null ? "" : request.getAttribute("login_error") %></div>
</body>
</html>
package cn.itcast.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.設置request編碼
request.setCharacterEncoding("utf-8");
//2.獲取參數
String username = request.getParameter("username");
String password = request.getParameter("password");
String checkCode = request.getParameter("checkCode");
//3.先獲取生成的驗證碼
HttpSession session = request.getSession();
String checkCode_session = (String) session.getAttribute("checkCode_session");
//刪除session中存儲的驗證碼
session.removeAttribute("checkCode_session");
//3.先判斷驗證碼是否正確
if(checkCode_session!= null && checkCode_session.equalsIgnoreCase(checkCode)){
//忽略大小寫比較
//驗證碼正確
//判斷用戶名和密碼是否一致
if("zhangsan".equals(username) && "123".equals(password)){//需要調用UserDao查詢數據庫
//登錄成功
//存儲信息,用戶信息
session.setAttribute("user",username);
//重定向到success.jsp
response.sendRedirect(request.getContextPath()+"/success.jsp");
}else{
//登錄失敗
//存儲提示信息到request
request.setAttribute("login_error","用戶名或密碼錯誤");
//轉發到登錄頁面
request.getRequestDispatcher("/login.jsp").forward(request,response);
}
}else{
//驗證碼不一致
//存儲提示信息到request
request.setAttribute("cc_error","驗證碼錯誤");
//轉發到登錄頁面
request.getRequestDispatcher("/login.jsp").forward(request,response);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}
package cn.itcast.servlet;
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
@WebServlet("/checkCodeServlet")
public class CheckCodeServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int width = 100;
int height = 50;
//1.創建一對象,在內存中圖片(驗證碼圖片對象)
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
//2.美化圖片
//2.1 填充背景色
Graphics g = image.getGraphics();//畫筆對象
g.setColor(Color.PINK);//設置畫筆顏色
g.fillRect(0,0,width,height);
//2.2畫邊框
g.setColor(Color.BLUE);
g.drawRect(0,0,width - 1,height - 1);
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789";
//生成隨機角標
Random ran = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 4; i++) {
int index = ran.nextInt(str.length());
//獲取字符
char ch = str.charAt(index);//隨機字符
sb.append(ch);
//2.3寫驗證碼
g.drawString(ch+"",width/5*i,height/2);
}
String checkCode_session = sb.toString();
//將驗證碼存入session
request.getSession().setAttribute("checkCode_session",checkCode_session);
//2.4畫干擾線
g.setColor(Color.GREEN);
//隨機生成坐標點
for (int i = 0; i < 10; i++) {
int x1 = ran.nextInt(width);
int x2 = ran.nextInt(width);
int y1 = ran.nextInt(height);
int y2 = ran.nextInt(height);
g.drawLine(x1,y1,x2,y2);
}
//3.將圖片輸出到頁面展示
ImageIO.write(image,"jpg",response.getOutputStream());
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1><%=request.getSession().getAttribute("user")%>,歡迎您</h1>
</body>
</html>
系列文章旨在記錄和總結自己在Java Web開發之路上的知識點、經驗、問題和思考,希望能幫助更多(Java)碼農和想成為(Java)碼農的人。
提示:盡量使用頭條APP閱讀,頭條網頁展示代碼會有問題。
前面的文章我們實現了租房網平臺的用戶注冊、用戶登錄、會話跟蹤等功能,本篇文章繼續實現用戶的登出/退出功能。
實際上,我們之前的版本中,只要用戶登錄之后,在每個頁面當中已經有退出按鈕,如下圖:
其對應的代碼在我們的 include.jsp 中:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.List" %>
<%@ page import="houserenter.entity.House" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>租房網</title>
</head>
<body>
<h1>你好,${sessionScope.userName}!歡迎來到租房網! <a href="login.html">退出</a></h1>
<br><br>
可以看到,這個退出按鈕就是一個普通的鏈接,直接返回到登錄頁面而已。那這樣有什么不好的地方呢?我們做這樣一個實驗:
結論就是這樣簡單的登錄功能很不安全。
究其原因,其實是我們采用了session進行會話跟蹤,只要session不過期,Servlet容器(我們這里是Tomcat)中的該session對象就還有效,綁定到該session對象中的數據也就還有效。
不過,因為HTTP是基于TCP的,所以不同的TCP連接肯定會產生不同的session,大家有興趣的話可以自行測試一下TCP連接和session之間的關系。
所以,我們的用戶退出功能必須是這樣的:
Servlet容器感知用戶的退出很簡單,只要發送一個請求給Servlet容器即可。
所以我們設計一個用戶退出的動作,讓上面的退出按鈕指向該動作:
<h1>你好,${sessionScope.userName}!歡迎來到租房網! <a href="logout.action">退出</a></h1>
我們可以在HouseRenterController中添加處理用戶退出請求的動作:
@GetMapping("/logout.action")
public ModelAndView getLogout(HttpSession session) {
System.out.println("session: " + session);
System.out.println("session id: " + session.getId());
session.invalidate();
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:login.html");
return mv;
}
重點關注的是,我們調用了HttpSession的invalidate()方法,這樣我們就銷毀了該session。
我們是如何得知該方法的呢?我們可以充分利用IDE的自動補足功能,然后查看每一個方法的javadoc:
可以發現invalidate()方法就是用來使此session無效,并解除綁定到它的任何對象。
大家可以自行驗證一下,經過這樣改造后,上述問題得到解決。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。