---
title: 'Cookie、Session - 機制、管理'
disqus: kyleAlien
---
Cookie、Session - 機制、管理
===
## Overview of Content
如有引用參考請詳註出處,感謝 :cat:
[TOC]
## 會話管理 Session Management
Http 通訊之所有需要有管理,是因為 Http 的特性!**Http 本身是無狀態通訊**,所以伺服器、客戶端都不會知道上一次的通訊情況
**++基於 Http 這個 ==無狀態== 特性++,造就了 `Cookie`、`Session` 管理**
> 這種記得上一次請求、這次請求的關聯關係也稱為 `Session Management`;
>
> eg. 記得問卷的上下頁關聯就需要會話管理
## Web 應用管理
`Hidden Field`、`Cookie`、`URL Rewriting` 三種方案,這些方案都是將資訊保存在遊覽器中,並且以下這些操作都由 Web 應用開法
### 隱藏欄位 - Hidden Field
* 既然伺服器不會記得客戶端的通訊,那就由客戶端(瀏覽器),來傳送一個隱藏欄位(類似一個 TAG),來讓伺服器判斷這個欄位
> 藉由多次請求間,帶入必要的資訊,來讓伺服器認得… 這種手法就稱為 **隱藏欄位 `Hidden Field`**
1. 第一次訪問,客戶端傳進隱藏欄位,伺服器照樣回傳
```mermaid
graph LR;
客戶端_瀏覽器 --> |1. 參數 apple=red| 服務端_伺服器
服務端_伺服器 -.-> |2. 參數 apple=red| 客戶端_瀏覽器
```
2. 第二次訪問,客戶端將隱藏參數繼續帶給伺服器
```mermaid
graph LR;
客戶端_瀏覽器 --> |3. 參數 apple=red&banana=yellow | 服務端_伺服器
```
* 這種隱藏欄位的方式有以下特點
* 透過 HTML 的「**隱藏欄位**」(`type='hidden'`)來達到數值的傳遞
```xml=
<input type='hidden' name='" + 參數Key + "' value='" + 參數Value + "'><br>"
```
* 這種方式需要客戶端的遊覽器與伺服器協作,才能達到參數傳遞的功能
:::warning
* 當客戶遊覽器關閉時,這種機制就會失效
* **「隱藏欄位」++並非++ Servlet 實際的會話管理機制**
:::
* 隱藏欄位 範例如下
```java=
@WebServlet(
"/hidden.field"
)
public class HiddenFieldServlet extends HttpServlet {
private static final String QUESTION = "color_question";
private static final String APPLE_PARAM = "apple";
private static final String BANANA_PARAM = "banana";
private static final String BANANA_QUESTION = "next_banana";
private static final String FINISH_QUESTION = "question_finish";
private void handleHiddenField(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("UTF8");
// setting MIME(Header)
resp.setContentType("text/html; charset=big5");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hidden Field</title>");
out.println("</head>");
String question = req.getParameter(QUESTION);
out.println("<body>");
// 由於是導向相同頁面,所以 action 就是這頁的 URI
out.println("<form action='hidden.field' method='post'>");
if (question == null) {
out.println("Apple color:<br>");
out.println("<input type='text' name='" + APPLE_PARAM + "'><br>");
out.println("<input type='submit' name='" + QUESTION + "' value='"+ BANANA_QUESTION +"'>");
} else if(question.equals(BANANA_QUESTION)) {
String appleColor = req.getParameter(APPLE_PARAM);
// 透過參數的傳遞
out.println("<input type='hidden' name='" + APPLE_PARAM + "' value='" + appleColor + "'><br>");
out.println("Banana color:<br>");
out.println("<input type='text' name='" + BANANA_PARAM + "'><br>");
out.println("<input type='submit' name='" + QUESTION + "' value='"+ FINISH_QUESTION +"'>");
} else if (question.equals(FINISH_QUESTION)) {
String appleColor = req.getParameter(APPLE_PARAM);
String bananaColor = req.getParameter(BANANA_PARAM);
out.println("Apple color: " + appleColor + "<br>");
out.println("Banana color: " + bananaColor + "<br>");
}
out.println("</form>");
out.println("</body>");
out.println("</html>");
out.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
handleHiddenField(req, resp);
}
}
```
> 
### Cookie 機制 - addCookie 自動登入
* **`Cookie` 是在遊覽器儲存訊息的一種機制**,伺服器可以回應 **客戶端遊覽器的 ==`set-cookie` 標頭==**;這個機制的 **特性** 如下
> `set-cookie` 是後來拓展的標頭,並非原始的表準標頭
1. 會將 **儲存在客戶端的一個「檔案」** 回傳給伺服器做紀錄,這個 **檔案就稱之為 Cookie**
2. 伺服器可以將一些資訊保存在 Cookie 中
3. 伺服器可以設定 Cookie 存活時間(**透過 `Expires`設定**)
:::info
伺服器不會直接設定(它是間接設定),而是透過 Header 指定,並由客戶端瀏覽器來設置 Cookie 存活時間
:::
```mermaid
graph LR;
服務端_伺服器 --> |set-cookie| 客戶端_瀏覽器
subgraph 客戶端_瀏覽器
檔案
end
檔案 -.-> |cookie 檔案資訊| 服務端_伺服器
```
* Cookie 機制簡單的自動登入範例如下(分為幾個部分,分開來解釋)
1. 入口,**使用者首先進入 Cookie 機制**:
判斷客戶端傳入的 Cookie 是否有需要(自動登入)的資訊
* 如果客戶端沒有 Cookie,則導向到純登入頁(`login.html`)
* 判斷 Cookie 的正確性,其中要存有 `user` 作為 Key 、`cache` 作為 Value;如果都有,則導向使用者頁面(`user.view`)
```java=
@WebServlet(
"/cookie.use"
)
public class CookieServlet extends HttpServlet {
public static final String COOKIE_KEY = "user";
public static final String COOKIE_VALUE = "cache";
private void handleCookie(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName();
String value = cookie.getValue();
// 檢查 Cookie
if (COOKIE_KEY.equals(name) && COOKIE_VALUE.equals(value)) {
RequestDispatcher dispatcher = req.getRequestDispatcher("user.view");
dispatcher.forward(req, resp);
return;
}
}
}
// 沒有 Cookie 代表首次登入
resp.sendRedirect("login.html");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
handleCookie(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
handleCookie(req, resp);
}
}
```
2. 純 HTML 登入頁面
* 兩個輸入框、一個送出按鈕
* 點擊按鈕後轉向 `cookie.use`(第一步),先去確認使用者 Cookie 是否已經存伺服器要求的訊息!
```xml=
<!DOCTYPE html>
<html lang="en">
<head>
<meta content = "text/html; charset=BIG5">
<title>Title</title>
</head>
<body>
<form method="post" action="login.do">
<label>
User name:
<input type="text" name="user.name">
</label><br>
<label>
Password:
<input type="password" name="user.password">
</label><br>
<button>submit</button>
</form>
</body>
</html>
```
> 
3. **登入請求判別**:
登入請求會判斷使用者是否具有 Cookie,並執行對應行為
* Mock 帳密檢查的行為,如果檢查失敗則導向到純登入頁(`login.html`)
* 帳密檢查成功,則設定 Cookie 存活時間、並添加 Cookie Header,並導向使用者頁面(`user.view`)
> 為了讓 `cookie.use` 頁面可以判斷
```java=
@WebServlet(
"/login.do"
)
public class LoginServlet extends HttpServlet {
private static final String INPUT_USER = "user.name";
private static final String INPUT_PASSWD = "user.password";
private void handleLoginLogic(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
String user = req.getParameter(INPUT_USER);
String password = req.getParameter(INPUT_PASSWD);
// 假裝當成帳號密碼確認
if (!"Alien".equals(user) || !"123456".equals(password)) {
resp.sendRedirect("login.html");
return;
}
Cookie cookie = new Cookie(CookieServlet.COOKIE_KEY, CookieServlet.COOKIE_VALUE);
// 設定 expires 時間(7 天),單位秒
cookie.setMaxAge(7 * 24 * 60 * 60); // second is unit
// 設定 setCookie Header
resp.addCookie(cookie); // like set cookie
// 轉發前,將使用者名放入 Attribute
req.setAttribute(CookieServlet.COOKIE_KEY, user);
// 轉發到登入頁
RequestDispatcher dispatcher = req.getRequestDispatcher("user.view");
dispatcher.forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
handleLoginLogic(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
handleLoginLogic(req, resp);
}
}
```
> 
:::info
* 如果想測試,也可以手動刪除 Cookie
:::
4. **使用者頁面**:
單純顯示使用者資訊
```java=
@WebServlet(
"/user.view"
)
public class UserServlet extends HttpServlet {
private void handleUser(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset=big5");
Object user = req.getAttribute(CookieServlet.COOKIE_KEY);
if (user == null) {
user = "Auto login.";
}
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>User information</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>User: " + user +" already login</h1>");
out.println("</body>");
out.println("</html>");
out.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
handleUser(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
handleUser(req, resp);
}
}
```
:::danger
* **為什麼 `doGet` 也要寫,明明就是 post**?
因為瀏覽器在 URL 載入時,是使用 GET 請求;假設使用者是自動登入的狀況之下,就會使用到 Get 請求(**重新導向時就用 Get**)
> 
:::
首次登入
> 
重新從第一步進入頁面,就會直接進入 `user.view` 頁面
> 
### URL 重寫 - URL Rewriting
* URL 重寫(`URL Rewriting`)其實是 **GET 請求參數的應用**;當伺服器回應瀏覽器的 GET 請求時,將相關資訊以 HTML 的「**超連結**」方式回應給瀏覽器
> 並且超連接中會包括請求參數資訊
```java=
@WebServlet(
"/url-rewriting.search"
)
public class URLRewriting extends HttpServlet {
private static final String CUR_PAGE = "curPage";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset=big5; charset=UTF-8");
String start = req.getParameter(CUR_PAGE);
if (start == null || start.isBlank()) {
start = "1";
}
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Url rewriting</title>");
out.println("</head>");
out.println("<body>");
int count = Integer.parseInt(start);
int begin = (10 * count) - 9;
int end = (10 * count);
out.println("The result: " + begin + " ~ " + end + "<br>");
out.println("<ul>");
for (int i = 1; i < 10; i++) {
out.println("<li>Search result: " + i + "</li>");
}
out.println("</ul>");
for (int i = 1; i < 10; i++) {
if (i == count) {
out.println(i);
continue;
}
out.println("<a href='url-rewriting.search?" + CUR_PAGE + "=" + i + "'>" + i + "</a>");
}
out.println("</body>");
out.println("</html>");
out.close();
}
}
```
> 
## Web 容器管理
伺服器(遠端)管理會話的機制,是將會話期間必須「共用的資料」儲存為「屬性」,並且這些資料由 Web 容器管理
### Servelt Session 使用 - getSession
* 在 Servlet/JSP 中,如果想要進行 Session 會話管理,需使用 `HttpServletRequest`#`getSession()` 方法取得 HttpSession 物件,常用方法如下
1. Servlet 中的 `getSession()` 的參數如果設置為 `true`(該函數有被重載),就會在沒有 Session 時自動創建;
> 如果沒有設置參數… 則可能返回 Null
>
> 
2. 如果想在瀏覽器、Web 應用程式的會話(通訊)期間保留訊息,可以使用 HttpSession 類的 `setAttribute`、`getAttribute` 方法
> 將與 Client 端通訊的資料設定在 Web 應用中
:::warning
* 如果 Session 已經被設置為 `invalidated`… 那就不能再設定 Attribute,否則伺服器會出錯
> 
:::
3. HttpSession 也可以透過 `setMaxInactiveInterval` 設定存活區間(秒為單位),如果使用者在幾秒內沒有請求應用的話,就讓該 Session 失效
:::success
* Session 特點:
* **可以無視 Http 的請求週期**
* 預設在遊覽器關閉之前,HttpSession 物件中儲存的資料都會是相同實例
> 如果像提前在這次會話中清除 Session,可以使用 HttpSession#`invalidate` 方法
>
> 
:::
:::danger
* **`HttpSession` 並非執行緒安全**,所以在使用時請注意同步的問題
:::
### Session 使用 - 改寫隱藏欄位
* 小試 HttpSession 的使用,範例如下
* 改寫隱藏欄位的範例
```java=
@WebServlet(
"/session.base.usage"
)
public class SessionUsageServlet extends HttpServlet {
private static final String QUESTION = "color_question";
private static final String APPLE_PARAM = "apple";
private static final String BANANA_PARAM = "banana";
private static final String BANANA_QUESTION = "next_banana";
private static final String FINISH_QUESTION = "question_finish";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("UTF8");
// setting MIME(Header)
resp.setContentType("text/html; charset=big5");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hidden Field</title>");
out.println("</head>");
String question = req.getParameter(QUESTION);
out.println("<body>");
// 由於是導向相同頁面,所以 action 就是這頁的 URI
out.println("<form action='hidden.field' method='post'>");
if (question == null) {
out.println("Apple color:<br>");
out.println("<input type='text' name='" + APPLE_PARAM + "'><br>");
out.println("<input type='submit' name='" + QUESTION + "' value='"+ BANANA_QUESTION +"'>");
} else if(question.equals(BANANA_QUESTION)) {
String appleColor = req.getParameter(APPLE_PARAM);
// use session
HttpSession session = req.getSession(true);
session.setAttribute(APPLE_PARAM, appleColor);
out.println("Banana color:<br>");
out.println("<input type='text' name='" + BANANA_PARAM + "'><br>");
out.println("<input type='submit' name='" + QUESTION + "' value='"+ FINISH_QUESTION +"'>");
} else if (question.equals(FINISH_QUESTION)) {
HttpSession session = req.getSession();
String appleColor = (String) session.getAttribute(APPLE_PARAM);
String bananaColor = req.getParameter(BANANA_PARAM);
out.println("Apple color: " + appleColor + "<br>");
out.println("Banana color: " + bananaColor + "<br>");
}
out.println("</form>");
out.println("</body>");
out.println("</html>");
out.close();
}
}
```
> 
### Session 使用 - 登入、登出功能
* 改寫 Login 頁面的範例,並在 user 頁面新增登出功能(並且這些功能都用 Session 完成)
1. 入口,**登入頁面**:
純 `HTML`,這部分沒有修改很多,只修改以下兩點
* `form`#`action` 導向 `session.login.do`
* 並且入口不再是 Cookie 判斷,是直接從這個頁面進入(拿掉自動登入功能)
```xml=
<!DOCTYPE html>
<html lang="en">
<head>
<meta content = "text/html; charset=BIG5">
<title>Title</title>
</head>
<body>
<form method="post" action="login.do">
<label>
User name:
<input type="text" name="user.name">
</label><br>
<label>
Password:
<input type="password" name="user.password">
</label><br>
<button>submit</button>
</form>
</body>
</html>
```
> 
2. **Login 參數處理**:
* 這裡會透過 `getSession()` 方法 **創建 HttpSession 物件**
* 並使用 HttpSession#`setAttribute` 方法將使用者的名稱儲存進 Session
```java=
@WebServlet(
"/session.login.do"
)
public class LoginServlet extends HttpServlet {
public static final String INPUT_USER2 = "user.name";
private static final String INPUT_PASSWD2 = "user.password";
private void handleLoginLogic(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
String user = req.getParameter(INPUT_USER2);
String password = req.getParameter(INPUT_PASSWD2);
// 假裝檢查帳密
if (!"Alien".equals(user) || !"123456".equals(password)) {
resp.sendRedirect("login_with_session.html");
return;
}
// 創建 Session
HttpSession session = req.getSession(true);
// 名稱儲存
session.setAttribute(INPUT_USER2, user);
// 請求轉發
RequestDispatcher dispatcher = req.getRequestDispatcher("session.user.view");
dispatcher.forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
handleLoginLogic(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
handleLoginLogic(req, resp);
}
}
```
3. **使用者頁面**:
* 透過 HttpSession#`getAttribute` 取得使用者名稱
* **新增 Logout 頁面** 的連結導向
```java=
@WebServlet(
"/session.user.view"
)
public class UserServlet extends HttpServlet {
private void handleUser(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset=big5");
// 取得 Session
HttpSession session = req.getSession(true);
// 取得使用者名稱
String user = (String) session.getAttribute(LoginServlet.INPUT_USER2);
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>User information</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>User: " + user +" login</h1>");
out.println("<a href='session.logout.view'>Logout</a>");
out.println("</body>");
out.println("</html>");
out.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
handleUser(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
handleUser(req, resp);
}
}
```
> 
4. **登出機制**
* 使用 HttpSession#`invalidate` 清除儲存在伺服器端的使用者訊息
```java=
@WebServlet(
"/session.logout.view"
)
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset=big5");
HttpSession session = req.getSession(true);
Object user = session.getAttribute(LoginServlet.INPUT_USER2);
// 呼叫 invalidate,告知 Web 容器 這個 Session 失效
session.invalidate();
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>User Logout</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>User: " + user +" already logout</h1>");
// 導回 HTML
out.println("<a href='login_with_session.html'>Login</a>");
out.println("</body>");
out.println("</html>");
out.close();
}
}
```
> 
### Web 容器 - Session 機制原理
* Session 機制原理其實是透過「**Web 容器**(也就是伺服器)」來達成(Web 應用開發者並不用處理),當我們在執行 HttpSession 時,Web 容器 會幫我們處理如下
1. 建立 HttpSession 物件
```mermaid
graph TB;
subgraph 伺服器_Web容器
HttpSession_物件
subgraph Web應用
create_session
end
end
create_session --> |創建| HttpSession_物件
```
2. HttpSession 物件與 `Session ID` 綁定
> 可以透過 HttpSession#`getId` 函數來獲取這個特殊 ID
```mermaid
graph TB;
subgraph 伺服器_Web容器
HttpSession_物件 <-.-> |綁定| Session_ID
subgraph Web應用
create_session
end
end
create_session --> HttpSession_物件
```
3. 這個 Session ID 預設會使用 Cookie 存放至客戶端的瀏覽器之中… 也就是說,**其實 ++Session 仍是使用了 Cookie 機制++**
:::success
Web 容器幫我們操作 Cookie 機制,並將 Web 容器綁定的 ID 透過 `JSESSIONID` 存到客戶端的 Cookie 中
:::
```mermaid
graph LR;
subgraph 伺服器_Web容器
HttpSession_物件 --> Session_ID
subgraph Web應用
create_session
end
end
subgraph 客戶端_瀏覽器
Cookie_文件
end
create_session --> HttpSession_物件
Session_ID --> Cookie_文件
```
Web 伺服器會替 Web 應用創建 Session,並回傳到使用者空間存處起來,如下圖所見,它會放置在 Cookie 的位置
> 
* Web 容器(網頁伺服器),其實本身也是 JVM 之下的一個應用程式,這個應用程式會一直存在伺服器端,而 **Session 機制則是將 Session ID 交給 Web 容器來管理**
* 伺服器端設定 Session 的概念圖
伺服器將這個 Session ID 通過 HTTP 回應的 Set-Cookie Header 傳送到客戶端,讓客戶端將其儲存在 Cookie 中
```mermaid
graph LR;
subgraph 伺服器_Web容器
HttpSession_物件A --> Session_ID_A
HttpSession_物件B --> Session_ID_B
subgraph Web應用
create_session
end
end
subgraph 客戶端A_瀏覽器
Cookie_文件A
end
subgraph 客戶端B_瀏覽器
Cookie_文件B
end
create_session --> HttpSession_物件A
create_session --> HttpSession_物件B
Session_ID_A --> Cookie_文件A
Session_ID_B --> Cookie_文件B
```
* 客戶端回傳資訊時自動會帶入 Session
在後續的每次 HTTP 請求中,客戶端會自動將這個 Session ID 以 Cookie 的形式放在 HTTP Header 中傳送給伺服器
```mermaid
graph LR;
subgraph 伺服器_Web容器
Session_ID_A --> HttpSession_物件A
Session_ID_B --> HttpSession_物件B
subgraph Web應用
create_session
end
end
subgraph 客戶端A_瀏覽器
Cookie_文件A
end
subgraph 客戶端B_瀏覽器
Cookie_文件B
end
HttpSession_物件A --> create_session
HttpSession_物件B --> create_session
Cookie_文件A --> Session_ID_A
Cookie_文件B --> Session_ID_B
```
:::danger
* Web 容器儲存的 Session ID 的 Cookie 預設為關閉瀏覽器時失效
> 客戶端重開遊覽器後,HttpSession 取得的 Session 就會是新的物件
* 由於 Session 是存在 Web 容器的「記憶體中」,所以不適合儲存過大的資料(像是音頻、視頻... 等等資料就不合適,因為瀏覽器自身也會限制存儲空間)
:::
### Cookie 禁用 - 自動產生 URL 重寫
* 我們知道其實 Session 是基於遊覽器的 Cookie 機制後,那我們就可以發現一個問題,使用者可以禁用 Cookie 的存取,這時 Session 機制就會失效
* 在使用者禁用 Cookie 的存取後,我們可以搭配「URL 重寫」機制,向遊覽器回應的 HTML 中,插入需要的超連結
這時可以運用 JavaEE Servlet 的 **HttpServletResponse#`encodeURL` 方法**,這個方法在不同狀況之下有不同的反應
```java=
@WebServlet(
"/url-rewriting.refresh.count"
)
public class URLRewriting extends HttpServlet {
private static final String COUNT_KEY = "count_key";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset=big5; charset=UTF-8");
int count = 0;
// 產生 Session
HttpSession session = req.getSession(true);
// 嘗試取得 Attribute
Object curPage = session.getAttribute(COUNT_KEY);
if (curPage != null) {
// 取的到,代表使用者沒有禁用 Cookie
// 將 Cookie 的值 + 1
count = Integer.parseInt(((String) curPage)) + 1;
}
// 刷新 Session Attribute 的設置
session.setAttribute(COUNT_KEY, count);
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Url count: " + count + "</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Url count: " + count + "</h1>");
// 自動產生對應的 URL!不同情況會產生不同 URL
String url = resp.encodeURL("url-rewriting.refresh.count");
out.println("<h2>Url: " + url + "</h2>");
out.println("<a href='" + url + "'>refresh</a>");
out.println("</body>");
out.println("</html>");
out.close();
}
}
```
* **Cookie 沒有被禁用的情況下**
代表 **Session 可以正常取得遊覽器端的 Cookie**,`encodeURL` 方法會將傳入的 URL 原封不動的返回(你寫什麼它就返回什麼)
> 未禁用,刷新多次的情況
>
> 
:::info
* **伺服器如何知道客戶端的遊覽器是否禁用 Cookie**?
伺服器實際上是無從得知客戶端遊覽器是否禁用 Cookie,所以在 **第一次設定 Session 時會 ++寫在 URL 中++、++存在 Cookie++ 兩個方法都做**
直到第一次通訊後,確認客戶端的遊覽器沒有禁用 Cookie 後(Web 容器會嘗試取得 Session),再改用純 Cookie 機制的 Session
> 以下使用「**無痕模式**」訪問
1. 第一次訪問:可以看到伺服器訪問時,會將 Cookie 帶入 Get 參數中(`jsessionid` 參數)
> 
2. 第二、三… 次訪問之後的訪問,發現客戶端沒有禁用 Cookie 機制,Web 容器就會使用純 Session 機制(不將 Session ID 保存在參數中)
> 
:::
* **Cookie 被禁用的狀況下**
**Session 無法取得 Cookie**,`encodeURL` 方法會自動產生帶有 Session ID 得 URL,並且
> 在之後的請求,Web 容器就會從回應中取得對應的的 Session ID
:::warning
* 本地開發似乎無法禁用 localhost Cookie
:::
### Session ID 的安全性
* 在伺服器 Web 容器中,會儲存 `Session ID`,而伺服器其實並不會區分是哪個客戶端擁有這個 `Session ID`,也就是說… 只要另一個人使用同一個 `Session ID` 就可以訪問到不同客戶的相同資源
```mermaid
graph LR;
subgraph 伺服器_Web容器
Session_ID_A --> HttpSession_物件A
subgraph Web應用
create_session
end
end
subgraph 客戶端A_瀏覽器
Cookie_文件A
end
subgraph 客戶端B_瀏覽器
Cookie_文件B
end
HttpSession_物件A --> create_session
Cookie_文件A --> Session_ID_A
Cookie_文件B -.-> |與 A 相同的 ID | Session_ID_A
```
所以建議
1. 在同 Session 下,加密取得的敏感資訊(使他人取得也不知如何破解)
2. 在不使用 `Session ID` 時,執行 `invlidate`,使該 ID 失效
3. 在會話的重要階段,務必重新做一次身份確認
### Cookie 與 Session 的差異
* 前面小節我們說到 Cookie、Session 都是瀏覽器保存資料的手段,並且保存的資料都是放置在客戶端(使用者個瀏覽器端),那它們兩者的差異在哪呢?
我們已幾個面向來討論
1. **儲存位置**:
* **Cookie**:**資料儲存在客戶端的瀏覽器中**,每當客戶端向伺服器發出請求時,相關的 Cookie 資料會被自動傳送到伺服器,而這些資訊往往是明碼可識別的有用資訊
* **Session**:真正有意義的資料主要儲存在伺服器端,而不是客戶端,客戶端只保留 ID,但它並不會知道 ID 對應的資料的含義;
而伺服器根據客戶端的 Session ID 來識別和存取儲存在伺服器的 Session 資料
2. **安全性**:
* Cookie:由於儲存在客戶端,Cookie 容易受到攻擊,例如 XSS(跨站腳本攻擊)或竊取 Cookie 資料。除非加密,否則不建議在 Cookie 中存儲敏感資訊
* Session:儲存在伺服器端的資料相對更安全,因為客戶端無法直接存取 Session 資料… 客戶端僅傳送 Session ID,伺服器會根據此 ID 存取相關資料。
3. **生命週期**:
* Cookie:可以設置過期時間,並且即使 **瀏覽器關閉後,Cookie 仍可保留**,直到過期時間到達或被手動刪除
* Session:通常與瀏覽器會話相聯繫,當瀏覽器關閉時,Session 通常會自動失效;當然,也可以設置特定的失效時間,但默認情況下,Session 會比 Cookie 更短暫
4. **用途**:
* Cookie:常用於儲存客戶端的一些持久性資料,例如用戶偏好設置、追蹤 ID、廣告相關資料等
* Session:常用於儲存與用戶會話相關的臨時資料,例如登入狀態、購物車內容等,這些資料只需在特定的會話期間內有效
## Session 監聽器
JavaEE Servlet 與 HttpSession 相關的監聽器有 4 個:^1.^ `HttpSessionListener`、^2.^ `HttpSessionAttributeListener`、^3.^ `HttpSessionBindingListener`、^4.^ `HttpSessionActivationListener`
這些 Listener 都 **須使用 `@WebListener` 註解** 告知容器,否則容器不會通知
### 監聽 Session 生命週期
* HttpSessionListener 是 Session 生命週期監聽器
> 
:::info
可以用這個機制來實現當前服務有多少使用者在訪問
:::
* HttpSessionListener 使用範例:
在 Session 被移除時抓取,並將特殊屬性設定給 ServletContext
```java=
@WebListener
public class HttpSessionListenerImpl implements HttpSessionListener {
public static final String SESSION_CATCHER = "session catcher";
// Session 創建
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSessionListener.super.sessionCreated(se);
}
// Session 失效
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
ServletContext context = session.getServletContext();
Object attrVal = session.getAttribute(SessionTesterServlet.SESSION_CREATE);
if (attrVal == null) {
context.setAttribute(SESSION_CATCHER, "Get nothing ?");
} else {
context.setAttribute(SESSION_CATCHER, attrVal + ", i got you.");
}
}
}
```
測試 HttpSessionListenerImpl
```java=
@WebServlet("/session.tester")
public class SessionTesterServlet extends HttpServlet {
public static final String SESSION_CREATE = "session create";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
HttpSession session = req.getSession(true);
session.setAttribute(SESSION_CREATE, "Session hello :D");
session.invalidate();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
out.println("<h3> Catcher : " + getServletContext().getAttribute(HttpSessionListenerImpl.SESSION_CATCHER) + "</h3>");
out.println("</body>");
out.close();
}
}
```
> 
### 監聽 Session 屬性
* HttpSessionAttributeListener 是 Session 屬性的監聽器(包括添加、移除、替換)
> 
```java=
@WebListener
public class HttpSessionAttrsListenerImpl implements HttpSessionAttributeListener {
public static final String SESSION_ATTRS_CATCHER = "session attrs catcher";
private final List<HttpSession> sessionCather = new ArrayList<>();
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
if (session.getAttribute(SESSION_ATTRS_CATCHER) == null) {
session.setAttribute(SESSION_ATTRS_CATCHER, session.getId() + " added");
sessionCather.add(session);
}
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
sessionCather.remove(session);
}
}
```
### 監聽 Session 物件綁定狀態
* 當有一個物件要賦予給 Session 作為屬性時(或移除),你希望可以知道該物件是否在 Session 之中,就可以使用 HttpSessionBindingListener
> 
```java=
@WebListener
public class HttpSessionBindingListenerImpl implements HttpSessionBindingListener {
private String webSessionId = "";
@Override
public void valueBound(HttpSessionBindingEvent event) {
webSessionId = event.getSession().getId();
// 可以在這裡取得資料庫檔案
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
webSessionId = "";
// 可在這裡移除資料庫檔案
}
}
```
### 監聽 Session 啟用
* HttpSessionActivationListener 不是很常使用,它是「**物件遷移監聽器**」;通常會在用「**分散式**」開發環境中,因為 **分散式開發會將物件傳遞到不同的 JVM 中**(這就需要 **序列化物件**)
所以其實該監聽器就是在監聽物件的「序列化」(`sessionWillPassivate`)、「反序列化」(`sessionDidActivate`)
> 
:::info
* 觸發時機是 Web 容器對於該物件的序列化、反序列化… 其中並不包括自己手動序列化、反序列化
* 並且序列化物件也需要標示(實做)`Serializable` 界面
:::
範例:在序列化、反序列化時反轉密碼
```java=
@WebListener
public class HttpSessionActivationListenerImpl implements HttpSessionActivationListener {
public static class UserInfo {
public final String userName;
public String password;
public UserInfo(String userName, String password) {
this.userName = userName;
this.password = password;
}
}
// 序列化
@Override
public void sessionWillPassivate(HttpSessionEvent se) {
Object userInfo = se.getSession().getAttribute("UserInfo");
if (userInfo instanceof UserInfo) {
revertPassword((UserInfo) userInfo);
}
}
// 反序列化
@Override
public void sessionDidActivate(HttpSessionEvent se) {
Object userInfo = se.getSession().getAttribute("UserInfo");
if (userInfo instanceof UserInfo) {
revertPassword((UserInfo) userInfo);
}
}
private void revertPassword(UserInfo userInfo) {
byte[] bytes = userInfo.password.getBytes(StandardCharsets.UTF_8);
byte[] revertBytes = new byte[bytes.length];
for (int i = bytes.length - 1, j = 0; i >= 0; i--, j++) {
revertBytes[j] = bytes[i];
}
userInfo.password = new String(revertBytes, StandardCharsets.UTF_8);
}
}
```
## Appendix & FAQ
:::info
:::
###### tags: `Web`