用戶登出 → 瀏覽器點擊「後退」或「刷新」 → 表單重新提交 → 服務端復用舊 Session 或創建新 Session 卻未驗證登出狀態 → 用戶「幽靈登錄」成功,嚴重安全隱患。
HttpSession (session.invalidate())<%-- login.jsp --%>
<%
String token = String.valueOf(new Random().nextLong());
session.setAttribute("token", token);
%>
<form action="LoginServlet" method="post">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="hidden" name="token" value="${sessionScope.token}"/>
<input type="submit" value="登錄"/>
</form>
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession(false); // 不自動創建
String sessionToken = (session != null) ? (String) session.getAttribute("token") : null;
String formToken = req.getParameter("token");
// 關鍵判斷:表單 Token 與 Session Token 不匹配 → 登出後重發
if (sessionToken == null || !sessionToken.equals(formToken)) {
req.setAttribute("msg", "您已登出,請重新登錄");
req.getRequestDispatcher("/message.jsp").forward(req, resp);
return;
}
// 正常登錄邏輯...
// 登錄成功後:移除/更新 Token,防止同一表單重複提交
session.removeAttribute("token");
}
}
@WebServlet("/LogoutServlet")
public class LogoutServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession(false);
if (session != null) session.invalidate();
resp.sendRedirect(req.getContextPath() + "/login.jsp");
}
}
| 階段 | Session Token | 表單 Token | 匹配結果 |
|---|---|---|---|
| 首次登錄頁 | T1 | T1 (隱藏域) | ✅ 匹配 → 登錄成功,Session 移除 Token |
| 用戶登出 | Session 銷毀,T1 消失 | 瀏覽器緩存頁面仍為 T1 | — |
| 後退/刷新重發 | 新 Session,無 Token (null) | 舊表單攜帶 T1 | ❌ null != T1 → 攔截,提示重新登錄 |
userId:token 存 Redis,TTL 30 分鐘history.pushState(null, '', '/login') 替換歷史記錄,配合 window.onpageshow 檢測 persisted 強制跳轉Set-Cookie: JSESSIONID=xxx; SameSite=Strict 防 CSRF(現代瀏覽器支持)Cache-Control: no-store 防止緩存(瀏覽器對 POST 重發不受此影響)同步 Token 模式是經典、輕量、無狀態的服務端防禦,配合現代前端歷史管理,可徹底根治「登出後後退重複登錄」安全漏洞。