cookie和session在java web開發中扮演了十分重要的做用,本篇文章對其中的重要知識點作一些探究和總結。(轉發自https://www.cnblogs.com/roy-blog/p/8250519.html)html
隨意打開一個網址,用火狐的調試工具,隨意選取一個連接,查看其請求頭。你就會看到cookie的信息。以下圖所示。java
如上圖所示,咱們訪問了新浪網,經過火狐瀏覽器的調試窗口能夠看到cookie存在於請求頭也就是httprequest中,而且是以鍵值對(數組)的形式存在。ios
只要有請求,就會在請求頭攜帶一個cookie的數組(鍵值對)。cookie是瀏覽器層面的東西。web
事實上,在java的servlet體系裏,咱們能夠經過以下方式獲取cookie.redis
HttpServletRequest req=ServletActionContext.getRequest(); Cookie[] cookies=req.getCookies(); for(int i=0;i<cookies.length;i++){ Cookie cookie=cookies[i]; System.out.println("name:"+cookie.getName()+",domain"+cookie.getDomain()+",value:"+cookie.getValue()+",maxage:"+cookie.getMaxAge()); }
能夠看到,在servlet體系中,把cookie做爲一個屬性放到了HttpRequest對象裏面。經過getCookies()方法獲得一個cookie數組。數據庫
咱們在一個action中加入上述代碼,而且訪問這個action,則能夠看到控制檯打印出以下信息。小程序
servlet對cookie進行了封裝,cookie對象有幾個屬性,如name,domain,value,maxage等,具體的意義能夠參考servlet的api文檔。微信小程序
以上的請求的cookie是我首次訪問某一個網站的連接時候產生的。能夠看到cookie數組中只有一個元素。這邊先注意一下,後續會有更進一步的說明。api
說了獲取cookie數組和cookie,咱們必定也想知道如何把咱們本身的一些信息放進cookie,其實很簡單。http的一次請求老是伴隨着一次響應,咱們就將cookie信息放入到響應中,傳遞給瀏覽器。在java下代碼是這樣寫的。數組
HttpServletResponse res=ServletActionContext.getResponse(); Cookie cookie=new Cookie("xdx", "i'm xdx"); res.addCookie(cookie);
能夠看到當咱們發起這個請求時,在響應頭有下列信息。
也就是經過此次請求,咱們把xdx=i'm xdx 這個cookie經過response放進了瀏覽器。
當咱們再次訪問該網站上的其餘頁面的時候,在請求頭都將帶有這個cookie。以下圖所示。
而假如咱們清除歷史記錄,包括cookie。
再次訪問該網站的某一個地址。剛纔咱們加進去的cookie就不存在了。
總結來講就是:servlet經過response將cookie放入到cookie數組中,這樣瀏覽器端就會擁有這一個cookie信息。瀏覽器會在之後的請求過程當中把這個cookie信息放在請求頭。
session咱們通常指的是HTTPSession,爲了理解它,咱們直接打開HttpSession的源碼來一看究竟。
/** * * Provides a way to identify a user across more than one page * request or visit to a Web site and to store information about that user. * * <p>The servlet container uses this interface to create a session * between an HTTP client and an HTTP server. The session persists * for a specified time period, across more than one connection or * page request from the user. A session usually corresponds to one * user, who may visit a site many times. The server can maintain a * session in many ways such as using cookies or rewriting URLs. * * <p>This interface allows servlets to * <ul> * <li>View and manipulate information about a session, such as * the session identifier, creation time, and last accessed time * <li>Bind objects to sessions, allowing user information to persist * across multiple user connections * </ul> * * <p>When an application stores an object in or removes an object from a * session, the session checks whether the object implements * {@link HttpSessionBindingListener}. If it does, * the servlet notifies the object that it has been bound to or unbound * from the session. Notifications are sent after the binding methods complete. * For session that are invalidated or expire, notifications are sent after * the session has been invalidated or expired. * * <p> When container migrates a session between VMs in a distributed container * setting, all session attributes implementing the {@link HttpSessionActivationListener} * interface are notified. * * <p>A servlet should be able to handle cases in which * the client does not choose to join a session, such as when cookies are * intentionally turned off. Until the client joins the session, * <code>isNew</code> returns <code>true</code>. If the client chooses * not to join * the session, <code>getSession</code> will return a different session * on each request, and <code>isNew</code> will always return * <code>true</code>. * * <p>Session information is scoped only to the current web application * (<code>ServletContext</code>), so information stored in one context * will not be directly visible in another. * * @author Various * * @see HttpSessionBindingListener * @see HttpSessionContext
簡單的翻譯一下:
--提供一個在多頁面請求切換的狀況下用於驗證、存儲用戶信息的手段。 --servlet容器使用Httpsession來建立鏈接客戶端和服務端的一個會話(session)。這個會話能持續一段指定的時間(也就是咱們常說的session過時時間),該會話能在多個請求之間共享。 --這個會話通常跟用戶信息關聯,因爲這個用戶可能屢次訪問網站,因此咱們把他們存儲在這個會話(也就是httpsession)裏。 --服務端一般是經過cookies或者rewriting URLs來保持一個session。
個人理解,session是一種持久的會話,它的存在主要是爲了克服http無狀態的特色,關於http無狀態,或者說沒有記憶,這裏很少闡述,涉及到計算機網絡的知識,簡單來講就是http一次請求對應一次響應,在這個過程當中會攜帶一些信息,但這些信息也僅僅在這個過程當中有效。當一個請求結束,咱們進入下一個請求的時候,上一個請求裏面的信息對當前的請求就沒什麼意義了,由於當前的請求根本不會知道上一個請求裏面所包含的信息。
那麼當咱們須要一些在各個請求都能公用的信息的時候,該怎麼辦呢?有不少辦法,能夠把信息存在數據庫,而後每次從數據庫去取出來,固然io存取會浪費不少時間,它僅僅針對大數據量。還有一種就是將這些信息存在內存當中。沒錯session其實就是這樣一種對象,他把項目當中一些經常使用的信息存在內存當中,這些經常使用的信息一般是跟用戶相關的,好比用戶名,用戶暱稱,用戶角色等。由於他們須要常常用到,因此把這些信息存在session中進行管理再好不過了。
在servlet體系裏,咱們能夠用以下代碼來獲取session。
HttpServletRequest request = ServletActionContext.getRequest(); HttpSession httpSession=request.getSession(); System.out.println(httpSession);
咱們來查閱HttpServletRequest的源碼,看看其getSession()方法。
/** * * Returns the current session associated with this request, * or if the request does not have a session, creates one. * * @return the <code>HttpSession</code> associated * with this request * * @see #getSession(boolean) * */ public HttpSession getSession();
它的解釋是返回當前與request關聯的session,若是這個請求不存在session,就新建一個。
咱們在兩個請求中加入上述代碼並運行,獲得以下結果。
能夠看到在整個項目內,這個session被共享着調用。
簡單點說,每個session對象都有一個sessionId,而在cookie數組中,又一個元素叫作JSESSIONID,服務器將session對象的sessionId,以名稱叫作JSESSIONID,值爲sessionId的形式存入cookie數組中,這樣cookie和session就發生了關聯。
上面的描述能夠用下圖來表示。
上述的過程能夠用相似以下的代碼來實現。
HttpServletRequest req=ServletActionContext.getRequest(); HttpServletResponse res=ServletActionContext.getResponse(); Cookie cookie=new Cookie("JSESSIONID", req.getSession().getId());
res.addCookie(cookie);
只不過咱們並不須要寫這個代碼,servlet自動幫咱們完成了如上的操做。
具體的過程是這樣的:
(1)當咱們首次在某個請求中經過調用request.getSession去獲取session的時候(這個調用不必定是顯式的,不少框架把session封裝成map等其餘的類型,名稱也不必定是session,可是本質都是在調用session),首先servlet經過getCookie的到本次請求的cookie信息,而後去尋找cookie數組中是否有否有一個名爲JSESSIONID的cookie,沒有的話就建立一個session,而且把sessionId作爲JSESSIONID這個cookie的值,而後調用addCookie()方法把該cookie放入cookie數組。
(2)若是上一步中從cookie數組中取到的cookie數組已經包含了JSESSIONID這個cookie,這時候我咱們取出JSESSIONID的值,而後去內存中的session(內存中有不少session)去尋找對應的sessionId爲JSESSIONID的值的session,若是找獲得的話,就使用這個session,找不到的話,就新建一個session,而且一樣的調用addCookie()方法覆蓋掉原來的JSESSIONID這個cookie的值。
上述的過程能夠用相似以下的代碼來表示。
HttpServletRequest req=ServletActionContext.getRequest();//具體獲取request的狀況可能有所不一樣 HttpSession session; Cookie JSESSIONID=null; Cookie[] cookies=req.getCookies(); for(int i=0;i<cookies.length;i++){ Cookie cookie=cookies[i]; if(cookie.getName().equals("JSESSIONID")){ JSESSIONID=cookie; } } if(JSESSIONID==null){ session= createSession();//建立一個session }else{ session=findSessionBySessionId(JSESSIONID.getValue());//經過sessionId獲取session if(session==null){ session= createSession();//建立一個session } HttpServletRequest req=ServletActionContext.getResponse();//具體狀況可能有所不一樣 Cookie cookie=new Cookie("JSESSIONID", session.getId()); res.addCookie(cookie);
咱們將瀏覽器緩存清除,這樣cookie中就沒有JSESSIONID了,而後咱們訪問一個action。以下。
這個問題包含着一些陷阱。由於不少時候當咱們清空瀏覽器之後,確實須要從新登陸系統才能夠操做,因此不少人天然而然認爲清空瀏覽器緩存(包含cookie)之後。session就會消失。
其實這種結論是錯誤的。要知道,session是存在於服務器的,你清除瀏覽器緩存,只是清除了cookie,跟session一點關係都沒有。那麼爲何咱們卻不能訪問網站,而須要從新登陸了呢?
通常的web項目會經過session來判斷用戶是否有登陸,經常使用的判斷語句是if(session.get("userId")==null。若是爲空,則表示須要從新登陸。這時候其實隱式地調用了getSession()方法,這就回到了上一步咱們所講的session獲取的具體步驟了。
由於清空了瀏覽器緩存,這時候cookie數組中一定不會有JSESSIONID這個cookie,因此必須得新建一個session,用新的sessionId來給JSESSIONID這個cookie賦值。因爲是新建的session,session中一定沒有userId這樣的屬性值,因此判斷結果天然爲空,因此須要從新登陸。此次賦值之後,下一次再請求該網站的時候,因爲cookie數組中已經有了JSESSIONID這個cookie,而且能經過該JSESSIONID的值找到相應的session,因此就不須要再從新登陸了。
第一種:當項目正常運行,咱們清空瀏覽器,cookie和session會發生什麼變化。
由於清空了瀏覽器,因此不會存在JSESSIONID這個cookie,servlet沒法找到對應的session,因此他會新建一個session,而後在本次請求的響應中將sessionId傳入cookie中。
下一次請求,cookie數組中就帶有JSESSIONID這個cookie了,servlet就能夠找到對應的session,沿用便可。
第二種:瀏覽器正常,項目重啓(可中止tomcat來模擬這種狀況),cookie和session會發生什麼變化。
由於項目重啓,內存中的一切session都消失了,雖然訪問一個action,請求頭中有JSESSIONID這個cookie,可是經過它的值(sessionId)並不能找到session(由於根本沒有任何session),因此仍是得從新建立一個session,而且這個session的sessionId跟當前cookie數組中的JSESSIONID的值不同,因此它會將新的sessionId覆蓋掉cookie數組中原來的JSESSIONID,而且因爲此時的session是嶄新的,因此他不可能有userId這樣的屬性值,因此在攔截的時候依然會被截獲,所以也是須要從新登陸的。
有興趣的同窗能夠去試驗一下。
能夠經過setMaxInactiveInterval()方法來設置session的時限,好比能夠設爲半個小時。這個時間指的是session不活躍開始計算的時間。超過這個時間,session就失效了。此時若再getSession(),則會建立一個新的session,而且其sessionId爲此時瀏覽器中JSESSIONID的值。
有三種方式來設置session的時限:
--經過在web容器中設置(以tomcat爲例),在tomcat-7.0\conf\web.xml中設置,如下是tomcat7.0中默認配置:
<session-config> <session-timeout>30</session-timeout> </session-config>
--經過web項目的web.xml文件來設置。設置15分鐘失效。
<session-config> <session-timeout>15</session-timeout> </session-config>
--直接在代碼中設置
session.setMaxInactiveInterval(30*60);//以秒爲單位,即在沒有活動30分鐘後,session將失效
上述三種方法的優先級:1<2<3.
咱們來作個試驗,在web.xml中設置session的過時時間爲1分鐘,而後觀察session和cookie的變化。
第一次訪問:
過了一分鐘後,再次訪問
發現仍是沒變,但立刻再次訪問。
結合咱們以前所說的session和cookie的做用過程來解釋一下:第二次訪問的時候,由於過了一分鐘,超過了session的過時時間,因此此時雖然cookie中仍是原來的sessionID(這也是爲何第二次與第一次的請求頭中SessionID相同的緣由),可是經過此SessionID是沒法再找到那個已經失效的session,因此服務端必須從新建立一個session,而且把新的sessionID放到cookie中,覆蓋掉原來舊的。因此當咱們立刻再次訪問的時候,這一次就是新的cookie了。
ps:其實這個過程跟服務端暫停服務的效果是同樣的,只不過服務端暫停服務影響的是內存中的全部session,而session過時只是影響當前過時的這個session。
pss:cookie過時與session過時相似,只不過是發生在客戶端,你們能夠依照前面講的做用過程試着推導cookie過時會發生什麼事情。
psss:若是用了redis等內存數據庫來管理session,那麼設置過時時間將不起做用。
cookie依賴於瀏覽器,session依賴於服務器。若是項目不在瀏覽器上面運行,那麼cookie也就無用武之地,可是咱們仍是想要使用session。不巧的是,session依賴cookie進行管理,這時候要怎麼辦呢?
舉個很常見的場景,咱們想要在安卓,或者ios、或者微信小程序等非瀏覽器的項目中使用session。這時候這些客戶端不會自動幫咱們作管理cookie的工做。那麼這時候咱們須要本身來作。
前面講到過,servlet底層幫咱們自動使用cookie來管理session,大致過程是:從cookie中找尋sessionID,根據sessionID去找session,找到合適的就用,找不到的話就新建一個,而且用新的sessionID覆蓋掉cookie中舊的。
而如今須要明白兩點:
(1)咱們沒有瀏覽器了,因此不會在http的請求頭中攜帶cookie信息,servlet後臺收不到cookie信息,天然找不到JESSIONID,因此他會在每次請求都建立一個新的session,這對於服務端來講,是一筆嚴重的性能開銷。
(2)由於沒有瀏覽器了,servlet在更新完sessionID之後,不會自動執行set-cookie操做將新的sessionID去覆蓋舊的cookie中的JESSIONID的值。
根據以上兩點,我咱們須要作以下幾件事情。
(1):模擬http請求的時候在請求頭中帶上cookie,在cookie中塞入從JESSIONID。這樣服務端就能夠取到JESSIONID,從而獲取sessionID,進行比對來獲取session;
(2),在響應給客戶端的時候,手動調用set-cookie方法將本次的sessionID放入JESSIONID中,這樣客戶端就能夠獲得最新的JESSIONID。
(3):而後客戶端須要維護一個cookie的靜態變量(或者用其餘方法,總之就是維護一個cookie的內存變量)。將服務端響應回來的JESSIONID的值存在這個變量裏面。做爲下次請求的時候放入請求頭。
(4):咱們一樣能夠在服務端寫一個攔截器,客戶端的每一個請求都必須先通過攔截器,這樣就能夠經過session來判斷用戶是否處於登陸狀態了。這個過程與在瀏覽器平臺毫無二致。
下面咱們來實現上面的功能。
因爲本人不會寫安卓和iOS的代碼,因此以一個客戶端程序來模擬安卓端。
首先,定義一個靜態變量用於存儲JESSIONID.初始值爲空。
public static String J_SESSIONID="";
接下來咱們編寫模擬http請求的類,須要在這個類中的請求頭中加入cookie,而且在響應的時候獲得響應頭中的cookie.具體代碼以下。
public static String http4Cookie(String url, String param,String jsessionId) { String result = ""; BufferedReader in = null; try { String urlNameString = url + "?" + param; URL realUrl = new URL(urlNameString); // 打開和URL之間的鏈接 HttpURLConnection connection = (HttpURLConnection) realUrl .openConnection(); // 設置通用的請求屬性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); connection.setRequestProperty("Cookie", "JSESSIONID="+jsessionId);//請求頭中加入JessionID // 創建實際的鏈接 connection.connect(); // 獲取全部響應頭字段 Map<String, List<String>> map = connection.getHeaderFields(); // 遍歷全部的響應頭字段,獲取響應頭中的JESSIONID信息 for (String key : map.keySet()) { // System.out.println(key + "--->" + map.get(key)); if("Set-Cookie".equals(key)){ String setCookie=map.get(key).toString(); String newJessionId=setCookie.substring(12,setCookie.lastIndexOf("]")); J_SESSIONID=newJessionId;// 維護新的J_SESSIONID System.out.println(setCookie.substring(12,setCookie.lastIndexOf("]"))); } } // 定義 BufferedReader輸入流來讀取URL的響應 in = new BufferedReader(new InputStreamReader( connection.getInputStream(), "utf-8")); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("發送GET請求出現異常!" + e); e.printStackTrace(); } // 使用finally塊來關閉輸入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; }
而後咱們在服務端寫一個Action讓客戶端來模擬請求。
@ResponseBody @RequestMapping("httpCookieTest") public String httpTest(HttpServletRequest req,HttpServletResponse res) { Cookie[]cookies=req.getCookies(); for(Cookie cookie:cookies){ if(cookie.getName().equals("JSESSIONID")); System.out.println(cookie.getValue()); } OutPutMsg.outPutMsg(res, req, "httpCookieTest"); return null; }
特別注意這裏的OutPutMsg,它的代碼以下。
1 public static void outPutMsg(HttpServletResponse response,HttpServletRequest request,String msg) { 2 response.setCharacterEncoding("utf-8"); 3 String sessionId = request.getSession().getId(); 4 response.setContentType("text/html; charset=UTF-8"); 5 response.setHeader("Set-Cookie", "JSESSIONID=" + sessionId); 6 PrintWriter writer = null; 7 try { 8 writer = response.getWriter(); 9 writer.print(msg); 10 writer.flush(); 11 } catch (IOException e) { 12 e.printStackTrace(); 13 } finally { 14 if (writer != null) 15 writer.close(); 16 } 17 }
注意到第4和第5行代碼,這兩行代碼往響應頭中加入了此時的sessionId,做爲JSESSIONID的值,放入到cookie中。這樣客戶端才能從響應頭中獲取最新的JSESSIONID,以更新靜態變量J_SESSIONID的值,對應http4Cookie中的代碼。
而後咱們來看看客戶端的主函數。
public static void main(String args[]){ HttpUtil.http4Cookie("http://192.168.1.185:8080/warrior/httpCookieTest","1=1",J_SESSIONID); }
這樣之後,其實這個過程已經跟瀏覽器的cookie運做機制並沒有二致了。
最後,咱們在寫一個攔截器,對全部客戶端請求進行攔截。客戶端每次請求之間都率先訪問這個攔截器。
@ResponseBody @RequestMapping("otherPlatformIntercept") public String otherPlatformIntercept(HttpServletRequest req){ HttpSession httpSession=req.getSession(); if(httpSession.getAttribute("userId")!=null){ return "valid"; } return "Invalid"; }