WD
Classnote Docs课程课件
07

07 会话技术

学习目标:

  • 理解为什么 HTTP 无状态会引出会话技术的需求
  • 理解客户端技术和服务器技术之间的区别
  • 掌握 Cookie 的设置(构造)与获取,能够通过浏览器查看 Cookie 信息
  • 理解 Session 维护机制,能够分析 Session 的失效原因
  • 熟悉使用 Session 存储和获取信息
  • 掌握 Cookie 和 Session 的使用场景与组合方式

本章重点:

  • Cookie 与 Session 的核心区别与联系
  • JSESSIONID 的工作机制与 Session 的生命周期
  • 登录状态、记住我、访问记录等典型会话场景
  • Cookie / Session 在安全性与易用性之间的权衡
01 / Section

1. 前置知识准备

  • 响应头的设置
  • response.setHeader(String, String)
  • ApiFox中设置请求头
  • Header
  • URL编码
  • 浏览器能够完成编解码的工作
  • 比如我们发送一个Get请求(通过浏览器地址栏)其中的请求参数有中文,这个中文浏览器会自动进行URL编码 → Servlet中使用request获得请求参数的值,这个值是中文,说明request已经帮我们解码了
02 / Section

2. 会话技术概述

2.1 为什么需要会话技术

同一个客户端向服务器中发送的多个请求,需要信息共享。

在做服务器开发过程中,我们的客户端和服务器之间,会有请求报文和响应报文:

  • 客户端给服务器发送请求:请求报文
  • 服务器给客户端发送响应:响应报文

2.1.1 HTTP 协议的无状态性

HTTP协议是无状态的,这意味着服务器默认情况下无法识别两次请求是否来自同一个客户端。

image-20230217145950266
image-20230217145950266

问题:如果没有会话技术会怎样?

举个例子:

李雷(客户端):韩梅梅你好,我想请你吃饭 韩梅梅(服务器):你是谁呀? 李雷(客户端):韩梅梅你好,我是李雷,我想请你吃饭 韩梅梅(服务器):可以啊,在哪里吃饭呢 李雷(客户端):我们去吃香他她煲仔饭吧 韩梅梅(服务器):你是谁,我是和谁去吃煲仔饭 李雷(客户端):吃完饭去看电影吧 韩梅梅(服务器):你是谁,我是和谁去看电影

如果没有会话技术,服务器不清楚每一次请求来源于哪一个客户端。

2.1.2 引入会话技术后的改善

如果引入会话技术,这段会话就会变成:

李雷(客户端):韩梅梅你好,我是李雷,我想请你吃饭 韩梅梅(服务器):好啊,那吃什么 李雷(客户端):我们去吃香他她煲仔饭吧 韩梅梅(服务器):好啊,那说好了,李雷咱两去吃煲仔饭,几点见面 李雷(客户端):晚上6点可以吗 韩梅梅(服务器):可以啊,那吃完饭呢 李雷(客户端):吃完饭去看电影吧 韩梅梅(服务器):好吧

2.2 会话技术的实现方式

2.2.1 方式一:客户端携带信息(Cookie)

李雷(客户端):韩梅梅你好,我是李雷,我想请你吃饭 韩梅梅(服务器):好啊,那吃什么(我给你提醒,你要告诉我你是李雷) 李雷(客户端):我们去吃香他她煲仔饭吧(我是李雷) 韩梅梅(服务器):好啊,那说好了,李雷咱两去吃煲仔饭,几点见面

特点:客户端直接携带确切的信息,这就是客户端技术(Cookie)

2.2.2 方式二:服务器端保险柜(Session)

李雷(客户端):韩梅梅你好,我是李雷,我想请你吃饭 韩梅梅(服务器):好啊,那吃什么(我给你一个编号,这个编号是89757) 李雷(客户端):我们去吃香他她煲仔饭吧(编号89757) 韩梅梅(服务器):好啊,那说好了,李雷咱两去吃煲仔饭,几点见面

特点:客户端只携带编号,服务器通过编号找到对应的保险柜,这就是服务器技术(Session)

2.3 核心区别

归根结底,身份信息的维护方不同:

  • 客户端技术:Cookie - 信息存储在客户端,由客户端维护
  • 服务器技术:Session - 信息存储在服务器,由服务器维护

注意:Session技术是在Cookie技术基础上实现的,Session也需要通过Cookie传递会话ID。

2.4 本章小结

特性 Cookie Session
信息存储位置 客户端(浏览器) 服务器
安全性 较低(可被查看/篡改) 较高(仅在服务器存储)
存储容量 约4KB 无硬性限制(受服务器内存限制)
生命周期 可设置过期时间 默认30分钟
03 / Section

3. 客户端技术 Cookie

3.1 Cookie 基本概念

Cookie 是客户端(浏览器)向服务器发起请求时直接携带的信息,这些信息通过请求头中一个特殊的请求头 Cookie 携带。

3.1.1 Cookie 的格式

text
Cookie: key1=value1; key2=value2
  • 携带的是键值对信息
  • 值都是字符串类型
  • 多组键值对使用分号 ; 分隔

3.2 构造 Cookie 的方式

构造 Cookie 就是让请求头 Cookie 里面包含对应的值。有三种方式:

  1. 浏览器构造 Cookie
  2. ApiFox 构造 Cookie
  3. 服务器构造 Cookie

3.2.1 浏览器构造 Cookie

步骤

  1. 打开开发者工具,快捷键 F12
  2. 找到 应用程序(Application) 标签
  3. 存储(Storage) 中找到 Cookie
image-20230217160439567
image-20230217160439567

请求报文示例

http
GET http://localhost:8083/demo1/cookie/fetch HTTP/1.1
Host: localhost:8083
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="110", "Not A(Brand";v="24", "Microsoft Edge";v="110"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: zhangsan=123456; lisi=654321

第17行Cookie: zhangsan=123456; lisi=654321

3.2.2 ApiFox 构造 Cookie

在 ApiFox 中可以通过 Header 直接设置 Cookie:

<div style="text-align:center"> ApiFox设置Cookie </div>

请求报文

http
GET http://localhost:8083/demo1/cookie/fetch HTTP/1.1
Cookie: zhaoliu=123456; wangwu=789987
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Host: localhost:8083
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

也可以通过 Cookie 管理器 更方便地管理 Cookie:

<div style="text-align:center"> Cookie管理器 </div>

3.2.3 服务器构造 Cookie(重点)

流程

  1. 客户端 → 服务器:发送请求
  2. 服务器 → 客户端:响应(携带 Set-Cookie 头)
  3. 客户端 → 服务器:后续请求自动携带 Cookie

服务器通过特殊的响应头 Set-Cookie 让浏览器设置 Cookie:

java
/**
 * 设置 Cookie
 * 访问:http://localhost:8083/demo1/cookie/set?username=lilei
 * 
 * @author stone
 * @date 2023/02/17
 */
@WebServlet("/cookie/set")
public class CookieSetServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        String username = req.getParameter("username");

        // 方式1:通过响应头设置(原始方式)
        resp.setHeader("Set-Cookie", "username=" + username);
        
        // 方式2:使用 Cookie 对象(推荐方式)
        // Cookie cookie = new Cookie("username", username);
        // resp.addCookie(cookie);
    }
}

响应报文

http
HTTP/1.1 200
Set-Cookie: username=lilei
Content-Length: 0
Date: Fri, 17 Feb 2023 08:15:48 GMT
Keep-Alive: timeout=20
Connection: keep-alive
image-20230217161620669
image-20230217161620669

推荐方式:使用 response.addCookie() 方法:

java
Cookie cookie = new Cookie("username", username);
resp.addCookie(cookie);

3.3 获取 Cookie

3.3.1 获取方式

java
@WebServlet("/cookie/fetch")
public class CookieFetchServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse resp) 
            throws ServletException, IOException {
        // 获取所有 Cookie
        Cookie[] cookies = request.getCookies();
        
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                String name = cookie.getName();
                String value = cookie.getValue();
                System.out.println(name + " -> " + value);
            }
        }
    }
}

3.3.2 获取单个 Cookie 的值

java
/**
 * 获取指定名称的 Cookie 值
 */
private String getCookieValue(HttpServletRequest request, String cookieName) {
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookieName.equals(cookie.getName())) {
                return cookie.getValue();
            }
        }
    }
    return null;
}

3.4 Cookie 的属性详解

属性 方法 说明
name 构造方法参数 Cookie 的名称(必填)
value 构造方法参数 Cookie 的值(必填)
Path setPath(String) Cookie 的有效路径
Domain setDomain(String) Cookie 的域名,用于跨域共享
MaxAge setMaxAge(int) Cookie 的过期时间(秒)
HttpOnly setHttpOnly(boolean) 禁止 JavaScript 访问
Secure setSecure(boolean) 仅通过 HTTPS 传输
image-20230217173529919
image-20230217173529919

3.4.1 Path 属性

作用:设置 Cookie 的有效路径,只有访问该路径或其子路径时,浏览器才会携带此 Cookie。

java
@WebServlet("/cookie/path-demo")
public class PathDemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // 方式1:使用当前请求的默认路径
        Cookie cookie1 = new Cookie("user1", "zhangsan");
        // 默认路径为 /demo1/cookie
        resp.addCookie(cookie1);
        
        // 方式2:手动指定路径
        Cookie cookie2 = new Cookie("user2", "lisi");
        cookie2.setPath("/demo1");  // 整个 /demo1 应用都可访问
        resp.addCookie(cookie2);
        
        // 方式3:根路径
        Cookie cookie3 = new Cookie("user3", "wangwu");
        cookie3.setPath("/");  // 整个域名下都可访问
        resp.addCookie(cookie3);
    }
}

3.4.2 Domain 属性

作用:实现不同子域名间的 Cookie 共享。

java
@WebServlet("/cookie/domain-demo")
public class DomainDemoServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        Cookie cookie = new Cookie("shared_data", "test_value");
        
        // 设置父域名,子域名可共享此 Cookie
        cookie.setDomain(".example.com");
        cookie.setPath("/");
        
        resp.addCookie(cookie);
    }
}

域名层级示例

image-20230217174121261
image-20230217174121261

注意事项

  • 不能设置与当前域名无关的 domain
  • 访问 localhost 时不能设置 ccc.com 这样的 domain,否则浏览器会忽略

3.4.3 MaxAge 属性

作用:设置 Cookie 的有效期(单位:秒)。

含义
正数 Cookie 在指定的秒数后过期,会持久化到硬盘
负数(默认) Cookie 为会话级别,关闭浏览器即失效
0 立即删除该 Cookie
java
@WebServlet("/cookie/maxage-demo")
public class MaxAgeDemoServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // 1. 会话级别 Cookie(浏览器关闭即失效)
        Cookie sessionCookie = new Cookie("temp", "value");
        resp.addCookie(sessionCookie);
        
        // 2. 持久化 Cookie(7天有效)
        Cookie persistentCookie = new Cookie("remember", "token123");
        persistentCookie.setMaxAge(7 * 24 * 60 * 60); // 7天
        resp.addCookie(persistentCookie);
        
        // 3. 删除 Cookie
        Cookie cookieToDelete = new Cookie("username", "");
        cookieToDelete.setMaxAge(0);
        cookieToDelete.setPath("/demo1"); // 注意:路径要与原 Cookie 一致
        resp.addCookie(cookieToDelete);
    }
}

3.4.4 安全属性(重点)

HttpOnly

作用:禁止 JavaScript 访问 Cookie,防止 XSS 攻击。

java
Cookie cookie = new Cookie("session_token", "secret_value");
cookie.setHttpOnly(true);  // JavaScript 无法读取
resp.addCookie(cookie);

攻击场景

javascript
// 如果没有 HttpOnly,恶意脚本可以窃取 Cookie
document.cookie; // 获取所有 Cookie

Secure

作用:Cookie 仅通过 HTTPS 协议传输,防止中间人攻击。

java
Cookie cookie = new Cookie("sensitive_data", "value");
cookie.setSecure(true);  // 仅 HTTPS 传输
resp.addCookie(cookie);

SameSite(现代浏览器支持)

作用:控制 Cookie 在跨站请求中的发送行为,防止 CSRF 攻击。

说明
Strict 完全禁止第三方 Cookie,仅同站请求可发送
Lax 默认级别,部分跨站请求(如 GET)可发送
None 允许所有跨站请求,但必须配合 Secure
java
// Tomcat 8.5+ 支持 SameSite
// 需要通过 Set-Cookie 头手动设置
resp.setHeader("Set-Cookie", "name=value; SameSite=Strict");

📋 实战练习已分离

本节练习内容已提取至:07-conversation-practice.md

包含内容:需求描述、完整实现代码、关键点说明

3.5 实战案例:记录上次访问时间

3.5.1 需求描述

用户访问页面时,显示上一次访问的时间。如果是第一次访问,显示"欢迎首次访问"。

3.5.2 实现代码

java
@WebServlet("/cookie/last-visit")
public class LastVisitServlet extends HttpServlet {

    private static final String COOKIE_NAME = "lastVisit";
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    private static final String CHARSET = "UTF-8";

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        req.setCharacterEncoding(CHARSET);
        resp.setContentType("text/html;charset=" + CHARSET);
        
        PrintWriter out = resp.getWriter();
        
        // 1. 读取上次访问时间
        String lastVisit = getCookieValue(req, COOKIE_NAME);
        
        if (lastVisit != null) {
            // 解码并显示上次访问时间
            String decodedTime = URLDecoder.decode(lastVisit, CHARSET);
            out.println("<h2>欢迎回来!</h2>");
            out.println("<p>您上次访问时间是:" + decodedTime + "</p>");
        } else {
            out.println("<h2>欢迎首次访问!</h2>");
        }
        
        // 2. 更新访问时间
        String currentTime = new SimpleDateFormat(DATE_FORMAT).format(new Date());
        // URL 编码:处理空格等特殊字符
        String encodedTime = URLEncoder.encode(currentTime, CHARSET);
        
        Cookie cookie = new Cookie(COOKIE_NAME, encodedTime);
        cookie.setMaxAge(30 * 24 * 60 * 60); // 30天有效期
        resp.addCookie(cookie);
    }
    
    private String getCookieValue(HttpServletRequest request, String name) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (name.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

3.5.3 关键点说明

  1. URL 编码必要性yyyy-MM-dd HH:mm:ss 格式中的空格需要编码
  2. 持久化存储:设置 MaxAge 使 Cookie 在浏览器关闭后仍然有效
  3. 中文支持:通过 UTF-8 编码支持中文显示

3.6 常见问题与注意事项

3.6.1 Q1:Cookie 可以存储中文吗?

不能直接存储。Cookie 值不支持特殊字符和中文,需要使用 URL 编码:

java
// 存储时编码
String encoded = URLEncoder.encode("张三", "UTF-8");
Cookie cookie = new Cookie("name", encoded);

// 读取时解码
String decoded = URLDecoder.decode(cookie.getValue(), "UTF-8");

3.6.2 Q2:Cookie 有大小限制吗?

限制项 说明
单个 Cookie 约 4KB
每个域名 Cookie 数量 约 20-50 个(浏览器不同)
总 Cookie 数量 约 300-400 个

3.6.3 Q3:如何删除 Cookie?

java
Cookie cookie = new Cookie("name", "");  // 值为空
cookie.setMaxAge(0);                      // 立即过期
cookie.setPath("/demo1");                // 路径必须与原 Cookie 一致
cookie.setDomain(".example.com");        // 域名必须与原 Cookie 一致
resp.addCookie(cookie);

注意:删除 Cookie 时,name、path、domain 必须与原 Cookie 完全一致。

3.6.4 Q4:Cookie 的安全风险

风险 说明 防护措施
XSS 攻击 恶意脚本窃取 Cookie 设置 HttpOnly
中间人攻击 明文传输被截获 设置 Secure,使用 HTTPS
CSRF 攻击 跨站伪造请求 设置 SameSite
信息泄露 Cookie 内容被查看 不存储敏感信息

3.7 本章小结

Cookie 工作流程

diagram
第1次请求 浏览器 > 服务器 (无Cookie) 创建Cookie < Set-Cookie 响应 后续请求 浏览器 > 服务器 (自动携带 Cookie: xxx=xxx 读取Cookie Cookie)
┌─────────────┐         第1次请求          ┌─────────────┐
│   浏览器     │  ──────────────────────>  │    服务器    │
│  (无Cookie)  │                           │  创建Cookie  │
└─────────────┘  <──────────────────────  └─────────────┘
                         Set-Cookie 响应

┌─────────────┐         后续请求           ┌─────────────┐
│   浏览器     │  ──────────────────────>  │    服务器    │
│ (自动携带    │      Cookie: xxx=xxx      │  读取Cookie  │
│   Cookie)   │                           │             │
└─────────────┘                           └─────────────┘

Cookie 优缺点

优点 缺点
减轻服务器压力 存储容量有限(4KB)
可实现跨应用共享 数据类型有限(仅字符串)
可设置长期有效 安全性较低(客户端可见)
配置灵活 每次请求都会携带,增加流量
04 / Section

4. 服务器技术 Session

4.1 Session 基本概念

Session 相当于每个用户在服务器上的保险柜

  • 保险柜里可以存储数据(支持敏感数据)
  • 要打开保险柜需要携带钥匙(JSESSIONID)
  • 钥匙丢了就打不开保险柜

4.2 Session 的工作原理

4.2.1 第一次请求:创建 Session

当第一次调用 request.getSession() 时:

  1. 服务器创建 Session 对象
  2. 生成唯一的 Session ID(JSESSIONID)
  3. 通过响应头 Set-Cookie 将 JSESSIONID 发送给浏览器
http
HTTP/1.1 200
Set-Cookie: JSESSIONID=F1500D1D295B3953DCBBF89AD614F1E6; Path=/demo2; HttpOnly
Content-Length: 0
Date: Mon, 20 Feb 2023 06:55:17 GMT

4.2.2 后续请求:携带 Session ID

浏览器自动在 Cookie 中携带 JSESSIONID:

http
GET http://localhost:8083/demo2/hello HTTP/1.1
Host: localhost:8083
Cookie: JSESSIONID=F1500D1D295B3953DCBBF89AD614F1E6

服务器根据 JSESSIONID 找到对应的 Session 对象。

Session 工作流程
Session 工作流程

4.3 Session 的基本操作

4.3.1 获取 Session

java
// 方式1:常用方式
// 如果不存在则创建,如果存在则返回已有 Session
HttpSession session = request.getSession();

// 方式2:带参数方式
// true:同方式1
// false:不存在则返回 null,不创建新 Session
HttpSession session = request.getSession(false);

4.3.2 存取数据

Session 是一个键值对存储结构:

  • Key:String 类型
  • Value:Object 类型(可存储任意对象)
java
@WebServlet("/session/set")
public class SessionSetServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // 获取 Session(不存在则创建)
        HttpSession session = req.getSession();
        
        // 存储数据
        session.setAttribute("mobile", "18666778899");
        session.setAttribute("userId", 65536);
        session.setAttribute("user", new User("zhangsan", 20));
    }
}
java
@WebServlet("/session/get")
public class SessionGetServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        
        HttpSession session = req.getSession();
        
        // 获取数据
        String mobile = (String) session.getAttribute("mobile");
        Integer userId = (Integer) session.getAttribute("userId");
        User user = (User) session.getAttribute("user");
        
        out.println("UserId: " + userId + "<br>");
        out.println("Mobile: " + mobile + "<br>");
        out.println("User: " + user + "<br>");
    }
}

4.3.3 删除数据

java
// 移除单个属性
session.removeAttribute("mobile");

// 清空所有数据
session.invalidate();  // 同时使 Session 失效

4.4 Session 的生命周期

4.4.1 Session 对象的创建与销毁

事件 触发条件
创建 第一次调用 request.getSession()
销毁 1. 服务器关闭
2. 应用卸载
3. 调用 session.invalidate()
4. 超过有效期(默认30分钟)

4.4.2 数据的生命周期

java
// 设置 Session 有效期(秒)
session.setMaxInactiveInterval(30 * 60);  // 30分钟

// 获取创建时间
long createTime = session.getCreationTime();

// 获取最后访问时间
long lastAccessTime = session.getLastAccessedTime();

4.4.3 服务器重启与 Session 持久化

Tomcat 默认会将 Session 序列化到硬盘,服务器重启后:

  • Session 对象会被反序列化重新加载
  • JSESSIONID 保持不变
  • 但对象引用会改变(不是同一个对象)

注意:IDEA 内置的 Tomcat 可能无法复现此功能,建议在独立 Tomcat 中测试。

📋 实战练习已分离

本节练习内容已提取至:07-conversation-practice.md

包含内容:需求描述、登录Servlet、过滤器、受保护资源、注销功能

4.5 实战案例:用户登录状态管理

4.5.1 需求描述

实现用户登录功能,登录成功后保持登录状态,可访问受保护的资源。

4.5.2 实现代码

登录 Servlet

java
@WebServlet("/user/login")
public class LoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=UTF-8");
        
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        
        // 验证用户(简化示例,实际应查询数据库)
        if ("admin".equals(username) && "123456".equals(password)) {
            // 登录成功,存储用户信息到 Session
            HttpSession session = req.getSession();
            session.setAttribute("currentUser", username);
            session.setAttribute("loginTime", new Date());
            
            resp.getWriter().write("{\"code\":200,\"msg\":\"登录成功\"}");
        } else {
            resp.getWriter().write("{\"code\":401,\"msg\":\"用户名或密码错误\"}");
        }
    }
}

登录状态检查过滤器

java
@WebFilter("/protected/*")
public class AuthFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        
        HttpSession session = req.getSession(false);
        
        // 检查是否已登录
        if (session == null || session.getAttribute("currentUser") == null) {
            resp.setStatus(401);
            resp.getWriter().write("请先登录");
            return;
        }
        
        // 已登录,继续访问
        chain.doFilter(request, response);
    }
}

受保护资源

java
@WebServlet("/protected/profile")
public class ProfileServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        resp.setContentType("text/html;charset=UTF-8");
        HttpSession session = req.getSession();
        
        String user = (String) session.getAttribute("currentUser");
        Date loginTime = (Date) session.getAttribute("loginTime");
        
        resp.getWriter().println("<h2>个人中心</h2>");
        resp.getWriter().println("<p>当前用户:" + user + "</p>");
        resp.getWriter().println("<p>登录时间:" + loginTime + "</p>");
    }
}

注销功能

java
@WebServlet("/user/logout")
public class LogoutServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        HttpSession session = req.getSession(false);
        if (session != null) {
            session.invalidate();  // 使 Session 失效
        }
        
        resp.sendRedirect("/login.html");
    }
}

4.6 常见问题分析

4.6.1 Q1:关闭浏览器,Session 是否被销毁?

:Session 对象没有被销毁,仍然存在于服务器。

原因

  • 浏览器关闭后,内存中的 JSESSIONID Cookie 丢失
  • 再次打开浏览器访问时,没有携带 JSESSIONID
  • 服务器认为是新用户,创建新的 Session

验证方法

java
// 记录下 JSESSIONID
HttpSession session = req.getSession();
System.out.println("Session ID: " + session.getId());

// 关闭浏览器后,手动在请求中携带之前的 JSESSIONID
// 仍然可以获取到原来的 Session 数据

4.6.2 Q2:Session 失效的常见原因

原因 说明 解决方案
跨域 IP 或端口变化,JSESSIONID 无法共享 统一域名、使用反向代理
Cookie 被禁用 浏览器禁用了 Cookie 使用 URL 重写
超时 超过 maxInactiveInterval 调整超时时间
手动失效 调用 invalidate() 检查代码逻辑
服务器重启 未配置 Session 持久化 配置 Session 序列化

4.6.3 Q3:禁用 Cookie 后如何使用 Session?

Session 默认依赖 Cookie,如果 Cookie 被禁用,可以使用 URL 重写

java
// 自动在 URL 后追加 JSESSIONID
String url = resp.encodeURL("/demo2/session/get");
// 结果:/demo2/session/get;JSESSIONID=aaaabc2849ea

resp.sendRedirect(url);

注意:现代应用中,建议要求用户启用 Cookie,URL 重写方式存在安全风险。

4.6.4 Q4:Session 并发问题

当同一个用户在不同浏览器/设备同时登录时:

java
// 方案1:允许同时登录(默认行为)
// 每个浏览器获得不同的 Session

// 方案2:限制同时登录(踢掉之前的)
// 需要配合应用层缓存实现

4.7 本章小结

Session 工作流程

diagram
第1次请求: 浏览器 > 服务器 创建Session < 返回JSESSIONID Set-Cookie 后续请求: 浏览器 > 服务器 (携带 Cookie: JSESSIONID=xxx 查找Session JSESSIONID) 返回数据 <
第1次请求:
┌─────────┐                    ┌─────────┐
│  浏览器  │  ────────────────> │  服务器  │
│         │                    │ 创建Session │
│         │  <──────────────── │ 返回JSESSIONID
└─────────┘   Set-Cookie       └─────────┘

后续请求:
┌─────────┐                    ┌─────────┐
│  浏览器  │  ────────────────> │  服务器  │
│(携带    │   Cookie: JSESSIONID=xxx │ 查找Session │
│JSESSIONID)│                   │ 返回数据   │
└─────────┘  <────────────────  └─────────┘

Session 使用建议

  1. 存储内容:用户 ID、登录状态、购物车等敏感/重要信息
  2. 有效期设置:根据业务场景设置合理的超时时间
  3. 安全性:敏感操作需配合其他验证机制
  4. 内存管理:避免在 Session 中存储大量数据
05 / Section

5. Cookie 与 Session 对比

5.1 核心区别

特性 Cookie Session
存储位置 客户端(浏览器) 服务器端
安全性 较低(可被查看/篡改) 较高(仅在服务器)
存储容量 约 4KB 无硬性限制
数据类型 仅 String 任意 Object
生命周期 可设置长期有效 默认 30 分钟
跨应用 可通过 Domain/Path 共享 局限于当前应用
服务器压力 较高(占用内存)

5.2 使用场景选择

5.2.1 适合使用 Cookie 的场景

  1. 记住用户名:7天免登录勾选框
  2. 用户偏好设置:主题、语言等
  3. 非敏感追踪数据:浏览历史(需用户同意)
  4. 跨应用共享数据:单点登录的 Token

5.2.2 适合使用 Session 的场景

  1. 用户登录状态:保存用户 ID、权限信息
  2. 敏感信息:银行卡号、身份证号等
  3. 购物车数据:电商网站的临时购物车
  4. 验证码:防止表单重复提交

5.3 综合实战:记住登录功能

📋 实战练习已分离

本节练习内容已提取至:07-conversation-practice.md

包含内容:需求描述、登录Servlet、自动登录过滤器、Cookie与Session结合使用

5.3.1 需求分析

实现"记住我"功能:

  • 勾选"记住我":7天内自动登录
  • 未勾选:关闭浏览器后需重新登录

5.3.2 实现代码

java
@WebServlet("/user/login-with-remember")
public class LoginWithRememberServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        String remember = req.getParameter("remember"); // "on" 表示勾选
        
        if (validateUser(username, password)) {
            // 1. 创建 Session(用于本次会话)
            HttpSession session = req.getSession();
            session.setAttribute("user", username);
            
            // 2. 如果勾选"记住我",写入长期 Cookie
            if ("on".equals(remember)) {
                Cookie rememberCookie = new Cookie("remember_token", 
                    generateToken(username));
                rememberCookie.setMaxAge(7 * 24 * 60 * 60); // 7天
                rememberCookie.setHttpOnly(true);
                rememberCookie.setSecure(true);
                rememberCookie.setPath("/");
                resp.addCookie(rememberCookie);
            }
            
            resp.sendRedirect("/home.html");
        } else {
            resp.sendRedirect("/login.html?error=1");
        }
    }
    
    private boolean validateUser(String username, String password) {
        // 实际应查询数据库验证
        return "admin".equals(username) && "123456".equals(password);
    }
    
    private String generateToken(String username) {
        // 实际应使用安全的 token 生成算法
        return username + "_" + System.currentTimeMillis();
    }
}

5.3.3 自动登录过滤器

java
@WebFilter("/*")
public class AutoLoginFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest req = (HttpServletRequest) request;
        HttpSession session = req.getSession(false);
        
        // 已有 Session,无需处理
        if (session != null && session.getAttribute("user") != null) {
            chain.doFilter(request, response);
            return;
        }
        
        // 检查 remember Cookie
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("remember_token".equals(cookie.getName())) {
                    String token = cookie.getValue();
                    String username = validateToken(token);
                    
                    if (username != null) {
                        // Token 有效,自动创建 Session
                        session = req.getSession();
                        session.setAttribute("user", username);
                    }
                    break;
                }
            }
        }
        
        chain.doFilter(request, response);
    }
    
    private String validateToken(String token) {
        // 实际应验证 token 的有效性
        if (token != null && token.startsWith("admin_")) {
            return "admin";
        }
        return null;
    }
}

5.4 数据共享域对比

域对象 作用范围 典型用途
Request 单次请求(含转发) Servlet 和 JSP 间传递数据
Session 单个用户会话 登录状态、用户专属数据
Context 整个应用(所有用户) 全局配置、连接池、缓存

5.4.1 使用示例

java
// Request 域:一次请求有效
req.setAttribute("error", "用户名错误");
req.getRequestDispatcher("/error.jsp").forward(req, resp);

// Session 域:用户会话有效
session.setAttribute("user", user);

// Context 域:应用全局有效
getServletContext().setAttribute("appConfig", config);
06 / Section

6. 安全最佳实践

6.1 Cookie 安全

6.1.1 必设的安全属性

java
Cookie cookie = new Cookie("token", value);

// 1. HttpOnly:防止 XSS 窃取
cookie.setHttpOnly(true);

// 2. Secure:仅 HTTPS 传输
cookie.setSecure(true);

// 3. SameSite:防止 CSRF
cookie.setPath("/");
// 注意:Java Cookie API 不直接支持 SameSite,需手动设置 Header
resp.setHeader("Set-Cookie", 
    "token=" + value + "; HttpOnly; Secure; SameSite=Strict");

6.1.2 敏感信息处理

java
// ❌ 错误:直接在 Cookie 中存储明文密码
Cookie badCookie = new Cookie("password", "123456");

// ✅ 正确:只存储随机生成的 Token
Cookie goodCookie = new Cookie("session_token", 
    UUID.randomUUID().toString());
goodCookie.setHttpOnly(true);
goodCookie.setSecure(true);

6.2 Session 安全

6.2.1 安全建议

  1. java
     session.setMaxInactiveInterval(15 * 60); // 15分钟
  1. java
     // 防止 Session Fixation 攻击
     request.changeSessionId();
  1. java
     // 修改密码、转账等操作需再次验证
     if (!verifyPasswordAgain(req)) {
         resp.sendError(403);
         return;
     }
  1. java
     // 注销时使 Session 失效
     session.invalidate();

6.3 常见问题排查

6.3.1 问题1:Session 数据丢失

排查步骤

  1. 检查浏览器是否禁用了 Cookie
  2. 检查域名/端口是否变化(跨域)
  3. 检查 Session 是否超时
  4. 检查是否调用了 invalidate()

6.3.2 问题2:Cookie 无法写入

排查步骤

  1. 检查响应是否已提交(Cookie 需在响应前设置)
  2. 检查 Path 是否匹配
  3. 检查 Domain 设置是否正确
  4. 检查浏览器隐私设置

6.3.3 问题3:中文乱码

java
// 存储时编码
Cookie cookie = new Cookie("name", 
    URLEncoder.encode("张三", "UTF-8"));

// 读取时解码
String name = URLDecoder.decode(cookie.getValue(), "UTF-8");
07 / Section

7. 本章总复盘

7.1 核心知识点回顾

7.1.1 Cookie

  • 客户端技术,存储在浏览器
  • 通过请求头 Cookie 携带,响应头 Set-Cookie 设置
  • 容量限制 4KB,仅支持字符串
  • 重要安全属性:HttpOnly、Secure、SameSite

7.1.2 Session

  • 服务器技术,存储在服务端
  • 通过 JSESSIONID 关联客户端
  • 无容量限制,支持任意对象
  • 默认有效期 30 分钟

7.1.3 选择原则

场景 技术选择
非敏感信息、长期存储 Cookie
敏感信息、临时存储 Session
用户登录状态 Session + Cookie(JSESSIONID)
记住登录 Cookie(Token)+ Session

7.2 下一章预告

下一章将学习 Filter 和 Listener,了解如何:

  • 使用 Filter 实现统一权限验证
  • 使用 Listener 监听应用生命周期事件
  • 结合 Session 实现更完善的用户认证体系
08 / Section

8. 常见问题 FAQ

8.1 Q1:Cookie 和 Session 可以单独使用吗?

:可以,但通常配合使用效果更好。

  • 只用 Cookie:适合存储非敏感偏好设置
  • 只用 Session:需要配合其他机制传递 Session ID
  • 配合使用:最常用,Session 存敏感数据,Cookie 存标识

8.2 如何防止 Session 被劫持?

  1. 使用 HTTPS 防止中间人攻击
  2. 设置 HttpOnly 防止 XSS 窃取
  3. 绑定 IP/UserAgent 验证
  4. 设置合理的超时时间
  5. 重要操作二次验证

8.3 分布式环境下 Session 如何处理?

  1. Session 复制:Tomcat 集群同步(小集群适用)
  2. Session 共享:使用 Redis/Memcached 集中存储
  3. JWT Token:无状态认证,服务端不存储 Session
  4. Sticky Session:负载均衡绑定特定服务器

8.4 浏览器隐私模式对 Cookie/Session 的影响?

  • 隐私模式下 Cookie 不会持久化到硬盘
  • 关闭隐私窗口后所有 Cookie 被清除
  • Session 在服务器端仍然存在,只是客户端丢失了 JSESSIONID

8.5 如何调试 Cookie/Session 问题?

  1. 使用浏览器开发者工具查看 Cookie
  2. 使用 ApiFox/Postman 模拟请求
  3. 在服务端打印 Session ID 和属性
  4. 使用抓包工具(Wireshark、Fiddler)分析请求头