老板的苦惱
假如你在繁華的街角開了一家店,每天客人絡繹不絕。
不過你作為老板卻有一些苦惱,你想知道自己的顧客上一次是什么時候來的?
在店里的時候買了什么商品,方便購物的時候進一步提升用戶體驗。
可是這些客人赤果果的來,無牽掛的走,店里一直沒有留下客人的信息,聰明的你會怎么解決這個問題呢?
輸入圖片說明
互聯網沒有記憶
我們常說互聯網沒有記憶。互聯網背后的 HTTP 協議也是如此,正因為它無狀態,所以足夠簡單,便于拓展,得以發展到今天這種局面。
同時也正是因為 HTTP 協議無狀態,所以對用戶訪問等缺乏識別記憶功能。
那怎么解決這個問題呢?
目前有兩張最主流的方式:cookie 和 session。
cookie
Cookie 是客戶端保存用戶信息的一種機制,用來記錄用戶的一些信息,也是實現 Session 的一種方式。
這個就好比我們把客戶上次到店里的時間放在用戶的口袋里,下次他們來的時候,我們拿出來看一下,就知道客戶上次是什么時候來的了。
當然這些信息用戶自己是可以修改的,比如各種瀏覽器的 cookies 可以被清空。
這讓我想起來以前讀的一個故事:
剛在路邊攤準備買點小吃。我說:老板我經常來買,給我便宜點吧。老板說:我今天第一天擺攤。
鐵鍋燉自己
信息都放在用戶的口袋里雖然方便,但是服務端也要記一些必要的信息,不然被忽悠了都不知道。
session
Session 是在服務端保存的一個數據結構,用來跟蹤用戶的狀態,這個數據可以保存在集群、數據庫、文件中;
這個就類似于店里來客人了,服務員留心看一下,知道用戶購物車里放了什么商品,是否需要幫助等等。
Cookie 操作
為了讓大家直觀的感受到 cookie 的使用,我們來看一下 CRUD 的例子。
為了簡單,此處使用 servlet 進行演示。
Cookie
說明
Cookie是瀏覽器保存信息的一種方式,可以理解為一個文件,保存到客戶端了啊,服務器可以通過響應瀏覽器的set-cookie的標頭,得到Cookie的信息。
你可以給這個文件設置一個期限,這個期限呢,不會因為瀏覽器的關閉而消失。
添加
我們可以新建一個 cookie 返回給 resp。
package com.github.houbb.simple.servlet;
import JAVAx.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author binbin.hou
* @since 0.0.2
*/
@WebServlet("/cookie/add")
public class CookieAddServlet extends HttpServlet {
private static final long serialVersionUID = 491287664925808862L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
//1. 創建 cookie 信息
Cookie cookie = new Cookie("age", "10");
//30min
cookie.setMaxAge(30 * 60);
//2. 返回給客戶端,用于客戶端保存
resp.addCookie(cookie);
//3. 頁面輸出
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
// 后端會根據頁面是否禁用 cookie,選擇是否將 sessionId 放在 url 后面
String url = resp.encodeURL("/cookie/get");
out.println("<a href='"+url+"'>獲取 cookie</a>");
}
}
獲取
獲取 cookie 也比較簡單,直接通過 req.getCookies() 就可以獲取到整個 cookie 列表。
package com.github.houbb.simple.servlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author binbin.hou
* @since 0.0.2
*/
@WebServlet("/cookie/get")
public class CookieGetServlet extends HttpServlet {
private static final long serialVersionUID = 491287664925808862L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 實際的邏輯是在這里
PrintWriter out = resp.getWriter();
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for(Cookie cookie : cookies) {
out.println(cookie.getName()+"="+cookie.getValue()+"");
}
}
}
}
刪除
cookie 是非法直接刪除的,一般都是首先獲取,然后設置 maxAge 為 0。
package com.github.houbb.simple.servlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 清空
* @author binbin.hou
* @since 0.0.2
*/
@WebServlet("/cookie/clear")
public class CookieClearServlet extends HttpServlet {
private static final long serialVersionUID = 491287664925808862L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
for(Cookie cookie : req.getCookies()) {
// 立刻失效
cookie.setMaxAge(0);
cookie.setPath("/");
resp.addCookie(cookie);
}
resp.setContentType("text/html;charset=utf-8");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.println("<a href='/cookie/add'>添加 cookie 信息</a>");
}
}
session
session
說明
session的實現原理是建立在給瀏覽器回寫cookie,并且是以 JSESSIONID 為鍵,但是這個cookie是沒有時間的,也就是說,當你關閉瀏覽器時,代表一個會話結束了,也就是說你的session會被刪除,當你再次訪問服務器的時候,服務器會為你重新創建一個session。
添加
添加 session 屬性的方式也比較簡單,直接使用 req.getSession().setAttribute("name", "session"); 即可。
package com.github.houbb.simple.servlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author binbin.hou
* @since 0.0.2
*/
@WebServlet("/session/add")
public class SessionAddServlet extends HttpServlet {
private static final long serialVersionUID = 491287664925808862L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 只有在 getSession 的時候,才會設置對應的 JSESSIONID
req.getSession().setAttribute("name", "session");
resp.setContentType("text/html;charset=utf-8");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
// 后端會根據頁面是否禁用 cookie,選擇是否將 sessionId 放在 url 后面
String url = resp.encodeURL("/session/get");
out.println("<a href='"+url+"'>獲取 session 信息</a>");
}
}
獲取
我們可以通過 httpSession.getAttributeNames() 獲取到所有的 session 屬性。
也可以通過 req.getSession().getId() 得到我們的 JSESSIONID 屬性。
package com.github.houbb.simple.servlet;
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;
import java.io.PrintWriter;
import java.util.Enumeration;
/**
* @author binbin.hou
* @since 0.0.2
*/
@WebServlet("/session/get")
public class SessionGetServlet extends HttpServlet {
private static final long serialVersionUID = 491287664925808862L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 實際的邏輯是在這里
PrintWriter out = resp.getWriter();
String jsessionId = req.getSession().getId();
out.println("jsessionId: " + jsessionId);
HttpSession httpSession = req.getSession();
Enumeration attrs = httpSession.getAttributeNames();
while (attrs.hasMoreElements()) {
String key = (String) attrs.nextElement();
Object value = httpSession.getAttribute(key);
out.println("key: " + key +"; value: " + value);
}
}
}
清空
清空 session 的操作非常簡單。
直接通過 httpSession.removeAttribute(key) 即可操作。
package com.github.houbb.simple.servlet;
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;
import java.io.PrintWriter;
import java.util.Enumeration;
/**
* @author binbin.hou
* @since 0.0.2
*/
@WebServlet("/session/clear")
public class SessionClearServlet extends HttpServlet {
private static final long serialVersionUID = 491287664925808862L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=utf-8");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
HttpSession httpSession = req.getSession();
Enumeration attrs = httpSession.getAttributeNames();
while (attrs.hasMoreElements()) {
String key = (String) attrs.nextElement();
httpSession.removeAttribute(key);
out.println("清空 key: " + key);
}
}
}
上面的代碼,為了便于大家學習,已經全部開源:
https://gitee.com/houbinbin/simple-servlet
session 的一些細節
相信很多小伙伴讀到這里依然是意猶未盡的。
接下來我們一起考慮幾個細節問題。
會話機制
session 創建于服務器端,保存于服務器,維護于服務器端,每創建一個新的Session,服務器端都會分配一個唯一的ID,并且把這個ID保存到客戶端的Cookie中,保存形式是以 JSESSIONID 來保存的。
一點細節
不要在意
通過HttpServletRequest.getSession 進行獲得HttpSession對象,通過setAttribute()給會話賦值,可以通過invalidate()將其失效。
- 每一個HttpSession有一個唯一的標識SessionID,只要同一次打開的瀏覽器通過request獲取到session都是同一個。
- WEB容器默認的是用Cookie機制保存SessionID到客戶端,并將此Cookie設置為關閉瀏覽器失效,Cookie名稱為:JSESSIONID
- 每次請求通過讀取Cookie中的SessionID獲取相對應的Session會話
- HttpSession的數據保存在服務器端,所以不要保存數據量耗資源很大的數據資源,必要時可以將屬性移除或者設置為失效
- HttpSession可以通過 setMaxInactiveInterval() 設置失效時間(秒)或者在 web.xml 中配置
<session-config>
<!--單位:分鐘-->
<session-timeout>30</session-timeout>
</session-config>
session 的創建時機
一個常見的誤解是以為session在有客戶端訪問時就被創建,然而事實是直到某server端程序調用 HttpServletRequest.getSession(true) 這樣的語句時才被創建。
Session 何時被刪除
綜合前面的討論,session 在下列情況下被刪除
- 程序調用 HttpSession.invalidate();
- 距離上一次收到客戶端發送的 session id時 間間隔超過了session的超時設置;
- 服務器進程被停止(非持久session)
JSESSIONID 的創建與獲取
我們在 session 創建的時候,也就是第一次調用 HttpServletRequest.getSession(true) 時,會給客戶端分配一個 JSESSIONID 用于唯一標識這個用戶。
這個信息會被寫回到客戶端的 cookie 中,并且后續的請求都會攜帶。
比如我測試時的 JSESSIONID:
Cookie: JSESSIONID=8AE65FE9AEB0AA6053FADF9ED7AEE544
可以發現實際上 JSESSIONID 是非常依賴客戶端 cookie 的,那么問題來了,如果用戶禁用了 cookie 怎么辦?
客戶端禁用 cookie
cookie 是用戶自己的口袋,如果用戶有一天把口袋全部封死也是有可能的。
如果客戶端禁用了 cookie,一般有兩種解決方案。
隱藏域
我們將 JSESSIONID 的值傳入到頁面中,放入一個隱藏的 input 框中,每次請求帶上這個參數。
<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="8AE65FE9AEB0AA6053FADF9ED7AEE544"/>
<input type="text">
</form>
后端通過 req.getParameter("jsessionid") 的方式獲取到這個 jsessionid 信息。
URL 重寫
URL地址重寫的原理是將該用戶Session的id信息重寫到URL地址中。服務器能夠解析重寫后的URL獲取Session的id。
這樣即使客戶端不支持Cookie,也可以使用Session來記錄用戶狀態。
encodeURL() 方法在使用時,會首先判斷Session是否啟用,如果未啟用,直接返回url。
然后判斷客戶端是否啟用Cookie,如果未啟用,則將參數url中加入SessionID信息,然后返回修改的URL;如果啟用,直接返回參數url。
就像老馬前面代碼寫的一樣:
// 后端會根據頁面是否禁用 cookie,選擇是否將 sessionId 放在 url 后面
String url = resp.encodeURL("/session/get");
out.println("<a href='"+url+"'>獲取 session 信息</a>");
如果我們禁用 cookie,鏈接的地址就會變成:
http://localhost:8080/session/get;jsessionid=3E2EEB9840F2566EDB3085BA392AE6CB
;jsessionid=3E2EEB9840F2566EDB3085BA392AE6CB 這個是 encodeURL 自己加上去的,這樣我們就可以像原來一樣處理 session id 了。
連鎖店的機遇與挑戰
當目前為止,你作為一家店的老板已經可以輕松的掌握客戶的信息了。
哪怕用戶把自己的口袋封死。
隨著你的生意越來越好,你的店從一家門面,變成了多家連鎖店。
新的問題又來了,一個用戶去了其中的一家,當到另外一家店面的時候,如何得到用戶對應的信息呢?
連鎖店
這個就涉及到分布式系統的 session 共享問題。
其實解決問題的思路也是從兩個角度出發:
(1)用戶的角度
在用戶的口袋中放著驗證信息。
不過需要考慮信息被惡意修改等,這方面 JWT 做的比較優秀。
可以參考:
分布式系統 session 共享解決方案 JWT 實戰筆記
(2)服務者的角度
我們作為連鎖店,只需要把各個店里的商戶信息共享即可。
至于共享到哪里,可以是 redis 也可以是數據庫。
這方面 spring session 設計的比較優秀,可以參考:
springboot整合redis實現分布式session
spring session 結合攔截器實戰
小結
這一節老馬和大家一起學習了 web 會話機制中的 session 和 cookie 機制。
我們知道問題的源頭,自然就理解了一個技術產生需要解決的問題。
最后拓展到了分布式系統中的 session 共享問題,后續我們將重點介紹下 spring sesison 和 jwt,感興趣的小伙伴可以關注一波不迷路。
希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。
我是老馬,期待與你的下次相遇。