Java Cookie 全面指南:从原理到 Spring Boot 实战

这是一份非常详细、实用、通俗易懂、权威且全面的 Java Cookie 指南,特别包含了在 Spring Boot 中的应用。


目录

  1. Cookie 基础概念
    • 1.1 什么是 Cookie?
    • 1.2 Cookie 的作用:为什么需要它?
    • 1.3 Cookie 的工作原理(通俗解释)
    • 1.4 Cookie 的属性
  2. Java 中的 Cookie 操作 (Servlet API)
    • 2.1 创建 Cookie
    • 2.2 将 Cookie 发送给客户端 (添加到响应)
    • 2.3 从客户端读取 Cookie (从请求中获取)
    • 2.4 修改 Cookie
    • 2.5 删除 Cookie
    • 2.6 设置 Cookie 的有效期 (setMaxAge)
    • 2.7 设置 Cookie 的路径 (setPath)
    • 2.8 设置 Cookie 的域 (setDomain)
    • 2.9 安全相关属性 (setHttpOnly, setSecure)
  3. Spring Boot 中的 Cookie 操作
    • 3.1 使用 HttpServletResponse (与传统 Servlet 类似)
    • 3.2 使用 @CookieValue 注解 (便捷获取)
    • 3.3 使用 ResponseCookie (Spring Boot 提供的更现代的构建器)
  4. Cookie 的安全性与最佳实践
    • 4.1 HttpOnly Cookie (防御 XSS)
    • 4.2 Secure Cookie (防御嗅探)
    • 4.3 SameSite 属性 (防御 CSRF)
    • 4.4 签名与加密 (防止篡改)
    • 4.5 敏感信息不要存储在 Cookie 中
    • 4.6 Cookie 大小限制
  5. 实战案例:可直接运行的 Spring Boot 示例
    • 案例 1:基础读写 - 记录用户访问次数
    • 案例 2:安全设置 - 登录认证 Token (HttpOnly, Secure)
    • 案例 3:跨域/子域共享 Cookie (Domain, Path)
    • 案例4:用户登录状态管理
    • 案例5:购物车实现
    • 案例6:记住我功能
  6. 高级主题与注意事项
    • 6.1 Cookie 与 Session 的关系
    • 6.2 Cookie 的替代方案 (Token, LocalStorage)
    • 6.3 浏览器限制与隐私政策 (GDPR, CCPA)
    • 6.4 在 RESTful API 中使用 Cookie
  7. 总结

1.1 什么是 Cookie?

想象一下你去一家超市购物。超市给你一张会员卡(Cookie),上面记录了一个唯一的号码(如 session_id=abc123)。下次你再去同一家超市(网站),出示这张卡(浏览器自动发送 Cookie),超市(服务器)就能认出你(识别用户状态),知道你上次买了什么(个性化推荐),或者直接让你进入会员区(保持登录状态)。

技术定义: Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储它,并在下一次向同一服务器 发起请求时携带上它。Cookie 使得服务器能够识别请求是否来自同一个浏览器,从而在无状态的 HTTP 协议上实现状态管理。

HTTP 协议本身是无状态的。这意味着服务器处理每个请求时,它"不记得"之前处理过哪些请求。这就像每次你去银行柜台办事,柜员都把你当作第一次见面的陌生人。这显然不行!

Cookie 的主要作用就是解决这个问题,实现:

  • 会话管理(Session Management): 用户登录状态、购物车内容等。服务器通常会生成一个唯一的 Session ID,通过 Cookie 发送给浏览器存储。浏览器后续请求携带此 ID,服务器就能找到对应的 Session 数据(存储在服务器端)。
  • 个性化(Personalization): 用户偏好设置(如语言、主题)、记住用户名(非密码)。
  • 跟踪(Tracking): 分析用户行为(广告、分析)。注意:这部分涉及隐私问题,需谨慎处理并遵守相关法规。
  1. 首次请求: 用户访问网站 example.com
  2. 服务器响应: 服务器处理请求后,在 HTTP 响应头中加入 Set-Cookie 指令,例如 Set-Cookie: user_id=12345; Max-Age=3600; Path=/; HttpOnly
  3. 浏览器存储: 用户的浏览器接收到响应,会根据指令将 Cookie(user_id=12345)及其属性(有效期 1 小时、作用于整个站点、仅 HTTP 传输)存储在本地(通常是硬盘上的特定文件)。
  4. 后续请求: 当用户再次访问 example.com 或其子路径(符合 Path 设置)时,浏览器会自动在 HTTP 请求头中加入 Cookie: user_id=12345
  5. 服务器识别: 服务器收到请求,读取 Cookie 头,就能知道这个请求来自用户 12345,从而提供个性化的服务或维持登录状态。

Cookie 不仅仅是键值对,它还有一些重要的属性控制其行为:

  • 名称(Name)和值(Value): 核心数据。例如 username=JohnDoe
  • 有效期(Expires / Max-Age): 决定 Cookie 的生命周期。
    • Expires: 指定一个具体的过期时间(GMT 格式)。
    • Max-Age: 指定从设置时刻起多少秒后过期。优先使用 Max-Age
    • 如果都不设置,Cookie 是会话 Cookie,只在浏览器打开期间有效,关闭浏览器即删除。
  • 作用域(Domain): 指定哪些域名可以接收此 Cookie。例如 .example.com 表示 example.com 及其所有子域名(如 www.example.com, shop.example.com)都可以接收和发送此 Cookie。不设置则默认为当前文档的域名(不包括子域名)。
  • 路径(Path): 指定 URL 路径前缀,只有匹配该路径的请求才会携带 Cookie。例如 /shop 表示只有访问 /shop 及其子路径(如 /shop/cart, /shop/checkout)时才会发送该 Cookie。默认为 /,即整个站点。
  • Secure: 标记为 Secure 的 Cookie 只会在使用 HTTPS 协议加密的请求中发送给服务器。防止 Cookie 在非加密的 HTTP 连接中被窃听。
  • HttpOnly: 标记为 HttpOnly 的 Cookie 不能被客户端的 JavaScript 脚本访问(例如 document.cookie)。这是重要的安全措施,有助于缓解跨站脚本攻击(XSS)窃取 Cookie。
  • SameSite: 控制 Cookie 在跨站点请求时是否被发送。是防御**跨站请求伪造(CSRF)**攻击的关键。
    • Strict: 最严格,仅在同站点请求(请求 URL 与当前页面 URL 完全匹配)时发送。
    • Lax: (现代浏览器默认值)允许在顶级导航(例如点击链接)且使用安全方法(GET)的跨站点请求中发送。阻止在跨站 POST 提交或通过 <img>, <iframe> 加载资源时发送。
    • None: 允许在跨站点请求中发送,但必须同时设置 Secure 属性(即只允许在 HTTPS 下发送)。这是旧版行为,安全性最低,仅在特定跨域场景下需要。

Java EE 的 Servlet API 提供了 javax.servlet.http.Cookie 类来操作 Cookie。

java 复制代码
import javax.servlet.http.Cookie;

// 创建一个名为 "username",值为 "JohnDoe" 的 Cookie
Cookie usernameCookie = new Cookie("username", "JohnDoe");

// 创建一个名为 "sessionId",值为随机 UUID 的 Cookie (常用于会话管理)
Cookie sessionCookie = new Cookie("sessionId", java.util.UUID.randomUUID().toString());
java 复制代码
import javax.servlet.http.HttpServletResponse;

// 假设 response 是 HttpServletResponse 对象
response.addCookie(usernameCookie); // 将 cookie 添加到响应头
java 复制代码
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Cookie;

// 假设 request 是 HttpServletRequest 对象
Cookie[] cookies = request.getCookies(); // 获取请求中的所有 Cookie

if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("username".equals(cookie.getName())) {
            String usernameValue = cookie.getValue();
            // 处理 usernameValue...
            break;
        }
    }
}

修改 Cookie 实际上是在服务器端创建一个同名 的新 Cookie,设置新的值或属性,然后通过 response.addCookie() 发送给浏览器。浏览器会用新的覆盖旧的。

java 复制代码
// 假设我们找到了要修改的 cookie
Cookie existingCookie = ...; // 从 request.getCookies() 中找出
existingCookie.setValue("NewValue"); // 修改值
existingCookie.setMaxAge(60 * 60 * 24 * 7); // 修改有效期为一周
response.addCookie(existingCookie); // 发送更新后的 cookie

删除 Cookie 也是通过创建一个同名的新 Cookie 来实现:

  1. 设置 setMaxAge(0) - 立即过期。
  2. (可选但推荐)设置 setPath()setDomain() 与要删除的 Cookie 创建时的设置完全一致。如果不一致,浏览器可能会创建一个同名但路径/域不同的新无效 Cookie,而旧的 Cookie 依然存在。
  3. 通过 response.addCookie() 发送。
java 复制代码
Cookie deleteCookie = new Cookie("username", ""); // 值不重要,设为空或任意
deleteCookie.setMaxAge(0); // 告诉浏览器立即删除此 Cookie
deleteCookie.setPath("/"); // 必须与原始 Cookie 设置的路径匹配!这里是 '/'
// deleteCookie.setDomain("yourdomain.com"); // 如果原始设置了 Domain,这里也要设置
response.addCookie(deleteCookie);
java 复制代码
Cookie cookie = new Cookie("rememberMe", "yes");
cookie.setMaxAge(60 * 60 * 24 * 30); // 设置有效期为 30 天 (单位:秒)
response.addCookie(cookie);
java 复制代码
Cookie cookie = new Cookie("cartId", "cart789");
cookie.setPath("/shop"); // 只有访问 /shop 及其子路径时才会发送此 Cookie
response.addCookie(cookie);
java 复制代码
Cookie cookie = new Cookie("preferences", "dark_theme");
cookie.setDomain(".example.com"); // 注意前面的点 '.',表示 example.com 及其所有子域
response.addCookie(cookie);
2.9 安全相关属性 (setHttpOnly, setSecure)
java 复制代码
Cookie cookie = new Cookie("authToken", "encryptedTokenValue");
cookie.setHttpOnly(true); // 防止 XSS 攻击窃取
cookie.setSecure(true);   // 只在 HTTPS 连接中发送
cookie.setPath("/");
cookie.setMaxAge(60 * 60); // 1小时有效
response.addCookie(cookie);

Spring Boot 构建在 Servlet API 之上,因此你可以直接使用 HttpServletResponseHttpServletRequest 来操作 Cookie,方法和第 2 节完全一样。此外,Spring 提供了更便捷的方式。

3.1 使用 HttpServletResponse (与传统 Servlet 类似)

在 Spring MVC 的 Controller 方法中,你可以注入 HttpServletResponse

java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@RestController
public class CookieController {

    @GetMapping("/setcookie")
    public String setCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("user", "Alice");
        cookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        response.addCookie(cookie);
        return "Cookie has been set!";
    }
}
3.2 使用 @CookieValue 注解 (便捷获取)

Spring MVC 提供了 @CookieValue 注解,可以直接将请求中的 Cookie 值绑定到 Controller 方法的参数上。

java 复制代码
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ReadCookieController {

    @GetMapping("/getcookie")
    public String readCookie(@CookieValue(name = "user", defaultValue = "Guest") String username) {
        return "Hello, " + username + "! (Read from cookie)";
    }
}

注意: @CookieValue 要求 Cookie 必须存在(除非设置了 defaultValue)。如果 Cookie 可能不存在且你不希望抛出异常,可以将其绑定为 java.util.Optional

java 复制代码
@GetMapping("/getcookieopt")
public String readCookieOptional(@CookieValue(name = "user") Optional<String> username) {
    return "Hello, " + username.orElse("Guest") + "!";
}
3.3 使用 ResponseCookie (Spring Boot 提供的更现代的构建器)

从 Spring Boot 2.5 (Spring Framework 5.1) 开始,引入了 org.springframework.http.ResponseCookie 类。它提供了流畅的构建器 API 来创建 Cookie,并且生成的字符串可以直接添加到 ResponseSet-Cookie 头。这种方式更符合 HTTP 头的语义。

java 复制代码
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResponseCookieController {

    @GetMapping("/setresponsecookie")
    public ResponseEntity<String> setResponseCookie() {
        // 使用构建器创建 ResponseCookie
        ResponseCookie responseCookie = ResponseCookie.from("theme", "dark") // 名称, 值
                .httpOnly(true) // HttpOnly
                .secure(true)   // Secure
                .path("/")      // Path
                .maxAge(60 * 60 * 24) // Max-Age (1天)
                .domain("example.com") // Domain
                .sameSite("Lax") // SameSite 属性 (Strict, Lax, None)
                .build();

        // 构建响应,将 Cookie 添加到 Set-Cookie 头
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, responseCookie.toString()) // 关键!
                .body("ResponseCookie has been set!");
    }
}

优点:

  • 流畅的 API,设置属性更清晰。
  • 直接生成符合 Set-Cookie 头规范的字符串。
  • 更容易设置 SameSite 等属性。
  • 可以添加多个 Set-Cookie 头(多次调用 .header())。

Cookie 经常用于存储会话标识符或认证令牌,因此安全性至关重要。

  • 为什么: 阻止恶意 JavaScript (document.cookie) 窃取敏感的 Cookie(如 Session ID)。
  • 怎么做: 在服务器端设置 Cookie 时,务必设置 setHttpOnly(true) 或使用 ResponseCookie.httpOnly(true)
  • 为什么: 防止 Cookie 在未加密的 HTTP 连接中被网络窃听者截获。
  • 怎么做: 在服务器端设置 Cookie 时,如果 你的网站启用了 HTTPS,为包含敏感信息(特别是会话标识符)的 Cookie 设置 setSecure(true).secure(true)注意: 在本地开发环境 (HTTP) 下设置 Secure 会导致浏览器不存储该 Cookie。
4.3 SameSite 属性 (防御 CSRF)
  • 为什么: 阻止攻击者利用用户的登录状态(浏览器中的 Cookie)在用户不知情的情况下发起恶意请求(如转账)。
  • 怎么做: 强烈建议为会话 Cookie 设置 SameSite=Lax(现代浏览器默认值)或 SameSite=StrictSameSite=None 仅在需要跨域携带 Cookie 的场景下使用(如 OAuth 登录回调、跨域 AJAX 请求),并且必须 配合 Secure 属性(即仅限 HTTPS)。使用 ResponseCookie 可以方便地设置 .sameSite("Lax")
4.4 签名与加密 (防止篡改)
  • 为什么: 防止客户端篡改 Cookie 的值(例如,用户试图将自己的普通权限 Cookie 改为管理员权限)。
  • 怎么做:
    • 签名 (Signing): 服务器在设置 Cookie 值时,附加一个基于该值和服务器密钥生成的签名(如 HMAC)。在读取 Cookie 时,服务器重新计算签名并与 Cookie 中的签名对比。如果签名不匹配,说明值被篡改,应拒绝该 Cookie。Spring 提供了 org.springframework.web.util.WebUtilsgetCookieaddCookie 方法,以及更强大的 org.springframework.session.web.http.CookieSerializer 来处理签名 Cookie(Spring Session 模块常用)。
    • 加密 (Encryption): 直接对 Cookie 的值进行加密(如 AES),只有服务器持有密钥才能解密。这比签名更安全,但也更耗费资源。通常结合使用(先签名再加密或反之)。
  • 为什么: Cookie 存储在用户浏览器上,可以被用户查看(即使 HttpOnly 也只能阻止 JavaScript),也可能被其他漏洞利用。网络传输中即使有 HTTPS 和 Secure Cookie,也存在潜在风险。
  • 怎么做: 不要在 Cookie 中直接存储用户的密码、信用卡号、身份证号等敏感信息。通常只存储一个随机生成的、无意义的标识符(如 Session ID、Token),服务器端通过这个标识符去关联存储在数据库或缓存中的真实用户数据。
  • 每个 Cookie 的大小通常限制在 4KB 左右(不同浏览器可能有细微差异)。
  • 每个域名下的 Cookie 总数也有限制(通常是 50 个左右)。
  • 怎么做: 避免在 Cookie 中存储大量数据。如果需要存储大量用户状态,考虑使用服务器端 Session 存储(数据库、缓存),Cookie 仅存储 Session ID。

5. 实战案例:可直接运行的 Spring Boot 示例

以下是几个使用 Spring Boot 3.x 的完整、可运行的案例。确保你的项目包含 Spring Boot Web Starter 依赖。

案例 1:基础读写 - 记录用户访问次数
java 复制代码
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
public class VisitCounterController {

    @GetMapping("/visit")
    public ResponseEntity<String> trackVisit(HttpServletRequest request, HttpServletResponse response) {
        // 1. 尝试从请求中获取名为 "visitCount" 的 Cookie
        Cookie[] cookies = request.getCookies();
        int visitCount = 0;

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("visitCount".equals(cookie.getName())) {
                    try {
                        visitCount = Integer.parseInt(cookie.getValue());
                    } catch (NumberFormatException e) {
                        visitCount = 0; // 如果值无效,重置
                    }
                    break;
                }
            }
        }

        // 2. 增加访问次数
        visitCount++;

        // 3. 创建或更新 Cookie,设置新值
        Cookie visitCookie = new Cookie("visitCount", String.valueOf(visitCount));
        visitCookie.setMaxAge(365 * 24 * 60 * 60); // 1年有效期
        visitCookie.setPath("/"); // 作用于整个站点
        visitCookie.setHttpOnly(true); // 推荐设置
        response.addCookie(visitCookie);

        // 4. 返回响应
        return ResponseEntity.ok("Welcome back! This is your visit #" + visitCount + ".");
    }
}

测试: 访问 http://localhost:8080/visit,刷新页面观察计数增加。查看浏览器开发者工具(Application -> Cookies)可以看到 visitCount Cookie。

案例 2:安全设置 - 登录认证 Token (HttpOnly, Secure)

假设你有一个登录服务,登录成功后生成一个认证 Token。

java 复制代码
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {

    // 模拟登录逻辑 (实际应用中应使用数据库和密码加密比较)
    @PostMapping("/login")
    public ResponseEntity<String> login(
            @RequestParam String username,
            @RequestParam String password,
            HttpServletResponse response) {

        // 1. 验证用户名密码 (这里简化为固定值)
        if ("admin".equals(username) && "secret".equals(password)) {

            // 2. 登录成功,生成一个安全的 Token (实际应用中应使用 JWT 或其他安全机制生成)
            String authToken = generateSecureToken(); // 伪代码

            // 3. 使用 ResponseCookie 构建安全 Cookie (推荐方式)
            ResponseCookie authCookie = ResponseCookie.from("authToken", authToken)
                    .httpOnly(true) // 必须!防御 XSS
                    .secure(true)   // 必须!仅限 HTTPS (本地开发用 HTTP 时注释掉这行,否则 Cookie 不存储)
                    .path("/")      // 应用路径
                    .maxAge(30 * 60) // 30 分钟有效 (根据需要调整)
                    .sameSite("Lax") // 推荐设置防御 CSRF
                    .build();

            // 4. 将 Cookie 添加到响应头
            return ResponseEntity.ok()
                    .header(HttpHeaders.SET_COOKIE, authCookie.toString())
                    .body("Login successful! Welcome, " + username + ".");

        } else {
            return ResponseEntity.status(401).body("Invalid username or password");
        }
    }

    // 模拟登出
    @GetMapping("/logout")
    public ResponseEntity<String> logout(HttpServletResponse response) {
        // 1. 创建一个立即过期、同名、同路径的 Cookie 来覆盖删除
        ResponseCookie deleteCookie = ResponseCookie.from("authToken", "")
                .httpOnly(true)
                .secure(true) // 保持与创建时一致
                .path("/")
                .maxAge(0) // 立即过期
                .sameSite("Lax")
                .build();

        // 2. 添加到响应头
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, deleteCookie.toString())
                .body("You have been logged out.");
    }

    private String generateSecureToken() {
        // 实际应用中:应使用安全的随机数生成器,并可能包含用户ID、过期时间、签名等。
        return "SECURE_RANDOM_TOKEN_" + System.currentTimeMillis(); // 简化示例
    }
}

测试:

  1. 使用 Postman 或 curl 向 POST http://localhost:8080/login 发送表单数据 (username=admin&password=secret)。检查响应头 Set-Cookie,应包含 authToken 且带有 HttpOnly; Secure; SameSite=Lax 属性(本地开发注释 .secure(true) 后则没有 Secure)。
  2. 访问其他需要认证的端点(假设你有),浏览器会在请求头 Cookie 中自动带上 authToken
  3. 访问 GET http://localhost:8080/logout,响应头 Set-Cookie 会设置一个过期时间为 0 的 authToken Cookie,浏览器收到后会删除它。

假设你有主站 www.example.com 和一个商城子站 shop.example.com,你想在主站登录后,用户在访问商城时也能保持登录状态。

java 复制代码
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SharedCookieController {

    @GetMapping("/setsharedcookie")
    public ResponseEntity<String> setSharedCookie() {
        // 1. 创建一个将在子域间共享的 Cookie (例如 Session ID)
        String sharedValue = "SHARED_VALUE_123";

        // 2. 关键:设置 domain 为 '.example.com' (注意前面的点 '.')
        ResponseCookie sharedCookie = ResponseCookie.from("sharedData", sharedValue)
                .httpOnly(true)
                .secure(true) // 生产环境启用
                .path("/")    // 根路径
                .maxAge(24 * 60 * 60) // 1天
                .domain(".example.com") // 允许 example.com 和所有子域访问
                .sameSite("Lax") // 或 None (如果需要跨顶级域)
                .build();

        // 3. 添加到响应头
        return ResponseEntity.ok()
                .header(HttpHeaders.SET_COOKIE, sharedCookie.toString())
                .body("Shared cookie has been set for .example.com!");
    }

    @GetMapping("/readsharedcookie")
    public ResponseEntity<String> readSharedCookie(@org.springframework.web.bind.annotation.CookieValue(name = "sharedData", required = false) String sharedValue) {
        if (sharedValue != null && !sharedValue.isEmpty()) {
            return ResponseEntity.ok("Found shared cookie value: " + sharedValue);
        } else {
            return ResponseEntity.ok("No shared cookie found.");
        }
    }
}

重要说明:

  • 此代码需要在 www.example.comshop.example.com 或其他 *.example.com 子域下运行。
  • 当你在 www.example.com/setsharedcookie 设置 Cookie 后,访问 shop.example.com/readsharedcookie,浏览器会因为设置了 Domain=.example.comPath=/,自动在请求中包含这个 Cookie。
  • 本地测试: 要测试跨子域,你需要修改本地 hosts 文件 (如 /etc/hostsC:\Windows\System32\drivers\etc\hosts),添加条目如 127.0.0.1 www.localdev.com127.0.0.1 shop.localdev.com。然后启动 Spring Boot 应用,分别访问 http://www.localdev.com:8080/setsharedcookiehttp://shop.localdev.com:8080/readsharedcookie。注意:非 HTTPS 环境下,Secure 属性会阻止 Cookie 存储和发送。

案例 4:用户登录状态管理

本案例实现基于Cookie的用户登录状态管理。用户登录后,服务器设置Cookie存储session ID;后续请求验证Cookie以维持登录状态。

完整代码
java 复制代码
import java.io.IOException;
import java.io.PrintWriter;
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;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 模拟验证(实际中需数据库查询)
        if ("admin".equals(username) && "password123".equals(password)) {
            // 创建session ID Cookie(实际应使用随机Token)
            Cookie sessionCookie = new Cookie("sessionID", "generatedSession123");
            sessionCookie.setMaxAge(60 * 60 * 24); // 24小时过期
            sessionCookie.setPath("/");
            sessionCookie.setHttpOnly(true);
            sessionCookie.setSecure(true); // 假设使用HTTPS
            response.addCookie(sessionCookie);
            
            out.println("<html><body>");
            out.println("<h1>Login Successful! Welcome, " + username + ".</h1>");
            out.println("<a href='/yourApp/profile'>Go to Profile</a>");
            out.println("</body></html>");
        } else {
            out.println("<html><body>");
            out.println("<h1>Login Failed. Invalid credentials.</h1>");
            out.println("</body></html>");
        }
    }
}

@WebServlet("/profile")
public class ProfileServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 检查session ID Cookie
        Cookie[] cookies = request.getCookies();
        String sessionID = null;
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("sessionID".equals(cookie.getName())) {
                    sessionID = cookie.getValue();
                    break;
                }
            }
        }
        
        if (sessionID != null && sessionID.equals("generatedSession123")) { // 实际应验证Token
            out.println("<html><body>");
            out.println("<h1>User Profile Page</h1>");
            out.println("<p>You are logged in.</p>");
            out.println("</body></html>");
        } else {
            response.sendRedirect("login.html"); // 重定向到登录页
        }
    }
}

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 删除session ID Cookie
        Cookie sessionCookie = new Cookie("sessionID", "");
        sessionCookie.setMaxAge(0);
        sessionCookie.setPath("/");
        response.addCookie(sessionCookie);
        
        response.sendRedirect("login.html");
    }
}
 
使用说明
  1. 创建login.html表单:

    html 复制代码
    <!DOCTYPE html>
    <html>
    <head>
        <title>Login</title>
    </head>
    <body>
        <form action="login" method="post">
            Username: <input type="text" name="username"><br>
            Password: <input type="password" name="password"><br>
            <input type="submit" value="Login">
        </form>
    </body>
    </html>
     
  2. 部署应用,访问http://localhost:8080/yourApp/login.html

  3. 登录后,访问/profile验证状态;退出使用/logout

案例 5:购物车实现

本案例使用Cookie实现简单购物车。用户添加商品时,服务器更新Cookie存储商品ID列表。

完整代码
java 复制代码
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
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;

@WebServlet("/addToCart")
public class AddToCartServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String productId = request.getParameter("id");
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 获取现有购物车Cookie
        Cookie[] cookies = request.getCookies();
        String cartValue = "";
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("shoppingCart".equals(cookie.getName())) {
                    cartValue = cookie.getValue();
                    break;
                }
            }
        }
        
        // 更新购物车(假设商品ID用逗号分隔)
        if (!cartValue.isEmpty()) {
            cartValue += "," + productId;
        } else {
            cartValue = productId;
        }
        
        // 设置新Cookie
        Cookie cartCookie = new Cookie("shoppingCart", cartValue);
        cartCookie.setMaxAge(60 * 60 * 24 * 7); // 1周过期
        cartCookie.setPath("/");
        cartCookie.setHttpOnly(true);
        response.addCookie(cartCookie);
        
        out.println("<html><body>");
        out.println("<h1>Product Added to Cart: " + productId + "</h1>");
        out.println("<a href='/yourApp/viewCart'>View Cart</a>");
        out.println("</body></html>");
    }
}

@WebServlet("/viewCart")
public class ViewCartServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        Cookie[] cookies = request.getCookies();
        String cartValue = "";
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("shoppingCart".equals(cookie.getName())) {
                    cartValue = cookie.getValue();
                    break;
                }
            }
        }
        
        out.println("<html><body>");
        out.println("<h1>Your Shopping Cart</h1>");
        if (!cartValue.isEmpty()) {
            List<String> products = Arrays.asList(cartValue.split(","));
            out.println("<ul>");
            for (String product : products) {
                out.println("<li>Product ID: " + product + "</li>");
            }
            out.println("</ul>");
        } else {
            out.println("<p>Cart is empty.</p>");
        }
        out.println("</body></html>");
    }
}

@WebServlet("/clearCart")
public class ClearCartServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 删除购物车Cookie
        Cookie cartCookie = new Cookie("shoppingCart", "");
        cartCookie.setMaxAge(0);
        cartCookie.setPath("/");
        response.addCookie(cartCookie);
        
        response.sendRedirect("viewCart");
    }
}
 
使用说明
  1. 添加商品链接:<a href="/yourApp/addToCart?id=1">Add Product 1</a>
  2. 访问/viewCart查看购物车;/clearCart清空。
  3. 注意:实际应用中,商品ID应来自数据库,Cookie值需URL编码处理特殊字符。

案例 6:记住我功能

本案例实现"记住我"功能:用户登录时选择记住,服务器设置长期Cookie存储用户名(加密),下次自动填充。

完整代码
java 复制代码
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
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;

@WebServlet("/rememberMeLogin")
public class RememberMeLoginServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String rememberMe = request.getParameter("rememberMe"); // "on" if checked
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 模拟验证
        if ("admin".equals(username) && "password123".equals(password)) {
            // 设置会话Cookie
            Cookie sessionCookie = new Cookie("sessionID", "generatedSession123");
            sessionCookie.setMaxAge(60 * 60); // 1小时会话
            sessionCookie.setPath("/");
            sessionCookie.setHttpOnly(true);
            response.addCookie(sessionCookie);
            
            // 如果选择记住我,设置长期Cookie(值用简单Base64模拟加密)
            if ("on".equals(rememberMe)) {
                String encodedUsername = Base64.getEncoder().encodeToString(username.getBytes()); // 实际应使用强加密
                Cookie rememberCookie = new Cookie("rememberUser", encodedUsername);
                rememberCookie.setMaxAge(60 * 60 * 24 * 30); // 30天
                rememberCookie.setPath("/");
                rememberCookie.setHttpOnly(true);
                rememberCookie.setSecure(true);
                response.addCookie(rememberCookie);
            }
            
            out.println("<html><body>");
            out.println("<h1>Login Successful!</h1>");
            out.println("</body></html>");
        } else {
            out.println("<html><body>");
            out.println("<h1>Login Failed.</h1>");
            out.println("</body></html>");
        }
    }
}

@WebServlet("/autoLogin")
public class AutoLoginServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        
        // 检查记住我Cookie
        Cookie[] cookies = request.getCookies();
        String encodedUsername = null;
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("rememberUser".equals(cookie.getName())) {
                    encodedUsername = cookie.getValue();
                    break;
                }
            }
        }
        
        if (encodedUsername != null) {
            // 解密用户名(实际需安全解密)
            String username = new String(Base64.getDecoder().decode(encodedUsername));
            // 自动登录:设置会话Cookie
            Cookie sessionCookie = new Cookie("sessionID", "generatedSession123");
            sessionCookie.setMaxAge(60 * 60);
            sessionCookie.setPath("/");
            sessionCookie.setHttpOnly(true);
            response.addCookie(sessionCookie);
            
            out.println("<html><body>");
            out.println("<h1>Auto Login Successful! Welcome back, " + username + ".</h1>");
            out.println("</body></html>");
        } else {
            response.sendRedirect("login.html");
        }
    }
}
 
使用说明
  1. 登录表单添加复选框:

    html 复制代码
    <input type="checkbox" name="rememberMe"> Remember me
     
  2. 访问/autoLogin尝试自动登录。

  3. 注意:实际应用中,应使用强加密(如AES)存储用户名,并考虑安全风险。


6. 高级主题与注意事项

  • Cookie: 通常用于在客户端存储一个小的标识符(Session ID)。
  • Session: 指在服务器端存储的用户相关数据(如登录状态、购物车)。Session 数据存储在服务器内存、数据库或缓存(如 Redis)中。
  • 关系: 服务器在用户首次访问时创建一个唯一的 Session ID,通过 Cookie (JSESSIONID 是 Java EE 默认名) 发送给浏览器。浏览器后续请求携带此 ID,服务器通过 ID 查找对应的 Session 数据。因此,Cookie 是实现 Session 持久化的一种常用机制。
  • Token-Based Authentication (JWT etc.): 在认证成功后,服务器生成一个签名的 Token(包含用户信息、过期时间等),通常通过响应体(JSON)返回给客户端。客户端(通常是 Web 前端)将其存储在 localStoragesessionStorage 中,并在后续请求的 Authorization 头(如 Bearer <token>)中发送。这种方式更符合 RESTful 无状态设计,避免了 CSRF 问题(因为 Cookie 不是自动发送),但需要防范 XSS 窃取存储的 Token。
  • LocalStorage / SessionStorage: HTML5 提供的浏览器存储机制,容量更大(通常 5MB),数据不会随 HTTP 请求自动发送。适合存储非敏感的应用状态数据(如用户偏好)。同样需要防范 XSS。
6.3 浏览器限制与隐私政策 (GDPR, CCPA)
  • 浏览器限制: 如前所述,有大小和数量限制。第三方 Cookie(Domain 与当前页面 URL 不同的 Cookie)在现代浏览器中受到越来越严格的限制(如 Safari 默认完全阻止,Chrome 计划逐步淘汰)。
  • 隐私法规:
    • GDPR (欧盟通用数据保护条例): 要求网站在使用非必要的 Cookie(特别是用于跟踪的)之前,必须获得用户的明确同意(通过 Cookie 横幅)。必要的 Cookie(如会话管理、安全)通常不需要同意。
    • CCPA (加州消费者隐私法案): 类似地,赋予了用户选择不出售其个人信息的权利,这会影响跟踪 Cookie 的使用。
    • 怎么做: 部署 Cookie 横幅/同意管理平台 (CMP),允许用户管理其 Cookie 偏好。仔细审查你的 Cookie 使用,区分必要和非必要。

虽然 RESTful API 通常倾向于无状态并使用 Token 认证,但有时也需要使用 Cookie(例如,在服务端渲染的 Web 应用提供的 API 端点)。

  • 支持 Cookie: 客户端(浏览器、支持 Cookie 的 HTTP 客户端库)需要在请求中自动包含 Cookie。
  • 跨域问题 (CORS): 如果 API 和前端处于不同域名,浏览器默认不会发送 Cookie。需要在服务器端设置 CORS 响应头:
    • Access-Control-Allow-Origin: <具体前端域名> (不能是 *,需要明确指定)
    • Access-Control-Allow-Credentials: true
    • 前端在发起请求时(如使用 fetch)需要设置 credentials: 'include'
  • 安全考虑: 务必设置 HttpOnly, Secure, SameSite 等属性,并考虑签名/加密。

7. 总结

Cookie 是 Web 开发中管理状态的基础机制。掌握 Java (Servlet API) 和 Spring Boot 中操作 Cookie 的方法至关重要。在实现功能的同时,必须 将安全性放在首位:使用 HttpOnly 防御 XSS,使用 Secure 防御嗅探(HTTPS 下),使用 SameSite (通常 Lax) 防御 CSRF。避免在 Cookie 中存储敏感信息,考虑使用服务器端 Session 存储。注意隐私法规要求(GDPR/CCPA),提供 Cookie 同意管理。理解 Cookie 在会话管理中的作用以及与替代方案(Token、LocalStorage)的区别和适用场景。

通过本文的详细解释和可直接运行的 Spring Boot 案例,你应该能够自信地在你的 Java Web 应用中安全有效地使用 Cookie 了。

相关推荐
红烧柯基2 小时前
nohup java -jar运行jar包时设置启动参数
java·开发语言·jar
AAD555888992 小时前
YOLO13-C3k2-FDConv:足球检测与定位的创新应用
python
2301_822377652 小时前
模板代码异常处理
开发语言·c++·算法
hcnaisd22 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
多恩Stone2 小时前
【3DV 进阶-12】Trellis.2 数据处理脚本细节
人工智能·pytorch·python·算法·3d·aigc
kylezhao20192 小时前
深入浅出理解 C# WPF 的事件
开发语言·c#·wpf
极客小云2 小时前
【基于AI的自动商品试用系统:不仅仅是虚拟试衣!】
javascript·python·django·flask·github·pyqt·fastapi
Warren982 小时前
一次文件上传异常的踩坑、定位与修复复盘(Spring Boot + 接口测试)
java·开发语言·spring boot·笔记·后端·python·面试
JMchen1232 小时前
Android相机硬件抽象层(HAL)逆向工程:定制ROM的相机优化深度指南
android·开发语言·c++·python·数码相机·移动开发·android studio