S01-06 Servlet-会话-Cookie
[TOC]
机制概述
Cookie 是由 Web 服务器生成并由客户端浏览器保存在本地的小型键值对数据,主要用于在无状态的 HTTP 协议下维系客户端与服务端的上下文会话。
本质定义
Cookie 是存储在浏览器端的轻量级文本信息片段。它作为客户端状态维护的载体,在后续请求中由浏览器自动附带发送至目标服务端,实现客户端身份的标记与追踪。
产生背景
协议无状态
HTTP 协议具有无状态(Stateless)特性。服务器处理完单次项目说明请求并返回响应后,连接即告结束,服务器不会记录该客户端的访问历史或上下文信息。
状态维系诉求
当用户在电商网站浏览商品或在系统中流转多个页面时,系统需要辨别多次离散的 HTTP 请求是否来自同一个访问主体。为了在不改变 HTTP 协议底层架构的前提下完成会话追踪,引入了让客户端代为保存状态标识的 Cookie 机制。
流转机制
Cookie 的运行依托于 HTTP 报文头部的协同交互:
浏览器初次向 Web 服务器发起普通网络请求。
服务器处理业务逻辑后,在 HTTP 响应头中注入
Set-Cookie字段,下发需要存储的键值数据。浏览器接收到响应,将
Set-Cookie中的数据暂存至客户端内存或本地存储介质。浏览器在后续向符合匹配规则的地址发起新请求时,自动将本地保存的数据附加在 HTTP 请求头的
Cookie字段中回传。服务器从请求头中解析 Cookie 键值,从而识别出请求的来源与会话上下文。

基础操作
在 JavaWeb 开发中,Cookie 的基础操作涵盖从服务端创建、下发给客户端,到后续请求的接收、修改与销毁全过程。
创建发送
实例化对象
在 Servlet 环境中,通过 Cookie 类的构造函数来定义一条键值对数据。创建时键(Name)与值(Value)均为字符串类型,且默认值必须为 ASCII 字符。
下发客户端
服务端通过 new Cookie() 创建好 Cookie 实例后,调用 HTTP 响应对象的注入方法 addCookie(),将其序列化并写入响应头的 Set-Cookie 字段下发至浏览器。
// 1. 实例化 Cookie 对象并指定键值对
Cookie cookie = new Cookie("userToken", "tk_98765");
// 2. 将 Cookie 写入响应头以通知客户端存储
response.addCookie(cookie);

获取读取
读取数组
当浏览器再次向服务器发起请求时,会自动在请求头中附带符合条件的 Cookie 数据。服务端通过请求对象将其统一读取为数组。
服务端调用获取方法
request.getCookies(),从请求头中提取 Cookie 数组。进行非空校验,若客户端为首次访问或未携带 Cookie,该方法返回
null。遍历数组并校验目标键名,提取对应存储的业务值。
提取数值
通过遍历数组,定位到对应的 Cookie 实例后,使用对应方法获取键与值。
// 从请求对象中提取客户端提交的所有 Cookie 数组
Cookie[] cookies = request.getCookies();
if (cookies != null) {
// 遍历数组查找指定名称的键值
for (Cookie c : cookies) {
if ("userToken".equals(c.getName())) {
// 提取目标 Cookie 的具体字符串内容
String token = c.getValue();
break;
}
}
}修改更新
覆盖机制
HTTP 协议本身并不支持直接在客户端修改已有 Cookie 的指令。修改 Cookie 的本质是利用“同名覆盖机制”:服务端下发一个具有相同名称(Name)、相同路径(Path)以及相同域名(Domain) 的全新 Cookie,浏览器接收后会自动将旧值替换为新值。
执行覆盖
在服务端重新构造同名 Cookie,指定新数值与相同路径后,再次添加至响应头。
// 1. 创建同名 Cookie 载入新值实现覆盖
Cookie updateCookie = new Cookie("userToken", "tk_new_12345");
// 2. 保持路径一致以确保覆盖生效并推入响应
updateCookie.setPath("/");
response.addCookie(updateCookie);销毁清除
销毁原理
浏览器对 Cookie 的删除是由服务端下发的生命周期指令驱动的。服务端无法直接删除客户端磁盘或内存中的文件,但可以通过将一个同名、同作用域的 Cookie 的最大存活时间设置为 0 秒,由浏览器在收到响应后立刻执行销毁操作。
实例化一个与待删除项同名的 Cookie 对象。
确保路径(Path)和域名(Domain)与原 Cookie 完全一致。
设置最大存活时间为 0 秒。
加入响应头回传给客户端,客户端触发删除逻辑。
执行清除
通过生命周期归零完成清除操作。
// 1. 创建同名同路径 Cookie 准备注销
Cookie deleteCookie = new Cookie("userToken", "");
// 2. 将生命周期设置为 0 秒通知浏览器立即删除
deleteCookie.setMaxAge(0);
// 3. 保持路径一致
deleteCookie.setPath("/");
// 4. 加入响应头回传给客户端,客户端触发删除逻辑
response.addCookie(deleteCookie);中文乱码
在 JavaWeb 中,Cookie 的中文乱码问题源于底层 HTTP 协议对报文头字符集的严格限制,直接存取非 ASCII 字符会触发容器异常或数据失真。
产生原因
规范约束
根据 RFC 6265 等 HTTP 协议规范,HTTP 头部字段(包括 Set-Cookie 与 Cookie)仅允许传输 US-ASCII 字符集(编码范围 32 到 126)。中文字符在 Unicode 编码中远远超出此范围,因此无法直接作为合法的 Header 字符传输。
容器差异
不同的 Servlet 容器与版本对非 ASCII 字符的处理机制不同:
- 早期容器(如 Tomcat 7 及以前):内置校验较为严苛,如果向 Cookie 中直接写入中文或空格,会直接抛出异常
java.lang.IllegalArgumentException: Control character in cookie value or attribute并中断响应。 - 现代容器(如 Tomcat 8 及以上):虽然默认采用更加灵活的 RFC 6265 标准解析,但若未显式指定编码方式,数据在跨浏览器、跨操作系统以及服务端反向代理流转时,极易因默认字符集不一致导致解码错乱。
解决机制
编解码方案
解决中文乱码的标准方案是使用 URL 编码(百分号编码,Percent-Encoding)。在写入前将中文字符转换为符合 ASCII 规范的十六进制编码串(例如将“张三”转换为 %E5%BC%A0%E4%B8%89),在服务端读取后再逆向解码还原。
处理流程
服务端获取业务中文字符串,使用
URLEncoder指定 UTF-8 编码集转换为 ASCII 安全字符。将转码后的字符串存入
Cookie并注入 HTTP 响应头。浏览器将该安全字符串保存在本地,后续请求原样回传至服务端。
服务端从请求头中提取该字符串,使用
URLDecoder配合相同的 UTF-8 字符集还原原始中文。
代码实现
存取转码
在读写流程中,必须保证编码与解码两端使用的字符集完全一致,工程中统一推荐采用 StandardCharsets.UTF_8。
// 1. 将中文数据使用 UTF-8 进行 URL 编码以符合 ASCII 规范
String encodedName = URLEncoder.encode("张三", StandardCharsets.UTF_8);
Cookie cookie = new Cookie("userName", encodedName);
cookie.setPath("/");
response.addCookie(cookie);
// 2. 从 Cookie 获取编码串并用相同字符集解码还原中文
String decodedName = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8);API: URLEncoder
StringURLEncoder.encode():(String s, Charset charset),指定字符集转义。使用指定的Charset实例将字符串转义为application/x-www-form-urlencoded格式。Java 10 引入的类型安全标准方法,无受检异常。StringURLEncoder.encode():(String s, String enc),指定字符集名称转义。使用指定的字符编码名称将字符串转换为表单转义格式。当编码名称不被系统支持时抛出UnsupportedEncodingException。
注意事项:
- 单参数
URLEncoder.encode(String s)会隐式依赖操作系统的默认字符编码(如 Windows 早期版本可能为 GBK,Linux 环境多为 UTF-8),会导致同一程序跨平台部署时产生乱码问题,严禁在生产环境中使用。URLEncoder.encode(String s, Charset charset)是目前最高优先级的推荐用法。相比使用String enc传参,它避免了字符串拼写错误带来的潜在异常,并且在传入null时会由底层严格执行Objects.requireNonNull校验直接抛出NullPointerException。
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class EncodeOverloadDemo {
public static void main(String[] args) {
String content = "Java 网络编程 & 微服务";
// 1. 推荐:JDK 10+ 标准 Charset 重载(类型安全且无受检异常)
String safeEncoded = URLEncoder.encode(content, StandardCharsets.UTF_8);
System.out.println("Charset 编码结果: " + safeEncoded);
// 2. 兼容:JDK 1.4+ 字符串字符集名称重载(需处理 UnsupportedEncodingException)
try {
String legacyEncoded = URLEncoder.encode(content, "UTF-8");
System.out.println("字符串编码名结果: " + legacyEncoded);
} catch (UnsupportedEncodingException e) {
System.err.println("字符集不支持: " + e.getMessage());
}
// 3. 淘汰:JDK 1.0 单参数方法(已弃用,依赖平台默认字符集)
@SuppressWarnings("deprecation")
String deprecatedEncoded = URLEncoder.encode(content);
System.out.println("已弃用方法结果: " + deprecatedEncoded);
}
}API: URLDecoder
StringURLDecoder.decode():(String s, Charset charset),指定字符集解码。使用指定的Charset实例将application/x-www-form-urlencoded格式的文本反转义。Java 10 引入的标准强类型方法,无受检异常。StringURLDecoder.decode():(String s, String enc),指定字符集名称解码。使用指定的字符编码名称反转义字符串。若编码名称不受支持,抛出受检异常UnsupportedEncodingException。
注意事项:
- 单参数方法
URLDecoder.decode(String s)会依赖操作系统默认编码(如 Windows 中文环境可能为 GBK,Linux 环境一般为 UTF-8),引发环境行为不一致问题,严禁在生产中使用。- 当待解码文本包含不符合
%xy规范的转义序列时,方法会直接抛出未受检异常IllegalArgumentException,在接收不可信外部输入时需进行前置校验或异常兜底。- 生产环境推荐统一采用
URLDecoder.decode(String s, StandardCharsets.UTF_8),避免受检异常捕获模板代码,并在传入null时遵循Objects.requireNonNull抛出NullPointerException。
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public class DecodeOverloadDemo {
public static void main(String[] args) {
String encodedText = "Java+%E5%AE%89%E5%85%A8%E8%A7%A3%E7%A0%81";
// 1. 推荐:JDK 10+ 标准 Charset 重载(类型安全且无受检异常)
String safeDecoded = URLDecoder.decode(encodedText, StandardCharsets.UTF_8);
System.out.println("Charset 解码结果: " + safeDecoded);
// 2. 兼容:JDK 1.4+ 字符串字符集名称重载(需显式捕获受检异常)
try {
String legacyDecoded = URLDecoder.decode(encodedText, "UTF-8");
System.out.println("字符串编码名解码结果: " + legacyDecoded);
} catch (UnsupportedEncodingException e) {
System.err.println("字符集不支持: " + e.getMessage());
}
// 3. 淘汰:JDK 1.0 单参数方法(已弃用,依赖平台默认字符集)
@SuppressWarnings("deprecation")
String deprecatedDecoded = URLDecoder.decode(encodedText);
System.out.println("已弃用方法解码结果: " + deprecatedDecoded);
}
}核心属性
在 HTTP 协议和 JavaWeb 规范中,Cookie 并不单纯是一个键值对,它还附带了一系列元数据属性。这些属性共同决定了 Cookie 的存活时长、访问范围以及安全级别。
键值定义 Name/Value
属性说明
键(Name)与值(Value) 是 Cookie 最基本的数据载体。浏览器通过 Name 区分不同条目,通过 Value 传递具体数据。
行为规则
只读名称:Cookie 实例创建后,其 Name 在该对象中是只读的,没有对应的 setter 方法。
数据修改:Value 可以随时通过 setter 方法更新。
字符范围:根据规范,Cookie 的值仅支持 ASCII 码字符。如果包含中文、空格、逗号等字符,写入前必须进行 URL 编码。
java// 获取当前 Cookie 的名称(只读属性) String name = cookie.getName(); // 动态修改 Cookie 存储的值 cookie.setValue("new_token_value");
存活期限 Max-Age/Expires
属性说明
存活期限由 Max-Age(以及早期标准中的 Expires)属性决定,用于控制 Cookie 在客户端保存的时间长短。
行为规则
在 Java 中通过 setMaxAge(int expiry) 进行设置,参数单位为秒:
大于 0:持久化存储。浏览器将 Cookie 写入磁盘文件中,在指定秒数后失效。小于 0(默认值 -1):会话级存储。Cookie 仅保存在浏览器运行内存中,当用户关闭整个浏览器时自动销毁。等于 0:立即失效。服务端用于通知浏览器让本地对应的 Cookie 立即删除。java// 设置生命周期为持久化(存活 1 小时) cookie.setMaxAge(3600); // 设置生命周期为会话级(关闭浏览器失效) cookie.setMaxAge(-1); // 设置生命周期为 0(通知浏览器立即失效) cookie.setMaxAge(0);
注意事项:
- 网络请求生效判定:Cookie 失效的核心判定体现在网络请求层面。无论是到达指定存活秒数后自然失效,还是服务端下发
setMaxAge(0)主动清除,只要判定为过期失效,浏览器再次发起网络请求时将不再在Cookie请求头中携带该记录。- 本地存储与记录残留:“失效”并不等同于浏览器会立刻从底层的本地存储介质(或开发者工具 Application/Storage 的 Cookies 存储面板)中将其物理抹除。在指定秒数后失效时,浏览器的本地存储中可能依然存在该条 Cookie 记录,何时真正物理清除取决于浏览器内核的垃圾回收(GC)与清理机制,但此时该记录在网络传输与业务逻辑上已彻底失效。
验证示例
在 Servlet 服务端验证 Cookie 是否失效时,关键在于理解:HTTP 请求头只回传 Cookie: name=value 键值对,不包含存活期元数据。从 request.getCookies() 获取的 Cookie 对象,其 cookie.getMaxAge() 默认永远为 -1。
因此,服务端验证 Cookie 是否失效的标准是检查当前请求是否依然携带该 Cookie:
- 浏览器在 Cookie 到期后会自动停止在网络请求头中携带该记录;
- 服务端遍历
request.getCookies()若未找到目标键名,即判定该 Cookie 已失效或未下发。
下发短生命周期 Cookie(
/cookie/set):设置setMaxAge(10),下发存活 10 秒的 Cookie 便于观察。java@WebServlet("/cookie/set") public class SetCookieServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); // 1. 创建待验证的 Cookie 实例 Cookie testCookie = new Cookie("auth_status", "ACTIVE_LOGIN"); // 2. 设置存活周期为 10 秒(便于快速测试超时失效) testCookie.setMaxAge(10); // 3. 统一设置根路径,避免不同路径导致的作用域不匹配 testCookie.setPath("/"); // 4. 写入响应头 Set-Cookie 下发给浏览器 response.addCookie(testCookie); response.getWriter().println("Cookie 已下发,有效时间 10 秒!"); } }检验 Cookie 是否有效(
/cookie/check):从请求中提取并判定是否存在。java@WebServlet("/cookie/check") public class CheckCookieServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); Cookie targetCookie = null; // 1. 从请求中获取所有的 Cookie(客户端未携带时返回 null) Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie c : cookies) { if ("auth_status".equals(c.getName())) { targetCookie = c; break; } } } // 2. 根据是否存在判定是否在有效期内 if (targetCookie != null) { out.println("【状态正常】Cookie 仍处于有效期内,Value = " + targetCookie.getValue()); } else { out.println("【判定失效】未检测到 Cookie,说明已自然超时失效或被销毁!"); } } }主动清除 Cookie(
/cookie/clear):设置setMaxAge(0)模拟主动注销。java@WebServlet("/cookie/clear") public class ClearCookieServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); // 1. 构造同名 Cookie,保持 Path 一致 Cookie deleteCookie = new Cookie("auth_status", ""); deleteCookie.setPath("/"); // 2. 将生命周期归零,通知浏览器立即删除 deleteCookie.setMaxAge(0); // 3. 写入响应下发执行物理销毁 response.addCookie(deleteCookie); response.getWriter().println("已下发 Max-Age=0 指令,Cookie 立即失效!"); } }验证流程与观察指标:
验证阶段 测试操作 浏览器行为(F12 抓包) 服务端判定结果 1. 初始下发 访问 /cookie/set响应标头包含 Set-Cookie: auth_status=...; Max-Age=10下发成功,客户端开始 10 秒倒计时 2. 存活期内访问 10 秒内访问 /cookie/check请求标头携带 Cookie: auth_status=ACTIVE_LOGIN状态正常,Cookie 判定为有效 3. 超时自然失效 10 秒后刷新 /cookie/check请求标头已不再携带 auth_status判定失效(未检测到 Cookie) 4. 主动提前失效 访问 /cookie/clear后立即访问/cookie/check接收到 Max-Age=0指令后,后续请求不再携带判定失效(已被主动清除)
匹配路径 Path
属性说明
Path 属性用于限制浏览器在向哪些服务端 URL 路径发起请求时才携带该 Cookie。
行为规则
前缀匹配:Path 遵循最长前缀匹配原则。例如设置为
/app,则请求/app、/app/user、/app/order都会携带;而请求/system时则不会携带。默认作用域:若未显式指定,默认取生成该 Cookie 的 Servlet 所在的相对目录。在实际项目中,为了实现站点共享,通常统一配置为根路径
/。java// 设置限制路径为根路径,允许全站所有路径访问 cookie.setPath("/");
验证示例
验证 Cookie 的 Path 路径匹配规则时,关键在于理解:Cookie 的路径过滤是由客户端浏览器在发起请求时主动裁决的。当浏览器向服务器发送 HTTP 请求时,会比对目标 URL 是否属于已保存 Cookie 的 Path 及其子路径(前缀匹配规则),只有匹配成功的 Cookie 才会放入 Cookie 请求头。
假设当前应用上下文根路径为 /cs(由 request.getContextPath() 获取):
下发不同 Path 的 Cookie(
/cookie/path/set):为不同 Cookie 设置不同范围的有效路径。java@WebServlet("/cookie/path/set") public class SetPathCookieServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); // 1. 创建两个待测试的 Cookie Cookie cookie = new Cookie("address", "bj"); Cookie cookie2 = new Cookie("salary", "20000"); // 2. 设置不同有效路径 // cookie 有效路径为当前应用根路径(/cs),对当前工程所有资源生效 cookie.setPath(request.getContextPath()); // cookie2 有效路径为限定子路径(/cs/aaa),仅对 /aaa 及其下级资源生效 cookie2.setPath(request.getContextPath() + "/aaa");// 3. 写入响应头,保存到客户端浏览器 response.addCookie(cookie); response.addCookie(cookie2); response.getWriter().println("已成功下发不同 Path 的 Cookie!<br>" + "address -> Path: " + request.getContextPath() + "<br>" + "salary -> Path: " + request.getContextPath() + "/aaa"); } } 验证平级或父级路径读取(
/bbb):请求非/aaa的其他应用路径。java@WebServlet("/bbb") public class CheckOtherPathServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<h3>访问路径:/bbb</h3>"); Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie c : cookies) { out.println("收到 Cookie:" + c.getName() + " = " + c.getValue() + "<br>"); } } else { out.println("未检测到任何 Cookie!"); } // 预期结果:仅输出 address=bj,浏览器不会携带 salary } }验证限定子路径读取(
/aaa):请求属于/aaa作用域的目标子路径。java@WebServlet("/aaa") public class CheckAaaPathServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<h3>访问路径:/aaa</h3>"); Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie c : cookies) { out.println("收到 Cookie:" + c.getName() + " = " + c.getValue() + "<br>"); } } else { out.println("未检测到任何 Cookie!"); } // 预期结果:同时输出 address=bj 与 salary=20000 } }验证流程与观察指标:
验证阶段 / 请求路径 浏览器行为(F12 抓包) 服务端接收结果( request.getCookies())路径匹配规则解析 1. 初始下发
访问/cs/cookie/path/set响应标头下发两条 Set-Cookie:
•address=bj; Path=/cs
•salary=20000; Path=/cs/aaa客户端成功保存两个 Cookie 及其 Path 作用域 两个 Cookie 分别绑定了不同的前缀匹配路径 2. 访问平级路径
访问/cs/bbb请求标头仅携带: Cookie: address=bj仅接收到 address,未检测到salary/cs/bbb命中/cs前缀,但不属于/cs/aaa前缀,浏览器自动屏蔽salary发送3. 访问限定子路径
访问/cs/aaa或/cs/aaa/xxx请求标头同时携带: Cookie: address=bj; salary=20000成功接收到 address和salary两个 Cookie/cs/aaa同时命中/cs和/cs/aaa前缀,符合匹配规则,浏览器全部携带
匹配域名 Domain
属性说明
Domain 属性用于指定 Cookie 能够生效的主机名范围,常用于单点登录(SSO)与跨子域数据共享。
行为规则
默认范围:未设置时,Cookie 仅绑定到生成它的具体域名(如
login.example.com),子域名之间互不透传。跨子域共享:若显式设置为主域名(如
.example.com),则该域名下的所有子域(如a.example.com、b.example.com)在发起请求时均会附带此 Cookie。同源限制:出于安全考量,服务端不能将 Domain 篡改为与当前主机名无隶属关系的第三方域名。
java// 设置共享主域名,允许该顶级域名下的所有子域名访问 cookie.setDomain(".example.com");
脚本隔离 HttpOnly
属性说明
HttpOnly 属性用于限制客户端运行环境对 Cookie 的访问权限。
行为规则
脚本阻断:当声明了
HttpOnly后,浏览器端运行的客户端脚本(如 JavaScript 的document.cookie)将无法读取该条目。安全价值:该属性是防御跨站脚本攻击(XSS) 窃取身份令牌的关键屏障。即便页面被恶意脚本注入,攻击者也无法通过 DOM 接口直接盗取身份凭证。
java// 开启脚本隔离,防止客户端 JavaScript 窃取凭证 cookie.setHttpOnly(true);
传输加密 Secure
属性说明
Secure 属性用于限制 Cookie 传输过程所依赖的网络协议类型。
行为规则
信道要求:声明了
Secure属性的 Cookie,浏览器仅在建立 TLS/SSL(即 HTTPS)加密信道时才会将其附加到请求头中回传。防嗅探窃听:在明文 HTTP 连接下,浏览器会自动屏蔽该 Cookie 的发送,防止网络传输中的中间人监听和抓包泄露。
java// 限制仅在 HTTPS 加密信道中传输 cookie.setSecure(true);
跨站策略 SameSite
属性说明
SameSite 属性用于控制 Cookie 是否在跨站(Cross-Site)发起的网络请求中被携带,是目前主流浏览器防范跨站请求伪造(CSRF) 的核心手段。
策略取值
Strict(严格):完全禁止跨站携带。只要请求是从第三方站点发起的(哪怕是从外部网页点击一个指向本站的超链接),都不会发送该 Cookie。Lax(宽松):大多数现代浏览器的默认值。允许从外部站点通过“顶级导航且为安全方法”(如用户点击 GET 链接跳转)携带 Cookie,但禁止跨站 POST、iframe 或 Ajax 请求携带。None(无限制):允许在跨站请求中携带 Cookie,但现代浏览器强制要求必须同时开启Secure属性(即必须运行在 HTTPS 信道)。java// 手动组装响应头设置 SameSite 属性以防范 CSRF 攻击 response.setHeader("Set-Cookie", "token=abc123; Path=/; Secure; HttpOnly; SameSite=Lax");
JSESSIONID
是 JavaWeb Servlet 容器(如 Tomcat、Jetty 等)用于在无状态的 HTTP 协议之上追踪用户会话状态的默认 Cookie 名称,其核心作用是作为客户端与服务端 JSESSIONIDHttpSession 之间的映射凭证。
本质定义
凭证定位
在 JavaWeb 架构中,用户的业务数据(如登录状态、购物车)保存在服务端的 HttpSession 对象中,而客户端浏览器并不保存这些数据。为了区分请求属于哪一个用户,服务端为每个会话分配一个全站唯一的字符串标识,即 JSESSIONID。
载体关系
JSESSIONID 本质上就是一条普通的 Cookie。键名为固定的 JSESSIONID,键值为容器生成的随机字符串,通过标准的 HTTP 请求头与响应头在客户端与服务端之间传递。
产生机制
触发时机
只有在服务端代码显式或隐式调用会话获取方法时,Servlet 容器才会生成 JSESSIONID:
- 调用
request.getSession()或request.getSession(true)。 - 访问未声明
<%@ page session="false" %>的 JSP 页面(JSP 默认会自动创建 Session)。 - 若仅请求静态资源(HTML、CSS、图片)或纯接口且未触碰 Session,容器不会主动创建会话,也不会下发
JSESSIONID。
生成示例
容器内部通过安全随机数算法生成高强度的伪随机字符串,确保会话标识不可预测。
// 获取或创建当前请求关联的服务端会话对象
HttpSession session = request.getSession();
// 读取容器自动分配的唯一会话标识符
String sessionId = session.getId();交互流程
流转步骤
JSESSIONID 的生命周期依托于标准的 HTTP 报文流转:
客户端浏览器首次向服务器发送请求,请求头中未携带任何会话信息。
服务端业务代码执行
request.getSession(),容器检测到请求中无有效标识,在内存中创建新的HttpSession实例并生成唯一的JSESSIONID。服务端在 HTTP 响应头中注入
Set-Cookie: JSESSIONID=...; Path=/; HttpOnly并回传给浏览器。浏览器接收到响应后,将该 Cookie 保存在客户端内存中。
客户端在后续向该站点发起请求时,浏览器自动在 HTTP 请求头中附加
Cookie: JSESSIONID=...。服务端容器拦截器从请求头中提取该值,在内部的会话管理器(SessionManager)中检索匹配的
HttpSession,从而恢复用户上下文。

核心特性
默认存活期
Servlet 容器默认下发的 JSESSIONID Cookie 的 Max-Age 为 -1,属于会话级 Cookie。它仅驻留在浏览器的运行内存中,当用户完全关闭浏览器后即被清除。再次打开浏览器发起请求时,服务端会视其为新访客并重新生成不同的 JSESSIONID。
自动化管理
在标准 JavaWeb 开发中,JSESSIONID 的创建、写入响应头、从请求头解析以及过期清理完全由 Servlet 容器自动化处理,开发者无需手动执行 new Cookie("JSESSIONID", ...)。
安全属性
现代 Servlet 容器默认会为 JSESSIONID 启用 HttpOnly 属性,禁止客户端 JavaScript 读取该值,以此防范跨站脚本(XSS)窃取会话令牌。
降级方案
URL 重写
若客户端浏览器在安全设置中禁用了 Cookie,浏览器将拒绝保存和回传 JSESSIONID,导致每次请求都会创建全新的 Session。针对该场景,Servlet 规范提供了 URL 重写作为降级补偿方案:
- 机制:通过在 URL 路径后追加参数(如
/index.jsp;jsessionid=A1B2C3D4...)直接传递会话标识。 - 实现:服务端在输出超链接或重定向地址时,通过
response.encodeURL(url)或response.encodeRedirectURL(url)动态判断是否需要将jsessionid拼接进 URL 中。
安全加固
由于 Cookie 存储在不可控的客户端浏览器中,并在每次网络通信时自动随请求回传,极易成为跨站脚本攻击、网络嗅探监听和跨站伪造请求的核心攻击目标。对 Cookie 进行全面的安全加固,是保障现代 Web 系统会话安全的关键防线。

跨站脚本攻击
脚本盗取:
在跨站脚本攻击(XSS)中,攻击者通过在页面注入恶意 JavaScript 代码,直接调用 document.cookie 窃取用户的会话令牌(Session ID 或 Auth Token),进而冒充受害者身份登录系统。
防护原理:
通过开启 HttpOnly 标志位,浏览器将对客户端脚本彻底屏蔽该 Cookie。无论是 document.cookie 读取操作,还是通过 DOM 注入的脚本逻辑,均无法获取其键值内容。
服务端下发包含
HttpOnly的响应头。浏览器将该属性写入本地 Cookie 存储仓库。
页面脚本尝试读取该 Cookie 时返回空或过滤后的数据。
浏览器向服务端发送网络请求时,底层仍会自动在请求头中正常附加该 Cookie。
配置方式:
在 Servlet 中直接通过 API 激活该属性:
// 开启脚本隔离以防止 XSS 恶意脚本直接读取会话凭证
cookie.setHttpOnly(true);网络嗅探监听
网络嗅探:
当客户端与服务端通过明文 HTTP 协议通信时,传输数据在局域网、公共 Wi-Fi 或中间路由节点处于暴露状态。中间人攻击者可以通过抓包工具截获包含 Cookie 的 HTTP 报文,造成凭证泄露。
防护原理:
通过声明 Secure 属性,强制规定该 Cookie 只能在建立 TLS/SSL(即 HTTPS)加密通道时才能通过网络发送。当客户端处于非加密的 HTTP 环境时,浏览器会自动拒绝发送该 Cookie。
配置方式:
在 Java 代码中设置传输加密标识:
// 开启安全传输标志,强制仅在 HTTPS 加密信道中传输
cookie.setSecure(true);传输升级:
单纯设置 Secure 无法阻止用户首次误通过 HTTP 访问网站。配合在服务端配置 HSTS(HTTP 严格传输安全)响应头,可以强制浏览器在本地将所有 HTTP 请求内部自动重定向为 HTTPS:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload跨站伪造请求
跨站伪造:
在跨站请求伪造(CSRF)中,受害者在已登录目标站点的情况下访问了恶意网站。恶意网站诱导浏览器向目标站点发起伪造请求,浏览器会自动携带该站点的有效 Cookie,导致服务端将恶意操作误认为是受害者的自主意图。
<!-- evil.com 上的隐藏代码 -->
<form id="csrfForm" action="http://bank.com/api/transfer" method="POST">
<input type="hidden" name="toAccount" value="hacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>
// 页面加载完成后自动提交表单
document.getElementById('csrfForm').submit()
</script>属性取值:
SameSite 属性用于约束 Cookie 在跨站发起请求时的携带行为,从源头阻断 CSRF 攻击。该属性支持三种策略模式:
Strict:严格模式。只要请求来源不是当前同源站点,浏览器均拒绝携带该 Cookie(包括用户在外部网站点击链接跳转)。Lax:宽松模式(现代浏览器默认值)。绝大部分跨站请求(如表单提交、POST 请求、iframe 嵌套、Ajax)均被阻止携带,仅允许顶级导航且为幂等操作(如点击普通 GET 超链接跳转)携带。None:关闭限制。跨站请求均可正常携带,但现代主流浏览器强制要求必须同时声明Secure属性,否则整条 Cookie 将被直接丢弃。
配置方式:
在较旧版本的 Servlet 规范中,可通过响应头手动拼装注入包含 SameSite 的标准头信息:
// 手动配置包含 SameSite 属性的安全 Cookie 响应头
response.setHeader("Set-Cookie", "sessionId=token_val; Path=/; Secure; HttpOnly; SameSite=Strict");子域篡改主域
覆盖篡改:
在子域名隔离不完全的场景下,攻击者若控制了某个安全性较弱的二级子域名(如 test.example.com),可通过设置作用域为 .example.com 强行下发同名 Cookie,覆盖甚至篡改主站用户的合法会话数据(即 Cookie Tossing 攻击):
- 恶意投掷:攻击者在控制的子域向父域下发同名 Cookie(如
Set-Cookie: JSESSIONID=fake_token; Domain=.example.com; Path=/)。 - 请求混淆:受害者访问主站
example.com时,浏览器将同时携带主域合法 Cookie 与子域投掷的 Cookie(Cookie请求头只发键值对且不附带Domain元信息)。 - 服务端误读:主站服务端解析 Cookie 时,通常仅读取首个遇到的同名键值,导致合法会话被恶意 Cookie 抢占,引发会话固定(Session Fixation)或登录态混乱。
前缀约束:
为了防御 Cookie Tossing(子域篡改主域) 以及在非加密连接下被恶意覆盖的问题,现代浏览器引入了 Cookie Prefixes 规范。当 Cookie 名称以特定前缀开头时,浏览器会对其属性施加强制校验。
__Secure-前缀:Cookie 名称必须以__Secure-开头,并且必须同时包含Secure属性。__Host-前缀:安全级别最高的约束。必须满足以下四个条件,否则浏览器将拒绝接收与保存:- 必须包含
Secure属性(仅限 HTTPS)。 - 必须运行在安全源下。
- 路径(Path)必须显式指定为根路径
/。 - 严禁指定域名(Domain)属性(锁定在当前主机名,子域名无法覆盖或共享)。
- 必须包含
配置方式:
使用 __Host- 前缀可以杜绝子域劫持和覆盖:
// 使用 Host 前缀强制限定安全属性与主机范围
Cookie hostCookie = new Cookie("__Host-sessionId", "sec_token_987");
hostCookie.setPath("/");
hostCookie.setSecure(true);
hostCookie.setHttpOnly(true);
response.addCookie(hostCookie);凭据安全设计
无论客户端防御策略多么严密,Cookie 本身都是暴露在客户端的存储载体。系统设计需遵循以下核心原则:
- 严禁存放敏感明文:绝对不能在 Cookie 中直接保存用户密码、身份证号、支付信息或权限标识。
- 使用不透明令牌(Opaque Token):Cookie 中仅存储由强加密安全随机数生成器生成的无业务含义字符串。
- 签名与散列校验:在服务端建立凭证白名单与撤销表,每次请求根据令牌哈希值检索会话状态。
进阶实践
在生产级 Web 系统中,Cookie 不仅用于简单的数据存取,还需要支撑企业级单点登录、防篡改签名、高安全持久化会话以及复杂的反向代理网络拓扑。
工具封装
通用读写
原生 Servlet API 中提取 Cookie 需要对数组进行非空判断并逐一循环遍历,编码处理繁琐且极易引发空指针异常。生产环境中应对 Cookie 的读写与字符编码进行统一封装:
// 读取指定 Cookie 并自动解码(防御 null 数组与空指针)
public static String getCookieValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
return null;
}
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
return null;
}
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
String value = cookie.getValue();
return value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : null;
}
}
return null;
}
// 写入安全 Cookie(统一 URL 编码并收紧 HttpOnly 与 Secure 安全边界)
public static void setCookie(HttpServletResponse response, String name, String value, int maxAge) {
String encoded = (value != null) ? URLEncoder.encode(value, StandardCharsets.UTF_8) : "";
Cookie cookie = new Cookie(name, encoded);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
}作用域清除
Cookie 的销毁依赖浏览器的匹配机制。若创建时指定了特定的 Path 或 Domain,执行删除时下发的空 Cookie 必须携带完全一致的作用域元数据,否则浏览器将无法命中原条目,造成删除失败。
// 清除指定 Cookie:必须与目标 Cookie 的 Path 及 Domain 作用域完全保持一致
public static void removeCookie(HttpServletResponse response, String name, String path, String domain) {
Cookie cookie = new Cookie(name, "");
cookie.setPath(path != null ? path : "/");
if (domain != null && !domain.isEmpty()) {
cookie.setDomain(domain);
}
// 将存活时间置 0 命令浏览器即时清除对应条目
cookie.setMaxAge(0);
response.addCookie(cookie);
}自动填写登录账户
需求分析
在韩顺平老师的 JavaWeb 教学体系中,该案例演示了利用 Cookie 实现“记住登录用户名(自动回显)”的典型场景:
- 凭据核验:当用户提交登录请求时,服务端校验账号与密码:若用户名是
hspedu且密码是123456,判定为合法用户并登录成功;否则判定为登录失败。 - 生命周期持久化:用户登录成功后,服务端下发持久化 Cookie 记录登录名,其生命周期显式设定为 3 天(
3 * 24 * 3600秒)。当用户在 3 天内再次访问登录页面时,系统自动读取 Cookie 并回显填入用户名输入框。 - 服务端动态渲染(关键约束):登录页面必须使用 Servlet 动态返回,而不能使用静态 HTML。因为纯静态 HTML 无法在服务器端接收并解析 HTTP 请求头附带的 Cookie 数组,更无法在服务端将读取到的用户名动态拼装注入到
<input value="...">属性中。
协同架构
系统由两个职责清晰的 Servlet 协同工作:
LoginServlet(视图渲染):映射路径/login,响应客户端GET请求。读取请求头携带的 Cookie 数组,若包含历史用户名则提取出该值,动态拼装并输出带有预填用户名的 HTML 登录表单。LoginCheckServlet(业务校验与状态持久化):映射路径/loginCheck,响应表单POST提交。核对账号密码凭据:- 校验成功:创建
loginUserCookie 并设置Max-Age为 3 天,调用resp.addCookie(cookie)写入响应头,输出成功界面及返回链接。 - 校验失败:不创建任何凭据 Cookie,输出失败提示信息并提供重新登录链接。
- 校验成功:创建
代码实现
1. 登录界面动态渲染 Servlet(LoginServlet)
通过重写 doGet 方法,在服务端提取请求携带的 Cookie,动态生成包含回显用户名的表单:
package com.hspedu.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(urlPatterns = "/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置响应内容格式与字符集编码,避免动态输出 HTML 乱码
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
// 2. 从 HTTP 请求中提取客户端提交的所有 Cookie 数组
String savedUsername = "";
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
// 查找保存历史登录账户名的 Cookie
if ("loginUser".equals(cookie.getName())) {
savedUsername = cookie.getValue();
break;
}
}
}
// 3. 动态拼接输出包含回填用户名的 HTML 登录表单
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println(" <meta charset='UTF-8'>");
out.println(" <title>用户登录</title>");
out.println("</head>");
out.println("<body>");
out.println(" <h1>用户登录</h1>");
out.println(" <form action='" + req.getContextPath() + "/loginCheck' method='post'>");
out.println(" <div>");
out.println(" <label>用户名:</label>");
out.println(" <input type='text' name='username' value='" + savedUsername + "' placeholder='请输入用户名'/>");
out.println(" </div><br/>");
out.println(" <div>");
out.println(" <label>密 码:</label>");
out.println(" <input type='password' name='password' placeholder='请输入密码'/>");
out.println(" </div><br/>");
out.println(" <div>");
out.println(" <input type='submit' value='登录'/>");
out.println(" </div>");
out.println(" </form>");
out.println("</body>");
out.println("</html>");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}2. 凭据校验与 Cookie 下发 Servlet(LoginCheckServlet)
通过重写 doPost 方法,接收表单参数、执行账号密码验证,并在校验通过时向客户端注入有效时长为 3 天的 Cookie:
package com.hspedu.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(urlPatterns = "/loginCheck")
public class LoginCheckServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 设置请求解码与响应内容字符集编码
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
// 2. 接收客户端表单提交的用户名与密码
String username = req.getParameter("username");
String password = req.getParameter("password");
// 3. 业务规则校验:用户名必须为 hspedu 且密码为 123456
if ("hspedu".equals(username) && "123456".equals(password)) {
// 4. 登录成功:实例化 Cookie 保存当前合法用户名
Cookie cookie = new Cookie("loginUser", username);
// 设置存活时长为 3 天(单位:秒,3 * 24 * 3600 = 259200 秒)
cookie.setMaxAge(3 * 24 * 3600);
// 设置有效路径为当前应用根路径,确保 /login 等全部页面均能携带回传
cookie.setPath(req.getContextPath().isEmpty() ? "/" : req.getContextPath());
// 将 Cookie 注入 HTTP 响应头(Set-Cookie)下发至客户端浏览器
resp.addCookie(cookie);
// 5. 动态响应登录成功提示页面
out.println("<!DOCTYPE html>");
out.println("<html><head><meta charset='UTF-8'><title>登录结果</title></head><body>");
out.println("<h2 style='color: green;'>登录成功!</h2>");
out.println("<p>欢迎您,<strong>" + username + "</strong>!</p>");
out.println("<p><a href='" + req.getContextPath() + "/login'>返回登录页面(测试 3 天内自动回填账号)</a></p>");
out.println("</body></html>");
} else {
// 6. 登录失败:不写入 Cookie,响应错误提示并提供重试入口
out.println("<!DOCTYPE html>");
out.println("<html><head><meta charset='UTF-8'><title>登录结果</title></head><body>");
out.println("<h2 style='color: red;'>登录失败!</h2>");
out.println("<p>用户名或密码错误,请检查后重新输入。</p>");
out.println("<p><a href='" + req.getContextPath() + "/login'>重新登录</a></p>");
out.println("</body></html>");
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
}执行流程
- 初始访问登录页:浏览器向
http://localhost:8080/应用名/login发起 GET 请求。因尚未存储该 Cookie,req.getCookies()无匹配项,LoginServlet渲染一个用户名为空白输入框的 HTML 表单。 - 提交凭据认证:用户输入用户名
hspedu与密码123456,点击“登录”,浏览器向POST /loginCheck提交表单数据。 - 身份核验与 Cookie 注入:
LoginCheckServlet核对用户名与密码一致无误:- 实例化
new Cookie("loginUser", "hspedu")。 - 调用
cookie.setMaxAge(3 * 24 * 3600)设定 3 天存活期。 - 调用
resp.addCookie(cookie),Tomcat 在 HTTP 响应头中生成Set-Cookie: loginUser=hspedu; Max-Age=259200; Path=/...。
- 实例化
- 客户端持久化:浏览器接收到响应,发现包含正数的
Max-Age,便将该 Cookie 写入操作系统的本地存储文件中(即使关闭浏览器重新打开依然有效)。 - 二次访问自动回显:在 3 天有效期内,用户再次访问
GET /login,浏览器在请求头中自动附带Cookie: loginUser=hspedu。LoginServlet提取出该键值,拼入<input ... value='hspedu'/>,实现用户名的自动填写。

核心细节剖析
- 为什么登录页面必须使用 Servlet 返回,而不能使用 HTML?
- 静态资源的无能为力:静态 HTML 资源仅由 Web 容器负责在磁盘上定位并以原始静态流输出到浏览器,完全无法在服务端执行 Java 逻辑,因而既无法调用
req.getCookies()解析客户端回传的 Cookie,也无法在服务端将 Cookie 值动态注入到 HTML 标签属性中。 - Servlet 的动态服务端渲染:通过在 Servlet 的
doGet中结合PrintWriter,服务端能够在接收到请求的瞬间,动态提取会话凭据并拼装实时 HTML 内容输出,这就是服务端渲染(SSR)在 Servlet 阶段最纯粹的基础体现。
- 静态资源的无能为力:静态 HTML 资源仅由 Web 容器负责在磁盘上定位并以原始静态流输出到浏览器,完全无法在服务端执行 Java 逻辑,因而既无法调用
- 作用域路径(
Path)的限定:- 若不显式调用
cookie.setPath(...),Cookie 默认的有效访问路径仅为创建该 Cookie 的 Servlet 所在虚拟目录。为确保整个 Web 应用下的所有资源(包括不同子路径的登录页)都能正常携带并回传该 Cookie,应显式设定setPath(req.getContextPath())。
- 若不显式调用
- 安全设计原则(为什么绝不自动填充密码):
- Cookie 存储在客户端本地文件中,缺乏强加密隔离保护,容易被同设备使用者查看或遭受客户端脚本(XSS)窃取。
- 安全红线:禁止将明文密码写入 Cookie 中。“自动填写登录账户”仅回填公开的用户名,密码必须由用户每次手动输入验证;如果需要实现免密码的“自动登录”,必须配合服务端数据库采用 Series 与动态 Token 的双令牌持久化机制。
单点登录
跨子域共享
在企业级多业务系统中,常见域名结构如 sso.example.com(认证中心)、mall.example.com(商城系统)和 oa.example.com(办公系统)。利用 Cookie 的主域下发特性,可实现低成本的跨子域单点登录(SSO):
用户访问任一子系统,未登录时统一重定向至认证中心。
用户在认证中心输入凭据,服务端完成身份校验。
认证中心生成全局会话 Token,写入 Cookie 并显式将
Domain设为主域.example.com,Path设为/。认证成功后跳转回业务系统,由于业务系统同属该主域,浏览器在发起后续请求时会自动携带该 Cookie。
业务系统拦截器读取 Token,完成本地会话绑定与权限放行。
跨主域隔离
当不同系统运行在完全独立的主域名下(如 company-a.com 与 company-b.com)时,浏览器的同源策略与 Domain 规范禁止跨域共享 Cookie。此时必须采用 CAS、OAuth2 或 OIDC 协议,通过重定向与授权码中转票据,最终在各自的域名下单独下发本地会话 Cookie。

防篡改签名
签名机制
为了在减少服务端存储压力的同时防止客户端篡改内容,可采用无状态签名 Cookie 方案。服务端将业务数据与数字签名拼接为一体下发,数据格式通常为:原始数据.HMAC签名。
// 使用服务端私钥生成数据的哈希摘要以防篡改
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(SECRET_KEY, "HmacSHA256"));
byte[] sign = hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
// 校验时比对签名一致性以识别客户端数据是否被非法篡改
boolean valid = MessageDigest.isEqual(sign, clientSignBytes);验签流程
服务端收到请求,按约定分隔符拆解 Cookie 内容,分离出业务负载与签名串。
使用服务端保存的密钥,对业务负载重新计算一次 HMAC 签名。
使用防计时攻击的方法(如
MessageDigest.isEqual)比对新生成的签名与客户端提交的签名。比对一致则信任数据内容;若不一致则视为数据被恶意篡改,直接废弃并记录安全告警。
高安全持久化会话
双令牌机制
为实现安全的“记住我”(Remember-Me)功能,业界普遍采用 Series 与 Token 双令牌联动方案,以防御重放攻击与令牌窃取:
用户登录时勾选“记住我”,服务端生成两个强随机串:固定系列标识(Series)与动态令牌(Token)。
将
Series:Token组合存入持久化 Cookie 下发给浏览器,并在数据库中记录对应的用户身份与有效期限。用户再次访问时,服务端根据客户端回传的 Series 与 Token 查询数据库。
若两者完全吻合,认证通过,服务端生成全新 Token 替换旧 Token,并同时更新数据库与浏览器端 Cookie,Series 保持不变。
若匹配发现 Series 存在但 Token 不匹配,说明该凭证曾被第三方截获并重放过。系统立即视此为凭证盗用事件,立刻强制撤销该 Series 下的所有会话凭据。
数据库同步
双令牌状态需要在持久化数据层进行高频读写与失效淘汰。
核验并检索持久化令牌有效性的 SQL 查询示例:
SELECT
series,
token,
last_used
FROM persistent_logins
WHERE series = 'series_abc123'
GROUP BY series, token, last_used;反向代理网络拓扑
协议识别
在微服务与容器化部署中,外部请求通常先经过 Nginx、Ingress 或云负载均衡器终结 SSL,内部以明文 HTTP 转发至 JavaWeb 容器。由于 Java 容器感知到的协议是 HTTP,直接调用 cookie.setSecure(true) 可能导致浏览器拒收,或者框架默认无法自动打上 Secure 标记。
解决该问题需要在反向代理上透传原始协议头:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;Java 服务端(如 Tomcat)则需配置 RemoteIpValve,确保容器能够感知上游反向代理的 HTTPS 状态。
属性重写
当旧版后端框架不支持直接设置 SameSite=None 或现代安全前缀时,可通过 Nginx 反向代理层在响应流中统一对 Set-Cookie 响应头进行修正与安全加固:
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=Lax";API: Cookie
继承自 javax.servlet.http.Cookiejava.lang.Object,并实现了 java.lang.Cloneable 与 java.io.Serializable 接口。该类封装了 Cookie 的全部元数据控制逻辑。
实例构造
CookieCookie():(String name, String value),构造方法。使用指定的键名与初始键值创建一个新的 Cookie 实例。键名一旦设定无法修改。
注意事项:
- Cookie 名称必须符合 RFC 2109/6265 语法规范,只能由 ASCII 字母、数字及部分无歧义符号组成;严禁包含空格、制表符、分号、逗号、双引号、反斜杠及控制字符,严禁以
$字符开头。value严禁直接存储未编码的中文字符或二进制内容。若存储非 ASCII 字符串,必须在构造前使用URLEncoder.encode(value, StandardCharsets.UTF_8)进行编码,读取时由URLDecoder.decode解码。
// 1. 构造基础标识 Cookie 实例
Cookie basicCookie = new Cookie("user_id", "1024");
// 2. 针对包含中文的值进行 URL 编码后构造实例
String encodedVal = java.net.URLEncoder.encode("技术笔记", java.nio.charset.StandardCharsets.UTF_8);
Cookie i18nCookie = new Cookie("user_label", encodedVal);数据状态维护
StringgetName():(),获取名称。返回当前 Cookie 的键名,该属性在对象实例化后只读不可变。StringgetValue():(),获取数据。返回当前 Cookie 实例存储的字符串值。voidsetValue():(String newValue),修改数据。重新指定 Cookie 存储的字符串值,在下发前更新有效。intgetVersion():(),获取协议版本。获取此 Cookie 所遵循的协议规范版本(0 代表 Netscape 原始规范,1 代表 RFC 2109 规范)。voidsetVersion():(int v),设置协议版本。显式设定状态管理规范版本号,生产环境中通常保持默认值 0 以确保跨客户端最大兼容性。Objectclone():(),对象副本克隆。重写Object.clone()方法,创建并返回当前 Cookie 对象的浅拷贝副本。
注意事项:
- 调用
setValue()仅改变服务端内存对象中的属性值,若要使客户端数据生效,必须重新调用response.addCookie()下发覆盖。clone()返回类型为Object,使用时需显式向下转型为Cookie。由于内部字段name与value均为不可变的String,其浅拷贝具有线程安全的逻辑副本特性。
// 1. 读取 Cookie 的键名与当前存储的值
String cookieName = basicCookie.getName();
String currentValue = basicCookie.getValue();
// 2. 更新存储的数据内容并显式声明协议版本
basicCookie.setValue("2048");
basicCookie.setVersion(0);
// 3. 克隆独立副本以避免多线程共享修改污染原对象
Cookie clonedCookie = (Cookie) basicCookie.clone();作用范围限定
StringgetPath():(),获取有效路径。返回限制此 Cookie 可见性的服务器端 URI 路径前缀。voidsetPath():(String uri),设置有效路径。指定客户端在向哪些 URI 发送请求时需要携带此 Cookie(例如/表示全站可用)。StringgetDomain():(),获取有效域名。返回此 Cookie 生效的域名规则或 DNS 区域。voidsetDomain():(String pattern),设置有效域名。指定跨子域共享的域名范围(例如.example.com使得api.example.com与web.example.com均可访问)。intgetMaxAge():(),获取存活时长。返回 Cookie 的最长生命周期,单位为秒。默认值为 -1。voidsetMaxAge():(int expiry),设置存活时长。设定 Cookie 在客户端保存的最大生存周期(秒)。值为正数表示持久化存储,负数表示浏览器会话结束即销毁,0 表示立即通知浏览器删除该 Cookie。
注意事项:
- 物理删除判定:利用
setMaxAge(0)执行删除操作时,下发的 Cookie 必须与原 Cookie 的name、path、domain保持完全一致。若路径或域名不匹配,浏览器将判定为新增了一个独立 Cookie,导致旧 Cookie 依然滞留在客户端。- 跨域限制:
setDomain必须遵循公网后缀(Public Suffix)与同源规则,不可为顶级域名(如.com、.org)或第三方域名设置 Cookie,否则会被浏览器内核直接丢弃。
// 1. 设置全站生效的访问路径与跨子域共享域名
Cookie sessionCookie = new Cookie("sid", "sess_token_abc");
sessionCookie.setPath("/");
sessionCookie.setDomain(".example.com");
// 2. 设置持久化生命周期为 7 天(7 * 24 * 3600 秒)
sessionCookie.setMaxAge(604800);
// 3. 构建用于立即通知客户端销毁此 Cookie 的清除对象
Cookie deleteCookie = new Cookie("sid", "");
deleteCookie.setPath("/");
deleteCookie.setDomain(".example.com");
deleteCookie.setMaxAge(0);兜底保障:若遇到极少数不规范的代理服务器、旧版爬虫或非标准嵌入式客户端没有正确响应
Max-Age=0,此时由于Value被覆写成了空字符串"",原有的敏感身份凭证(如sess_token_abc)也已被覆盖销毁,避免了敏感信息继续滞留或被误用的风险。
安全传输策略
booleangetSecure():(),获取安全标记。检查是否要求客户端仅在 HTTPS 等安全加密协议下向服务器回传此 Cookie。voidsetSecure():(boolean flag),设置安全标记。若设为true,客户端在明文 HTTP 连接中将被禁止发送该 Cookie,有效规避链路明文抓包风险。booleanisHttpOnly():(),获取脚本访问限制。检查是否禁止客户端脚本(如 JavaScript)通过document.cookie读取此 Cookie。voidsetHttpOnly():(boolean isHttpOnly),设置脚本访问限制。自 Servlet 3.0 引入。设为true可从浏览器运行沙箱层面切断 XSS 脚本窃取敏感凭证的链路。StringgetComment():(),获取注释信息。返回描述该 Cookie 用途的注释字符串(RFC 2109 规范支持)。voidsetComment():(String purpose),设置注释信息。指定 Cookie 的用途描述信息,供客户端用户审查。
注意事项:
isHttpOnly与setHttpOnly在 Servlet 3.0(Java EE 6)及以上版本才成为标准 API。在 Servlet 2.5 旧工程中,需借助HttpServletResponse.setHeader("Set-Cookie", ...)手动拼接字符。javax.servlet.http.Cookie类不原生支持现代浏览器核心的SameSite属性(Strict/Lax/None)。在生产环境中,防御 CSRF 攻击通常需要通过自定义 Servlet Filter 拦截响应标头并手动追加; SameSite=Lax,或依赖 Servlet 容器(如 Tomcat 8.5+/9.0+ 的Rfc6265CookieProcessor)进行底层接管。
// 1. 开启传输加密与客户端脚本防窃取标记
Cookie authCookie = new Cookie("auth_token", "secret_payload_8848");
authCookie.setSecure(true);
authCookie.setHttpOnly(true);
// 2. 为当前 Cookie 补充符合 RFC 2109 规范的业务用途说明
authCookie.setComment("User authentication session token");
// 3. 校验安全属性配置状态
boolean isEncrypted = authCookie.getSecure();
boolean isScriptHidden = authCookie.isHttpOnly();继承:Object
equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait