TTP 是一種"無狀態(tài)"協(xié)議,這意味著每次客戶端檢索網(wǎng)頁時(shí),客戶端打開一個(gè)單獨(dú)的連接到 Web 服務(wù)器,服務(wù)器會自動不保留之前客戶端請求的任何記錄。
但是仍然有以下三種方式來維持 Web 客戶端和 Web 服務(wù)器之間的 session 會話:
Cookies
一個(gè) Web 服務(wù)器可以分配一個(gè)唯一的 session 會話 ID 作為每個(gè) Web 客戶端的 cookie,對于客戶端的后續(xù)請求可以使用接收到的 cookie 來識別。
這可能不是一個(gè)有效的方法,因?yàn)楹芏酁g覽器不支持 cookie,所以我們建議不要使用這種方式來維持 session 會話。
隱藏的表單字段
一個(gè) Web 服務(wù)器可以發(fā)送一個(gè)隱藏的 HTML 表單字段,以及一個(gè)唯一的 session 會話 ID,如下所示:
<input type="hidden" name="sessionid" value="12345">
該條目意味著,當(dāng)表單被提交時(shí),指定的名稱和值會被自動包含在 GET 或 POST 數(shù)據(jù)中。每次當(dāng) Web 瀏覽器發(fā)送回請求時(shí),session_id 值可以用于保持不同的 Web 瀏覽器的跟蹤。
這可能是一種保持 session 會話跟蹤的有效方式,但是點(diǎn)擊常規(guī)的超文本鏈接(<A HREF...>)不會導(dǎo)致表單提交,因此隱藏的表單字段也不支持常規(guī)的 session 會話跟蹤。
URL 重寫
您可以在每個(gè) URL 末尾追加一些額外的數(shù)據(jù)來標(biāo)識 session 會話,服務(wù)器會把該 session 會話標(biāo)識符與已存儲的有關(guān) session 會話的數(shù)據(jù)相關(guān)聯(lián)。
例如,http://w3cschool.cn/file.htm;sessionid=12345,session 會話標(biāo)識符被附加為 sessionid=12345,標(biāo)識符可被 Web 服務(wù)器訪問以識別客戶端。
URL 重寫是一種更好的維持 session 會話的方式,它在瀏覽器不支持 cookie 時(shí)能夠很好地工作,但是它的缺點(diǎn)是會動態(tài)生成每個(gè) URL 來為頁面分配一個(gè) session 會話 ID,即使是在很簡單的靜態(tài) HTML 頁面中也會如此。
HttpSession 對象
除了上述的三種方式,Servlet 還提供了 HttpSession 接口,該接口提供了一種跨多個(gè)頁面請求或訪問網(wǎng)站時(shí)識別用戶以及存儲有關(guān)用戶信息的方式。
Servlet 容器使用這個(gè)接口來創(chuàng)建一個(gè) HTTP 客戶端和 HTTP 服務(wù)器之間的 session 會話。會話持續(xù)一個(gè)指定的時(shí)間段,跨多個(gè)連接或頁面請求。
您會通過調(diào)用 HttpServletRequest 的公共方法 getSession() 來獲取 HttpSession 對象,如下所示:
HttpSession session = request.getSession();
你需要在向客戶端發(fā)送任何文檔內(nèi)容之前調(diào)用 request.getSession()。下面總結(jié)了 HttpSession 對象中可用的幾個(gè)重要的方法:
Session 跟蹤實(shí)例
本實(shí)例說明了如何使用 HttpSession 對象獲取 session 會話創(chuàng)建時(shí)間和最后訪問時(shí)間。如果不存在 session 會話,我們將通過請求創(chuàng)建一個(gè)新的 session 會話。
// 導(dǎo)入必需的 java 庫 import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import java.util.*; // 擴(kuò)展 HttpServlet 類 public class SessionTrack extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 如果不存在 session 會話,則創(chuàng)建一個(gè) session 對象 HttpSession session = request.getSession(true); // 獲取 session 創(chuàng)建時(shí)間 Date createTime = new Date(session.getCreationTime()); // 獲取該網(wǎng)頁的最后一次訪問時(shí)間 Date lastAccessTime = new Date(session.getLastAccessedTime()); String title = "歡迎回到我的網(wǎng)站"; Integer visitCount = new Integer(0); String visitCountKey = new String("visitCount"); String userIDKey = new String("userID"); String userID = new String("ABCD"); // 檢查網(wǎng)頁上是否有新的訪問者 if (session.isNew()){ title = "歡迎來到我的網(wǎng)站"; session.setAttribute(userIDKey, userID); } else { visitCount = (Integer)session.getAttribute(visitCountKey); visitCount = visitCount + 1; userID = (String)session.getAttribute(userIDKey); } session.setAttribute(visitCountKey, visitCount); // 設(shè)置響應(yīng)內(nèi)容類型 response.setContentType("text/html"); PrintWriter out = response.getWriter(); String docType = "<!doctype html public \"-//w3c//dtd html 4.0 " + "transitional//en\">\n"; out.println(docType + "<html>\n" + "<head><title>" + title + "</title></head>\n" + "<body bgcolor=\"#f0f0f0\">\n" + "<h1 align=\"center\">" + title + "</h1>\n" + "<h2 align=\"center\">Session 信息</h2>\n" + "<table border=\"1\" align=\"center\">\n" + "<tr bgcolor=\"#949494\">\n" + " <th>Session 信息</th><th>值</th></tr>\n" + "<tr>\n" + " <td>id</td>\n" + " <td>" + session.getId() + "</td></tr>\n" + "<tr>\n" + " <td>Creation Time</td>\n" + " <td>" + createTime + " </td></tr>\n" + "<tr>\n" + " <td>Time of Last Access</td>\n" + " <td>" + lastAccessTime + " </td></tr>\n" + "<tr>\n" + " <td>User ID</td>\n" + " <td>" + userID + " </td></tr>\n" + "<tr>\n" + " <td>Number of visits</td>\n" + " <td>" + visitCount + "</td></tr>\n" + "</table>\n" + "</body></html>"); } }
編譯上面的 Servlet SessionTrack,并在 web.xml 文件中創(chuàng)建適當(dāng)?shù)臈l目。在瀏覽器地址欄輸入 http://localhost:8080/SessionTrack,當(dāng)您第一次運(yùn)行時(shí)將顯示如下結(jié)果:
Session 信息
Session 信息值id0AE3EC93FF44E3C525B4351B77ABB2D5Creation TimeTue Jun 08 17:26:40 GMT+04:00 2014Time of Last AccessTue Jun 08 17:26:40 GMT+04:00 2014User IDABCDNumber of visits0
再次嘗試運(yùn)行相同的 Servlet,它將顯示如下結(jié)果:
Session 信息
Session 信息值id0AE3EC93FF44E3C525B4351B77ABB2D5Creation TimeTue Jun 08 17:26:40 GMT+04:00 2014Time of Last AccessTue Jun 08 17:26:40 GMT+04:00 2014User IDABCDNumber of visits1
刪除 Session 會話數(shù)據(jù)
當(dāng)您完成了一個(gè)用戶的 session 會話數(shù)據(jù),您有以下幾種選擇:
<session-config> <session-timeout>15</session-timeout> </session-config>
上面實(shí)例中的超時(shí)時(shí)間是以分鐘為單位,將覆蓋 Tomcat 中默認(rèn)的 30 分鐘超時(shí)時(shí)間。
在一個(gè) Servlet 中的 getMaxInactiveInterval() 方法會返回 session 會話的超時(shí)時(shí)間,以秒為單位。所以,如果在 web.xml 中配置 session 會話超時(shí)時(shí)間為 15 分鐘,那么 getMaxInactiveInterval() 會返回 900。
Http協(xié)議是一種無狀態(tài)協(xié)議所以當(dāng)服務(wù)端需要記錄用戶的狀態(tài)時(shí),需要某種機(jī)制用于識別用戶,這個(gè)機(jī)制就是Session。服務(wù)器通過和用戶約定每一個(gè)請求攜帶一個(gè)id信息,用于統(tǒng)一用戶的請求有了管理,并且區(qū)分不同用戶?;趕ession方案,為讓用戶請求都攜帶同一個(gè)id,并且不妨礙用戶體驗(yàn)的情況下,選擇cookie作為載體是一個(gè)不錯(cuò)的選擇,用戶第一次訪問服務(wù)器的時(shí)候,沒有攜帶id,服務(wù)器端會生成sessionid:session鍵值對,并且發(fā)送sessionid給客戶端添加到cookie中。然后該用戶在之后的訪問中,每一次請求都會將sessionid放到cookie中,使得服務(wù)端可以很容易識別用戶。
但是有時(shí)候用戶為了保護(hù)個(gè)人信息或者安全考慮會禁用cookie,這時(shí)候cookie就無法使用。因此有時(shí)候服務(wù)還支持用url重寫來實(shí)現(xiàn),比如:
http://www.baidu.com;jessionid=xxx
URL重寫原本是為了兼容禁用cookie的瀏覽器而設(shè)計(jì)的,但也容易被黑客利用。黑客只需訪問一 次系統(tǒng),將系統(tǒng)生成的sessionId提取并拼湊在URL上,然后將該URL發(fā)給一些取得信任的用戶。只要 用戶在session有效期內(nèi)通過此URL進(jìn)行登錄,該sessionId就會綁定到用戶的身份,黑客便可以輕松享 有同樣的會話狀態(tài),完全不需要用戶名和密碼,這就是典型的會話固定攻擊。
sessionManagement是一個(gè)會話管理的配置器,其中,防御會話固定攻擊的策略有四種:
http.sessionManagement().sessionFixation().changeSessionId();
除了防御會話固定攻擊,還可以通過SpringSecurity配置會話過期策略,比如會話過期跳轉(zhuǎn)到某個(gè)URL。在Springboot應(yīng)用中有兩種會話超時(shí)設(shè)置的方式,當(dāng)會話超時(shí)之后用戶需要重寫登錄才可以訪問應(yīng)用:
方式1是springboot應(yīng)用自帶的session超時(shí)設(shè)置,方式2是使用Spring Session之后。提供的session超時(shí)配置,方式2優(yōu)先級高。
在Spring Boot中Session超時(shí)最短的時(shí)間是一分鐘,當(dāng)你的設(shè)置小于一分鐘的時(shí)候,默認(rèn)為一分鐘默認(rèn)超時(shí)時(shí)長是30分鐘
默認(rèn)情況下session失效以后會跳轉(zhuǎn)到認(rèn)證頁面,我們可以自定義session失效后,響應(yīng)結(jié)果,有以下兩種方式。
invalidSessionUrl作用是session失效后跳轉(zhuǎn)的url,配置如下,在安全配置中心的 configure(HttpSecurity http)方法中添加代碼如下:
<!--suppress ALL-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>springboot葵花寶典登錄頁面</title>
<!-- Tell the browser to be responsive to screen width -->
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>springboot葵花寶典登錄頁面</h1>
<h2>session會話失效</h2>
</body>
</html>
@RequestMapping("/invalidSession")
public String invalidSession() {
return "invalidSession"; // classpath: /templates/login.html
}
http.sessionManagement().invalidSessionUrl("/invalidSession")
具體配置如圖
注意要以上路徑需要配置permitAll()權(quán)限,即無需授權(quán)即可訪問
啟動項(xiàng)目登錄后,再次登錄,結(jié)果如下
session失敗后的策略,配置如下:
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setMaxAge(0);
String contextPath = request.getContextPath();
String c= contextPath.length() > 0 ? contextPath : "/";
cookie.setPath(c);
response.addCookie(cookie);
// 當(dāng)認(rèn)證失敗后,響應(yīng) JSON 數(shù)據(jù)給前端
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString("策略失效"));
}
}
@Configuration
public class Myconfig {
@Bean
@ConditionalOnMissingBean(InvalidSessionStrategy.class)
public CustomInvalidSessionStrategy customInvalidSessionStrategy(){
return new CustomInvalidSessionStrategy();
}
}
在LearnSrpingSecurity的configure(HttpSecurity http)添加配置,代碼如下
啟動項(xiàng)目登錄后,再次登錄,結(jié)果如下
如果您覺得本文不錯(cuò),歡迎關(guān)注,點(diǎn)贊,收藏支持,您的關(guān)注是我堅(jiān)持的動力!
公眾號 springboot葵花寶典 主要分享JAVA技術(shù),主要包含SpringBoot、SpingCloud、Docker、中間件等技術(shù)
原創(chuàng)不易,轉(zhuǎn)載請注明出處,感謝支持!如果本文對您有用,歡迎轉(zhuǎn)發(fā)分享!
于Cookie和Session的功能與工作原理,在這里我就不再敘述了,大家想要了解可以看一下我的上一篇博客,講的還是很細(xì)致的。
但是之前講的Session是運(yùn)行在一臺服務(wù)器上的,所有的訪問都會到達(dá)我們的唯一服務(wù)器上,這樣我們可以根據(jù)客戶端傳來的sessionID,來獲取session,或在對應(yīng)Session不存在的情況下(session 生命周期到了/用戶第一次登錄),創(chuàng)建一個(gè)新的Session;但是,如果我們在集群環(huán)境下,假設(shè)我們有兩臺服務(wù)器A,B,用戶的請求會由Nginx服務(wù)器進(jìn)行轉(zhuǎn)發(fā)(別的方案也是同理),用戶登錄時(shí),Nginx將請求轉(zhuǎn)發(fā)至服務(wù)器A上,A創(chuàng)建了新的session,并將SessionID返回給客戶端,用戶在瀏覽其他頁面時(shí),客戶端驗(yàn)證登錄狀態(tài),Nginx將請求轉(zhuǎn)發(fā)至服務(wù)器B,由于B上并沒有對應(yīng)客戶端發(fā)來sessionId的session,所以會重新創(chuàng)建一個(gè)新的session,并且再將這個(gè)新的sessionID返回給客戶端,這樣,我們可以想象一下,用戶每一次操作都有1/2的概率進(jìn)行再次的登錄,這樣不僅對用戶體驗(yàn)特別差,還會讓服務(wù)器上的session激增,加大服務(wù)器的運(yùn)行壓力。
為了解決集群環(huán)境下的seesion共享問題,共有4種解決方案:
1.粘性session
粘性session是指Ngnix每次都將同一用戶的所有請求轉(zhuǎn)發(fā)至同一臺服務(wù)器上,即將用戶與服務(wù)器綁定。
2.服務(wù)器session復(fù)制
即每次session發(fā)生變化時(shí),創(chuàng)建或者修改,就廣播給所有集群中的服務(wù)器,使所有的服務(wù)器上的session相同。
3.session共享
緩存session,使用redis, memcached。
4.session持久化
將session存儲至數(shù)據(jù)庫中,像操作數(shù)據(jù)一樣才做session。
其實(shí),最簡單的兩種方案,就是方案一和方案三,都不需要對session進(jìn)行任何操作,只需要將Nginx和Tomcat上的配置文件修改一下即可。由于我們做集群,訪問量一定是比較大的了,對于第一種方案,如果某臺服務(wù)器發(fā)生故障,此服務(wù)器上的所有用戶的session都會丟失,所以今天我們采用第三種解決方案。
感謝開源項(xiàng)目tomcat-redis-session-manager,感謝項(xiàng)目的發(fā)起者jcoleman。
此方案最簡單之處就在于我們無需修改項(xiàng)目,只需要修改Tomcat的context.xml配置文件即可,并且,redis服務(wù)器同樣可以做分布式。
接下來,我們來講解一下如何配置tomcat,首先,講解一下注意事項(xiàng),因?yàn)轫?xiàng)目的原因,并沒能用maven來管理依賴的包,而在我配置的時(shí)候,很正常的發(fā)生了包版本間的沖突,從redis中取出的序列化后的session無法轉(zhuǎn)換為HttpSession,所以,大家做的時(shí)候一定要注意版本間的兼容問題。
把我使用的版本給大家做個(gè)參考吧,tomcat-redis-session-manager-1.1.jar,jedis-2.1.0.jar,commons-pool-1.6.jar
這三個(gè)包需放入 tomcat的安裝目錄下的lib文件夾下,IDE自帶的tomcat可以在配置中查看。
需注意的是,當(dāng)redis的版本‘過高’時(shí),需要依賴commons-pool2.jar,(具體版本不寫)。
接下來,我們就講一下最核心的配置,配置context.xml文件(文件路徑workspace\.metadata\.me_tcat7\conf,此處僅是myeclipse編輯器的路徑)
[html] view plain copy
這個(gè)里的class路徑具體看tomcat-redis-session-manager里的路徑。
可能你會遇到重啟tomcat配置文件會被還原的問題,那么你還需要修改另一處的context.xml文件,workspace\Servers\MyEclipse Tomcat v7.0-config文件夾中的,修改內(nèi)容同上。
接下來,給大家看一下配置完成后的結(jié)果:
圖1為監(jiān)聽到的redis服務(wù)器的狀態(tài),可以看到我們將session序列化后以sessionID為key存入了redis中,圖2可以看到是客戶端header中的cookie信息(若有疑問可以查看我上一篇的博客),圖三是我在redis客戶端get key為sessionID的記錄,可以驗(yàn)證成功,我們配置是成功了的,因?yàn)槭褂玫淖约旱碾娔X,配置有限,并沒有開幾個(gè)tomcat來測試,不過上述還是很有說服力的(線上已經(jīng)測試過了)。
希望對你有幫助!
*請認(rèn)真填寫需求信息,我們會在24小時(shí)內(nèi)與您取得聯(lián)系。