整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          結合cookie和session,把登陸流程梳理的清清楚楚

          文為小編原創文章,首發于Java識堂,一個高原創,高收藏,有干貨的微信公眾號,一起成長,一起進步,歡迎關注

          前言

          原來分享過一篇文章,Java自定義注解及應用,當時為了能突出重點,直接在url中傳了用戶的所屬角色,并寫了一般的做法。加上最近看了一些人的簡歷,發現神奇的相似,都有類似商城的項目,為了不至于問些特別Low的問題,便總結了一下登錄這個模塊所涉及的東西

          單機Session

          Http協議使用的是無狀態連接,這樣會造成什么問題呢?看如下Demo

          測試



          HttpServletRequest對象代表客戶端的請求,當客戶端通過HTTP協議訪問服務器時,HTTP請求頭中的所有信息都封裝在這個對象中,當在一個請求中時HttpServletRequest中的信息可以共享,而在不同的請求中HttpServletRequest并不能共享,這樣就會造成用戶確實進行過登錄操作,但是跳到購物車頁面時發現并沒有東西,因為應用并不知道訪問這個頁面的用戶是誰

          我們可以用一個HttpSession對象保存跨多個請求的會話狀態,上面的例子就是保存用戶名,看下圖理解為什么HttpSession可以跨請求保存狀態




          對客戶的第一個請求,容器會生成一個唯一的會話ID,并通過響應把它返回給客戶。客戶再在以后的每一個請求中發回這個會話ID。容器看到ID后,就會找到匹配的會話,并把這個會話與請求關聯

          將上面代碼改成如下,再測試



          果然能保存會話狀態了,客戶和容器如何交換會話ID信息呢?其實是通過cookie實現的


          看上面能保存會話的代碼,我們并沒有對cookie進行操作啊,其實是容器幾乎會做cookie的所有工作,從最開始的Servlet開始講這些操作是如何實現的,先看一下Servlet執行過程

          1. 用戶點擊頁面發送請求->Web服務器應用(如Apache)->Web容器應用(如tomcat)
          2. 容器創建兩個對象HttpServletRequest和HttpServletResponse
          3. 根據URL找到servlet,并為請求創建或分配一個線程,將請求和響應對象傳遞給這個servlet線程
          4. 容器調用Servlet的service()方法,根據請求的不同類型,service()方法會調用doGet()和doPost()方法,假如請求是HTTP GET請求
          5. doGet()方法生成動態頁面,并把這個對象塞到響應對象里。容器有響應對象的一個引用
          6. 線程結束,容器把響應對象裝換為一個HTTP請求,把它發回給客戶,然后刪除請求和響應對象

          容器使用部署描述文件把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);
           }
          }
          

          在響應中發送一個會話cookie

          HttpSession session = req.getSession();
          


          我們只需要寫上述一行代碼即可,來看看容器幫我們做了哪些事情

          1. 建立一個新的HttpSession對象
          2. 生成唯一的會話ID
          3. 建立新的Cookie對象
          4. 把會話Id放到cookie中
          5. 在響應中設置cookie

          從請求得到會話ID

          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的過期時間,以保證用戶登錄后長期不操作需要重新登錄

          分布式Session

          當整個服務是分布式的該怎么處理呢?用戶在服務器A上登錄,結果在服務器B上查看購物車信息,因為在A上登錄,HttpSession存在A服務器上,當訪問B服務器上的購物車信息因為獲取不到用戶登錄的HttpSession,就會認為用戶沒有登錄,這種情況該怎么處理呢?

          實現分布式Session有多種方式,這里就介紹一下用Redis實現分布式Session,其實Spring Session項目就使用Redis實現Session共享的

          理解了單機Session,分布式Session也不難理解,主要步驟如下

          1. 用戶登錄以后,先生成類似于sessionId的唯一標識,我們把它叫token
          2. new一個cookie,將token寫到cookie當中傳遞到客戶端,并將以key=token,value=用戶信息的hash放到redis中,當然cookie和這個hash都可以設置過期時間
          3. 客戶端在隨后的訪問中服務器從cookie中拿到這個token,根據這個token去Redis中取到用戶信息

          當用戶登出時只要刪除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閱讀,頭條網頁展示代碼會有問題。

          目錄

          1. 介紹
          2. 原有的用戶退出功能
          3. 思路
          4. 修改include.jsp
          5. 添加處理用戶退出請求的動作
          6. 總結

          介紹

          前面的文章我們實現了租房網平臺的用戶注冊、用戶登錄、會話跟蹤等功能,本篇文章繼續實現用戶的登出/退出功能。

          原有的用戶退出功能

          實際上,我們之前的版本中,只要用戶登錄之后,在每個頁面當中已經有退出按鈕,如下圖:


          其對應的代碼在我們的 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>

          可以看到,這個退出按鈕就是一個普通的鏈接,直接返回到登錄頁面而已。那這樣有什么不好的地方呢?我們做這樣一個實驗:

          1. 先登錄到租房網平臺;
          2. 登錄后可以打開其他頁面,我這里假設打開某個房源詳情頁面,其URL是:http://localhost:8080/house-renter/house-details.action?houseId=1
          3. 然后點擊退出,會跳轉到登錄頁面;
          4. 此時我直接在瀏覽器的地址欄中重新輸入上述URL,可以新開一個瀏覽器標簽頁,甚至另外打開一個瀏覽器進程(不過必須是同一款瀏覽器,我這里是谷歌瀏覽器),敲回車;
          5. 結果是我跳過了登錄步驟,直接打開了該房源詳情頁面。

          結論就是這樣簡單的登錄功能很不安全。

          究其原因,其實是我們采用了session進行會話跟蹤,只要session不過期,Servlet容器(我們這里是Tomcat)中的該session對象就還有效,綁定到該session對象中的數據也就還有效。

          不過,因為HTTP是基于TCP的,所以不同的TCP連接肯定會產生不同的session,大家有興趣的話可以自行測試一下TCP連接和session之間的關系。

          思路

          所以,我們的用戶退出功能必須是這樣的:

          1. 用戶點擊退出按鈕;
          2. Servlet容器必須感知到用戶的退出;
          3. Servlet容器銷毀該用戶的session。

          修改include.jsp

          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無效,并解除綁定到它的任何對象。

          總結

          大家可以自行驗證一下,經過這樣改造后,上述問題得到解決。

          • 調用HttpSession的invalidate()方法可以使會話無效。

          主站蜘蛛池模板: 波多野结衣在线观看一区二区三区| 亚洲国产高清在线一区二区三区| 91成人爽a毛片一区二区| 亚洲丰满熟女一区二区哦| 国产精品一区在线麻豆| 无码人妻精品一区二区三区9厂 | 一区二区高清视频在线观看| 日韩精品无码久久一区二区三| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 中文字幕av日韩精品一区二区| 国产免费私拍一区二区三区| 国产精品无码AV一区二区三区| 中文字幕无线码一区2020青青| 精品免费国产一区二区| 国产精品无圣光一区二区| 日韩精品一区二区三区中文3d| 国产成人精品一区二区三区免费| 亚洲熟女乱色一区二区三区| 一区二区中文字幕在线观看| 成人免费观看一区二区| 亚洲一区精品伊人久久伊人| 麻豆国产一区二区在线观看| 亚洲毛片αv无线播放一区| 成人乱码一区二区三区av| 性色av无码免费一区二区三区 | 一区二区免费电影| 午夜精品一区二区三区在线视| 国产小仙女视频一区二区三区| 亚洲av无码片区一区二区三区| 日韩毛片基地一区二区三区| 消息称老熟妇乱视频一区二区| 国产av熟女一区二区三区| 真实国产乱子伦精品一区二区三区 | 女人18毛片a级毛片一区二区| 亚洲性色精品一区二区在线| 亚洲熟女综合一区二区三区| 丰满人妻一区二区三区视频 | 日美欧韩一区二去三区| 人妻无码一区二区视频| 精品日产一区二区三区手机| 国产一区二区电影在线观看|