利用Cookie跟蹤用戶狀態
博客系統由多個功能(頁面)組成:
- 首頁——包含博客功能簡介、用戶列表
- 用戶博客列表——包含某一用戶的文章列表
- 文章詳細頁面——包含某一篇文章的標題、詳細內容、創建時間等信息
- 創建文章頁面——包含文章標題、內容的表單
在創建文章時,除了標題和內容,還需要知道是誰創建的這篇文章,當然我們不能夠在表單中添加一個輸入框讓用戶輸入自己是誰——因爲用戶的身份很有可能被僞造。一個辦法是在每一個頁面被訪問之前,我們要求用戶輸入在網站註冊時輸入的用戶名/密碼進行驗證,如果驗證通過纔可以訪問對應的頁面。HTTP基本認證正好可以完成這樣的功能,可是這樣實在是太麻煩了,沒訪問一個頁面都需要用戶進行輸入。
我們知道HTTP協議本身是無狀態的,也就是說任何完全一致的請求都將會得到完全相同的返回。但是我們可以認爲的在HTTP請求中定義狀態。
Cookie是由客戶端保存的小型文本文件,其內容爲一系列的鍵值對,在瀏覽器訪問同一個域名的不同頁面時,會在HTTP請求中附上Cookie。
Cookie可以保存在內存中(關閉瀏覽器即消失),也可以保存在硬盤中(到達過期時間後消失)。另外,Cookie也是一個比較古老的東西,與JavaScript同樣由網景公司發明,現已被標準化爲RFC2109。
我們正是通過Cookie的這些特性來實現用戶狀態的保存的,每當用戶訪問一個網站時,服務器程序會分配給它一個唯一的id,這個id就是設置在Cookie中的,以打開瀏覽器第一次訪問http://tianmaying.com
爲例,因爲是打開瀏覽器後第一次訪問,所以請求頭中不會有任何網站相關的Cookie存在,這時服務器會隨機分配的一個字符串作爲用戶的id並放在Cookie中:
Set-Cookie:JSESSIONID=9074327855952DA4A296B67685523812; Path=/; HttpOnly
這裏沒有指定Cookie的過期時間(Expire),那麼當瀏覽器關閉後,Cookie自動失效,從獲取網站的Cookie到瀏覽器關閉的這個週期,被稱爲一個會話——Session。那麼在這個會話中,在同一瀏覽器中任何一次對tianmaying.com
的訪問,都會帶上這個名字爲JSESSIONID
(Servlet默認的名字)的Cookie值(在服務器上隨機生成的字符串,服務器並會將其保存下來)。
可以簡單地這樣理解,服務端建立了一個Map<String, Object>
,對於每一個請求都可以拿到Cookie中保存的JSESSIONID
值,那麼服務器程序可以往這個Map
裏寫入任何對象:
map.put(JSESSIONID, <obj>);
那麼在這一次會話中,不管是怎樣的請求,都可以訪問到這個Map
中的對象。而服務器保存的這個Map
,也被稱爲Session)。Session是一種服務器端保存用戶狀態的技術,它最常見的實現方式是在Cookie中加入一個字段存儲Session ID;但同時Cookie並不是實現Session的唯一手段,在URL中通過參數來保存Session ID同樣也是可行的。
用戶登錄
在註冊用戶後,用戶需要登錄網站來確認自己的身份,登錄的方式就是在HTML表單中填入用戶名和密碼並提交給服務器進行驗證。在瀏覽器的一次Session會話中,Cookie中的JSESSIONID
保持不變,所以登錄成功(服務器端通過驗證)後,我們可以在服務端Session中放入當前的用戶對象,這樣以後訪問任何URL,在對應的JSP/Servlet中都能輕易的拿到它。以下是一個多用戶訪問的Web應用示意圖:
可以看到,每一個用戶都有一個自己的Session。而在JSP/Servlet中,通過API可以很容易的獲取到當前用戶的Session並將狀態信息放入其中。
在Servlet中設置Session屬性值
以用戶登錄爲例:
@WebServlet("/account/login")
public class LoginController extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = Data.getByUsername(username);
if (user == null || !user.getPassword().equals(password)) {
//登錄不成功,返回錯誤信息
} else {
req.getSession().setAttribute("user", user);
//返回登錄成功信息
}
}
}
獲取Session的API是req.getSession()
,接下來通過getAttribute()
和setAttribute()
方法就可以像一個Map
一樣獲取/設置Session的屬性值。這裏我們將User
對象放入了Session,名字爲user
。
在JSP中獲取Session屬性值
在渲染頁面時,很多時候需要獲取當前用戶的信息,例如導航欄的顯示:
- 根據用戶是否登錄顯示不同的信息
- 如果用戶已經登錄,需要根據當前用戶信息顯示某些內容——例如“我的首頁”的鏈接
在JSP Scriptlet中可以通過session
對象來獲取Session的屬性值,如果使用EL表達式,那麼需要使用${sessionScope}
得到Session對象,下面以導航欄爲例:
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<c:choose>
<c:when test="${sessionScope.user != null}">
<li><a href="userPosts?username=${sessionScope.user.username}">我的首頁</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">管理
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">博客</li>
<li><a href="#">博客信息</a></li>
<li><a href="createPost">創建博文</a></li>
<li><a href="admin/posts">博客文章</a></li>
<li class="divider"></li>
<li class="dropdown-header">賬號</li>
<li><a href="#">個人信息</a></li>
<li><a href="#">更改密碼</a></li>
<li><a href="account/logout">退出</a></li>
</ul>
</li>
<li><a href="account/logout">退出登錄</a></li>
</c:when>
<c:otherwise>
<li><a href="account/login">登錄</a>
<li><a href="jsp/register.jsp">註冊</a>
</c:otherwise>
</c:choose>
</ul>
</div>
${sessionScope.user != null}
作爲判斷條件表示當前用戶已經登錄;href="userPosts?username=${sessionScope.user.username}"
則是利用當前用戶的屬性值渲染鏈接。