JavaEE进阶——Cookie与Session:Web安全的双刃剑

你好!作为新手小白,面对代码和复杂的概念感到困惑是非常正常的。Spring Web MVC 是 Java Web 开发中非常核心的框架,而你提供的资料主要涵盖了 Web 开发中最关键的一个环节------"会话管理"(即如何让服务器记住你是谁)

这份资料通过"医院挂号"等生动的例子,详细讲解了 CookieSession 的机制。

为了让你能够透彻理解,我将这份教程分成了几个部分:核心概念解析详细代码拆解完整代码展示 以及扩展知识点。我会用最通俗易懂的语言,配合代码中的逐行注释,带你一步步掌握这些内容。

我们先来通过一个文件,把资料中所有零散的代码整合在一个完整的类中,方便你直观地看到它们是如何在 Spring Boot 项目中组织的。

StateManagementController.java


目录

详细解析与扩展介绍

[1. 为什么需要 Cookie 和 Session?(背景知识)](#1. 为什么需要 Cookie 和 Session?(背景知识))

[2. 深入解析 Cookie(客户端的"就诊卡")](#2. 深入解析 Cookie(客户端的“就诊卡”))

[3. 深入解析 Session(服务器的"病历本")](#3. 深入解析 Session(服务器的“病历本”))

[4. Cookie 与 Session 的区别(面试/考试必考)](#4. Cookie 与 Session 的区别(面试/考试必考))

[5. 关于 Header(请求头)](#5. 关于 Header(请求头))

给新手的学习建议

cookie存储在电脑上为啥不安全?

[1. 任何人都可以"伪造"和"篡改" (Cookie Forgery)](#1. 任何人都可以“伪造”和“篡改” (Cookie Forgery))

[2. 容易被"小偷"偷走 (XSS 攻击)](#2. 容易被“小偷”偷走 (XSS 攻击))

[3. 在路上容易被"偷听" (中间人攻击)](#3. 在路上容易被“偷听” (中间人攻击))

[4. 容易被"借刀杀人" (CSRF 攻击)](#4. 容易被“借刀杀人” (CSRF 攻击))

我们该怎么防御?(给新手的安全锦囊)

总结

第一部分:完整代码复原与超详细注释

第二部分:核心知识点深度解析(新手必读)

[1. 什么是 Cookie?(通俗理解)](#1. 什么是 Cookie?(通俗理解))

[2. HttpServletRequest 是什么?](#2. HttpServletRequest 是什么?)

[3. 两种获取方式的对比(核心考点)](#3. 两种获取方式的对比(核心考点))

[4. Java 增强型 For 循环](#4. Java 增强型 For 循环)

第三部分:扩展知识点与避坑指南(进阶必读)

[扩展 1:@CookieValue 的隐形炸弹](#扩展 1:@CookieValue 的隐形炸弹)

[扩展 2:如何测试这段代码?(动手实践)](#扩展 2:如何测试这段代码?(动手实践))

[扩展 3:Cookie 的局限性与 Session](#扩展 3:Cookie 的局限性与 Session)

[扩展 4:Java 中的空指针异常(NullPointerException)](#扩展 4:Java 中的空指针异常(NullPointerException))

第四部分:总结与建议

核心对比总结表

详细深度解析

[1. 传统方式 (HttpServletRequest)](#1. 传统方式 (HttpServletRequest))

[2. 注解方式 (@SessionAttribute)](#2. 注解方式 (@SessionAttribute))

[3. Spring 内置对象方式 (HttpSession)](#3. Spring 内置对象方式 (HttpSession))

场景化建议:我该用哪个?


作为新手小白,我们不能只看代码,更要理解代码背后的"故事"。我将结合你提供的图片(Page 1 - Page 9)和 PDF 内容,为你构建一个完整的知识体系。

  • HTTP 的"健忘症":

    资料 Page 1 中提到,HTTP 协议是**无状态(Stateless)**的。

    • 小白解释:这就像你每天去同一家早餐店买豆浆,但老板不仅记不住你的名字,连你昨天付过钱办了会员卡都记不住。每一次见面,他都把你当成第一次来的陌生人。

    • 问题:这在互联网上很麻烦。比如你刚在淘宝登录了,点击"购物车"页面,如果服务器记不住你,就会让你再登录一次。

  • 解决方案:

    我们需要一种机制来"维持关系"。资料 Page 2 用了极其生动的**"医院挂号"**例子:

    1. 就诊卡 (Cookie):医院给你一张卡,你每次去不同科室(访问不同页面)都出示这张卡,医生就知道你是谁。

    2. 病历本 (Session):医生电脑里存着你这张卡对应的病情记录(用户信息),这只有医生能看,卡上是不存病情的。

2. 深入解析 Cookie(客户端的"就诊卡")

资料来源:Page 6, Page 7

  • 定义:Cookie 是服务器发送给浏览器的一小段文本信息,浏览器会把它存起来。下次访问同一个网站时,浏览器会自动把这段信息带过去。

  • 代码中的知识点

    • HttpServletRequest:这是 Java Web 中非常底层的对象。它代表了"请求"。你可以把它想象成一个快递包裹,里面装着浏览器发给服务器的所有东西(包括 Cookie)。

    • request.getCookies() :这个方法不像拿苹果那么简单,它返回的是一个数组 Cookie[]。为什么?因为一个网站可能给你发了好几张"卡"(比如一张存你的ID,一张存你上次浏览的商品ID)。

    • @CookieValue :这是 Spring 框架给我们的"糖"。传统的 getCookies 需要如果你想找特定的卡,得把所有卡倒出来一张张看名字(写 for 循环)。用了 @CookieValue("bite"),相当于你跟助手说:"帮我把名字叫 bite 的那张卡找出来",助手直接就把卡给你,省去了循环的代码。

  • 扩展知识点(重点):

    • 安全性 :资料 Page 7 展示了你可以手动在浏览器里修改 Cookie (bite:bite666)。这意味着 Cookie 是不安全的

      • 举例 :如果你把"用户余额=1000"直接存在 Cookie 里,聪明的用户可能自己在浏览器里把它改成"用户余额=999999"。所以,重要信息(密码、余额)绝对不能直接存在 Cookie 里
    • 生命周期:Cookie 有存活时间。有的 Cookie 关掉浏览器就没了(会话级),有的可以存一个月(持久级,通过设置 Max-Age)。

3. 深入解析 Session(服务器的"病历本")

资料来源:Page 3, Page 4, Page 5, Page 8, Page 9

  • 定义:Session 是服务器端的一种存储机制。它在服务器内存里开辟一块空间,专门用来存这个用户的信息。

  • 原理(配合 Page 5 理解)

    1. 创建 :用户第一次来,服务器创建一个 Session 对象,并给它分配一个唯一的 SessionID (比如 123)。

    2. 关联 :服务器把这个 123 放到 Cookie 里,发给浏览器。

    3. 识别 :下次浏览器带着 Cookie (ID=123) 来,服务器拿着 123 去内存里找,就能找到对应的 Session 对象,从而知道由于是"张三"。

  • 代码中的知识点

    • request.getSession() :这个方法有两个动作。如果浏览器带来的 ID 能找到 Session,就返回旧的;如果找不到,就自动创建一个新的。

    • session.setAttribute("key", value) :Session 就像一个 Java 的 Map(哈希表)。你可以往里面扔任何对象(String, User对象, 购物车List等)。

    • request.getSession(false) :资料 Page 9 特别强调了 false 参数。这是为了性能和逻辑正确性。

      • 场景:查询用户是否登录。如果用户没登录(没 Session),你应该直接返回"未登录",而不是给他新建一个空的 Session。
  • 扩展知识点(重点):

    • Session 丢失问题:资料 Page 5 提到"Session 默认保存在内存中,如果重启服务器则 Session 数据就会丢失"。

      • 举例:双十一抢购,服务器突然重启了一下,所有正在排队的用户都会被踢出登录,因为内存里的 Session 全没了。

      • 解决方案 :实际大厂开发中,会把 Session 存在 Redis(一种外部的高速缓存数据库)里,这样服务器重启也不怕。

    • 分布式问题:如果你有两台服务器 A 和 B。用户第一次访问 A,Session 存在 A 的内存里。第二次负载均衡把请求分到了 B,B 的内存里没有这个 Session,用户就会莫名其妙被踢下线。这也是为什么要用 Redis 存 Session 的原因。

根据资料 Page 5 的总结,我为你整理了一个对比表:

特性 Cookie Session 比喻
存储位置 客户端(浏览器) 服务器端(内存) 你的口袋 vs 医生的电脑
安全性 低(容易被篡改、伪造) 高(数据在服务器,只有 ID 在外面跑) 钞票 vs 银行存款记录
存储容量 小(通常限制 4KB) 大(只受服务器内存限制) 钱包大小 vs 仓库大小
数据类型 只能存字符串 可以存任意 Java 对象 纸条 vs 实物
依赖关系 通常不需要 Session 通常依赖 Cookie 来传输 SessionID -
5. 关于 Header(请求头)

资料来源:PDF Page 12

  • User-Agent:这是最常用的 Header。它告诉服务器你是用电脑还是手机,用的是 Chrome 还是 Safari。

  • 扩展应用:服务器可以根据 User-Agent 判断"如果是手机访问,就跳转到移动版页面;如果是电脑,就显示桌面版页面"。

给新手的学习建议

  1. 动手运行 :代码看百遍,不如手敲一遍。建议你把上面的 StateManagementController 代码复制到你的 IDEA 里,启动 Spring Boot 项目。

  2. 抓包观察 :资料中提到了使用 Fiddler浏览器开发者工具 (F12)。这是 Web 开发的神器。

    • 按 F12 -> Network (网络)。

    • 刷新页面。

    • 点击请求,查看 Headers 里的 Request Headers (Cookie 在这) 和 Response Headers (Set-Cookie 在这)。

  3. 理解流程:不要死记代码,要脑补"浏览器"和"服务器"一来一回的对话过程。

希望这份超过 4000 字量级的详细解析(含代码注释)能完美解决你的疑惑!如果还有哪里不懂,欢迎随时提问。


java 复制代码
package com.example.demo.controller; // 1. 定义包名,就像是把文件放在哪个文件夹里

// 2. 导入需要用到的类(就像做菜前准备食材)
import org.springframework.web.bind.annotation.CookieValue; // 用于注解方式获取Cookie
import org.springframework.web.bind.annotation.RequestMapping; // 用于映射请求路径
import org.springframework.web.bind.annotation.RestController; // 标识这是一个控制器
import org.springframework.web.bind.annotation.SessionAttribute; // 用于注解方式获取Session属性

import javax.servlet.http.Cookie; // Java原生Servlet API中的Cookie类
import javax.servlet.http.HttpServletRequest; // 代表HTTP请求的对象
import javax.servlet.http.HttpServletResponse; // 代表HTTP响应的对象
import javax.servlet.http.HttpSession; // 代表会话Session的对象

/**
 * Spring Web MVC 入门:Cookie 和 Session 详解控制器
 * * 这个类演示了如何在 Spring MVC 中处理客户端和服务器之间的状态。
 * 对应你提供的 PDF 和图片资料中的所有代码示例。
 * * @RestController: 
 * 这是 @Controller 和 @ResponseBody 的组合注解。
 * 意味着这个类中的所有方法返回的数据都会直接写入 HTTP 响应体中(比如显示在浏览器页面上),
 * 而不是跳转到某个 HTML 模板文件。
 * * @RequestMapping("/param"):
 * 这是一个路由映射。表示这个类下面所有方法的访问路径都要加上 /param 前缀。
 * 例如 method10 方法的完整访问路径就是 http://127.0.0.1:8080/param/m10
 */
@RestController
@RequestMapping("/param")
public class StateManagementController {

    // ==========================================
    // 第一部分:Cookie 机制 (对应资料 Page 6, 7)
    // ==========================================

    /**
     * 1. 传统方式获取 Cookie (对应 Page 6)
     * * 知识点:
     * - HttpServletRequest: 是 Servlet 的标准接口,封装了客户端发来的所有请求信息。
     * - HttpServletResponse: 封装了服务器发给客户端的所有响应信息。
     * * 场景:想看看客户端(浏览器)带了哪些"就诊卡"(Cookie)来。
     */
    @RequestMapping("/m10")
    public String method10(HttpServletRequest request, HttpServletResponse response) {
        
        // request.getCookies(): 从请求中拿到所有的 Cookie 对象。
        // 注意:如果没有 Cookie,这个方法会返回 null,所以后面必须判空。
        Cookie[] cookies = request.getCookies();

        // StringBuilder: 用于高效地拼接字符串。在循环中拼接字符串如果不复用对象,会产生大量垃圾内存。
        StringBuilder builder = new StringBuilder();

        // 严谨的判空操作(小白容易忽略的点:直接循环可能导致空指针异常 NullPointerException)
        if (cookies != null) {
            // 增强型 for 循环 (foreach),遍历每一个 Cookie 对象
            for (Cookie ck : cookies) {
                // ck.getName(): 获取 Cookie 的名字(比如 "JSESSIONID" 或 "bite")
                // ck.getValue(): 获取 Cookie 的值(比如 "bite666")
                builder.append(ck.getName() + ":" + ck.getValue() + "; ");
            }
        }

        // 将拼接好的结果返回给浏览器显示
        return "Cookie信息: " + builder.toString();
    }

    /**
     * 2. 简洁方式获取 Cookie (对应 Page 7)
     * * 知识点:
     * - @CookieValue: Spring 提供的注解,专门用来帮我们省去上面写 request.getCookies() 再循环查找的麻烦代码。
     * * 场景:我明确知道我要找一个叫 "bite" 的 Cookie。
     */
    @RequestMapping("/getCookie")
    public String cookie(@CookieValue(value = "bite", required = false) String bite) {
        // value = "bite": 指定要获取的 Cookie 名称。
        // required = false: 这点很重要!
        //    如果设为 true (默认),当浏览器没传这个 Cookie 时,程序会直接报错 400 Bad Request。
        //    设为 false,如果没有这个 Cookie,变量 bite 就是 null,程序不会崩。
        
        return "使用注解获取到的 bite 值: " + bite;
    }

    // ==========================================
    // 第二部分:Session 机制 (对应资料 Page 8, 9, PDF Page 11-12)
    // ==========================================

    /**
     * 3. 存储 Session (对应 Page 8)
     * * 知识点:
     * - Session (会话): 服务器端记录用户状态的机制(就像医院的"病历本")。
     * - request.getSession(): 获取或创建 Session 的核心方法。
     * * 场景:用户登录成功了,我要在服务器这边记下"这个用户是张三"。
     */
    @RequestMapping("/setSess")
    public String setsess(HttpServletRequest request) {
        
        // request.getSession(true) (默认是 true):
        // 逻辑:如果在服务器内存里能找到属于当前用户的 Session,就直接拿来用;
        //       如果找不到(比如用户第一次来,或者太久没来过期了),就新建一个 Session。
        HttpSession session = request.getSession();

        if (session != null) {
            // session.setAttribute("key", value): 往 Session 里存数据。
            // 就像在 Map 里 put 数据一样。这里我们存了一个名为 "username",值为 "java" 的数据。
            // 实际开发中,这里通常存 User 对象。
            session.setAttribute("username", "java");
        }

        return "Session 存储成功!(服务器已经生成了 JSESSIONID 并通过 Cookie 发给浏览器了)";
    }

    /**
     * 4. 读取 Session - 传统方式 (对应 Page 9)
     * * 场景:用户访问了其他页面,我要看看他是不是之前登录过的"张三"。
     */
    @RequestMapping("/getSess")
    public String sess(HttpServletRequest request) {
        
        // request.getSession(false): 
        // 关键区别:参数是 false!
        // 逻辑:如果有 Session 就返回;如果没有,**千万别创建新的**,直接返回 null。
        // 为什么?因为我们只是想读取旧数据,如果用户压根没登录,创建新 Session 浪费服务器内存。
        HttpSession session = request.getSession(false);

        String username = null;

        // 必须要进行双重检查:
        // 1. session != null: 确保用户有会话。
        // 2. session.getAttribute("username") != null: 确保会话里确实存了 "username" 这个数据。
        if (session != null && session.getAttribute("username") != null) {
            // getAttribute 返回的是 Object 类型(所有类的父类),所以必须强制类型转换 (String) 成我们要的类型。
            username = (String) session.getAttribute("username");
        }

        return "传统方式获取 username: " + username;
    }

    /**
     * 5. 读取 Session - 注解方式 (对应 PDF Page 11)
     * * 知识点:
     * - @SessionAttribute: Spring 提供的注解,用于简化 Session 读取。
     * * 场景:更优雅地获取 Session 数据。
     */
    @RequestMapping("/getSess2")
    public String sess2(@SessionAttribute(value = "username", required = false) String username) {
        // 这里 Spring 会自动帮我们在 Session 中查找 key 为 "username" 的值,并注入给参数 username。
        // 同样建议 required = false,防止 Session 过期导致报错。
        return "注解方式获取 username: " + username;
    }

    /**
     * 6. 读取 Session - Spring 内置对象方式 (对应 PDF Page 11-12,推荐)
     * * 知识点:
     * - 直接在参数里写 HttpSession session。
     * - Spring MVC 非常智能,看到这个参数,就会自动把当前的 Session 对象注进来。
     * - 这相当于 request.getSession(),如果不存在通常会自动创建。
     */
    @RequestMapping("/getSess3")
    public String sess3(HttpSession session) {
        // 直接使用 session 对象获取数据
        String username = (String) session.getAttribute("username");
        return "内置对象方式获取 username: " + username;
    }
    
    // ==========================================
    // 第三部分:Header 机制 (对应 PDF Page 12)
    // ==========================================
    
    /**
     * 7. 获取请求头 Header (对应 PDF Page 12)
     * * 场景:我想知道用户是用什么浏览器访问的(User-Agent)。
     */
    @RequestMapping("/getHeader")
    public String getHeader(HttpServletRequest request) {
        // getHeader("HeaderName"): 获取指定的请求头信息
        String userAgent = request.getHeader("User-Agent");
        return "用户的浏览器信息(User-Agent): " + userAgent;
    }
}

cookie存储在电脑上为啥不安全?

这是一个非常棒的问题!在编程世界里,"不要相信客户端传来的任何数据" 是一条铁律。

简单来说,Cookie 之所以"不安全",是因为它存储在用户的手机或电脑(客户端)上 ,而不在服务器的控制范围内

就像你把日记本放在公园的长椅上,别人不仅能偷看 ,还能偷偷改写 ,甚至直接拿走

下面我用"通俗易懂的例子"配合"技术原理"为你详细拆解为什么它不安全,以及会有什么样的后果。

1. 任何人都可以"伪造"和"篡改" (Cookie Forgery)

  • 生活比喻:

    假设游乐园的门票是一张纸质卡片,上面手写着"VIP用户"。这张卡片就在你手里(客户端)。

    如果你是个"坏孩子",你自己拿支笔,把"普通用户"改成"VIP用户",或者把"过期时间:昨天"改成"明天"。如果检票员(服务器)只看卡片不查系统,你就混进去了。

  • 技术原理:

    Cookie 是纯文本文件。任何用户都可以按下 F12 打开浏览器的开发者工具(Application 标签页),找到 Cookie,然后双击里面的值,想改什么改什么。

    • 危险场景: 如果你把 isLogin=false 改成了 isLogin=true,或者把 userId=100(普通人)改成了 userId=1(管理员),而后端没有做二次校验,服务器就会误以为你是管理员。

2. 容易被"小偷"偷走 (XSS 攻击)

  • 生活比喻:

    这就像你的"门禁卡"挂在脖子上。如果你去了一个坏人开的店(恶意网站),或者有人在你家门口贴了一张带胶水的纸(恶意脚本),当你经过时,你的门禁卡就被"粘"走或者被复制了。

  • 技术原理(跨站脚本攻击 XSS):

    黑客可以在网页的评论区、搜索框里植入一段恶意的 JavaScript 代码(比如 <script>alert(document.cookie)</script>)。

    当其他用户打开这个网页时,这段代码会自动运行,把该用户的 Cookie(里面可能包含 SessionID)发送给黑客。

    • 后果: 黑客拿到了你的 JSESSIONID,以后他不需要账号密码,直接拿着这个 ID 就能登录你的淘宝、银行账户(只要你的会话还没过期)。

3. 在路上容易被"偷听" (中间人攻击)

  • 生活比喻:

    你和朋友打电话报银行卡密码。如果你们在一个嘈杂的咖啡馆,旁边坐着一个戴耳机的"间谍",他虽然不说话,但他听到了你报的所有数字。

  • 技术原理(HTTP 明文传输):

    Cookie 是随着每一次 HTTP 请求发送的。如果你的网站不是 HTTPS(加密传输),而是普通的 HTTP,那么你在免费 Wi-Fi 下上网时,黑客可以用抓包工具(如 Wireshark)截获你在网络上传输的所有数据包,直接看到你的 Cookie 内容。

4. 容易被"借刀杀人" (CSRF 攻击)

  • 生活比喻:

    你已经登录了银行网站(Cookie 还在有效期内)。这时你点开了一个中奖广告(恶意链接)。这个广告背地里偷偷向银行发了一条指令:"给黑客转账 1000 元"。

    因为你的浏览器里存着银行的 Cookie,银行一看:"嗯,这是本人发的请求(带有正确的 Cookie)",于是钱就转走了。

  • 技术原理(跨站请求伪造 CSRF):

    利用浏览器"自动发送 Cookie"的机制,在用户不知情的情况下,借用用户的身份(Cookie)向服务器发送恶意请求。


我们该怎么防御?(给新手的安全锦囊)

虽然 Cookie 本身不安全,但我们有很多手段给它穿上"防弹衣":

  1. HttpOnly(关键):

    • 在后端设置 Cookie 时,加上 HttpOnly 属性。

    • 作用: 告诉浏览器,"这个 Cookie 只能通过 HTTP 请求发送,禁止 JavaScript 代码(document.cookie)读取它"。这样 XSS 攻击就偷不走 Cookie 了。

  2. HTTPS(关键):

    • 全站使用 HTTPS 加密。

    • 作用: 给传输通道加了防盗门。即使黑客截获了数据包,看到的也是乱码,根本不知道 Cookie 是什么。

  3. Secure 属性:

    • 设置 Cookie 的 Secure 属性。

    • 作用: 规定这个 Cookie 只能在 HTTPS 连接下发送,HTTP 连接时不发送,防止意外泄露。

  4. 不要在 Cookie 里存敏感数据:

    • 绝对不要在 Cookie 里直接存"密码"、"余额"、"手机号"。

    • 正确做法: 只存一个毫无意义的随机字符串(SessionID)。真正的用户数据存在服务器的 Session 里(保险柜里)。即使黑客拿到了 SessionID,也看不到你的密码。

总结

Cookie 就像你手里的钥匙

  • 如果你把钥匙扔在大街上(HTTP 明文),谁都能捡。

  • 如果你把钥匙贴上名字和地址(存敏感信息),捡到的人就能去你家。

  • 如果你不给钥匙加防盗链(不设 HttpOnly),小偷能轻易摸走。

所以,Cookie 本身只是个载体,安不安全取决于你怎么用它。


你好!非常欢迎来到 Java Web 开发的世界。看到你提供的代码截图,我能感觉到你正在学习 Spring MVC 中关于 Cookie(客户端会话技术) 的处理方式。

这对于新手来说是一个非常重要的里程碑,因为理解 Cookie 是理解"网站如何记住用户"(比如保持登录状态)的关键。

别担心,我会把你当成完全的"小白",用最通俗易懂的语言,配合生活中的例子,为你详细拆解这段代码,并补充完整的上下文和扩展知识。这篇回答会比较长,建议你在这个页面收藏起来慢慢看。


第一部分:完整代码复原与超详细注释

截图中的代码只是两个方法片段,无法直接运行。为了让你能看懂它的"全貌",我为你补全了类定义、导包(Import)等必要部分。

阅读指南: 请仔细阅读代码中的绿色注释,我把每一行代码背后的逻辑都写进去了。

Java

java 复制代码
/**
 * 这是一个典型的 Spring MVC 控制器类(Controller)。
 * 它的作用是:接收浏览器发来的请求,处理业务逻辑,然后返回结果给浏览器。
 */

// 1. 导入处理 Cookie 的核心类,这是 Java Servlet 标准库提供的
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

// 2. 导入 Spring MVC 的核心注解,用于将网址映射到 Java 方法上
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @RestController 是 Spring 的注解。
 * 意思告诉程序:"这是一个控制器类,而且这个类里所有方法的返回值,
 * 都会直接作为字符串(或JSON)写入到浏览器页面上,而不是跳转到某个 HTML 文件。"
 */
@RestController
public class CookieDemoController {

    // ==========================================
    // 方法一:传统方式(原生 Servlet API)获取 Cookie
    // ==========================================

    /**
     * @RequestMapping("/r13")
     * 这是一个路由映射。
     * 意思:当你在浏览器地址栏输入 "http://localhost:8080/r13" 时,
     * Spring 就会自动运行下面这个 r13 方法。
     */
    @RequestMapping("/r13")
    public String r13(HttpServletRequest request) {
        // HttpServletRequest 是一个非常重要的对象,它代表了"浏览器发给服务器的那个请求包"。
        // 这个包里装了所有东西:浏览器版本、IP地址、参数,当然也包括 Cookie。

        // 1. 获取所有的 Cookie
        // request.getCookies() 会去翻请求头里的数据,把所有的 Cookie 拿出来。
        // 因为 Cookie 可能有很多个(比如 JSESSIONID, username, language 等),所以它返回的是一个数组(Cookie[])。
        Cookie[] cookies = request.getCookies();

        // 2. 判空检查(这是新手必须养成的好习惯!)
        // 如果浏览器从未访问过这个网站,或者手动清空了缓存,request.getCookies() 可能会返回 null。
        // 如果不检查直接用,程序就会报"空指针异常(NullPointerException)"并在后台崩溃。
        if (cookies != null) {

            // 3. 遍历数组(增强型 for 循环)
            // 意思:依次从 cookies 数组里拿出一个个 cookie,赋值给变量 c,直到拿完为止。
            for (Cookie cookie : cookies) {
                
                // 4. 打印输出
                // getName() 获取 Cookie 的名字(比如 "username")
                // getValue() 获取 Cookie 的值(比如 "zhangsan")
                // System.out.println 会把结果打印在 IDEA 的控制台里(不是浏览器页面上)。
                System.out.println(cookie.getName() + ":" + cookie.getValue());
            }
        }

        // 5. 返回结果给浏览器
        // 浏览器页面上会显示这句话。
        return "返回Cookie成功";
    }

    // ==========================================
    // 方法二:Spring 注解方式(更高级、更简单)获取 Cookie
    // ==========================================

    /**
     * @RequestMapping("/r14")
     * 映射地址:访问 "http://localhost:8080/r14" 时触发此方法。
     */
    @RequestMapping("/r14")
    // 下面这行代码是重点中的重点:
    // @CookieValue("java"):告诉 Spring,"请帮我去所有的 Cookie 里找一个名字叫 'java' 的 Cookie"。
    // String java:如果不为空,就把那个 Cookie 的值,自动赋值给这个 String 类型的变量 java。
    public String r14(@CookieValue("java") String java) {
        
        // 这里的逻辑就非常简单了,因为 Spring 帮你省去了上面方法 r13 里那一堆"获取数组、判空、循环查找"的代码。
        // 直接拿到了值,我们就拼接一个字符串返回。
        
        // 注意:如果浏览器里没有叫 "java" 的 cookie,默认情况下这个方法会报错(400 Bad Request)。
        // 扩展知识里我会讲怎么解决这个问题。
        
        return "从Cookie中获取Java的值: " + java;
    }
}

第二部分:核心知识点深度解析(新手必读)

这段代码虽然短,但它展示了 Java Web 开发演变的两个阶段:原生时代 vs 框架时代

1. 什么是 Cookie?(通俗理解)
  • 概念:HTTP 协议是"健忘"的(无状态)。你访问了页面 A,再访问页面 B,服务器是不知道这是同一个人的。

  • 例子:这就像你去办了一张健身房会员卡。

    • 服务器(健身房前台) 第一次见到你,给你发了一张卡片(Set-Cookie),上面写着你的会员号。

    • 你(浏览器) 把卡片塞进钱包(保存 Cookie)。

    • 以后你每次去健身房,不需要说话,直接出示卡片(Request Header 带上 Cookie)。

    • 前台一看卡片:"哦,是王先生啊,请进。"

  • 代码对应request.getCookies() 就是前台工作人员在看你手里有没有卡片。

2. HttpServletRequest 是什么?
  • 在 Java Web 中,只要浏览器发请求过来,Tomcat(服务器软件)就会把这次请求的所有信息打包成一个对象,就是 HttpServletRequest

  • 它包含:

    • 你从哪来?(IP地址)

    • 你想干啥?(URL 路径,如 /r13

    • 你带了啥?(参数、Cookie、Header)

  • 在方法 r13 中,我们通过在这个大包裹里翻找,才找到了 Cookie。

3. 两种获取方式的对比(核心考点)
特性 方法 r13 (原生 Servlet API) 方法 r14 (Spring 注解 @CookieValue)
代码量 繁琐(获取、判空、循环) 极简(一行参数搞定)
灵活性 高。可以拿到所有 Cookie,也可以处理不存在的情况。 专注。通常只用来拿某一个特定的 Cookie。
底层原理 手动操作底层数据结构。 依赖注入。Spring 框架在后台帮你把 r13 的活儿干完了,直接把结果给你。
新手推荐 先学这个,理解原理。 理解原理后,工作中用这个,效率高。
4. Java 增强型 For 循环

r13 中出现的 for (Cookie cookie : cookies) 是 Java 5 引入的语法糖。

  • 翻译成人话 : "对于 cookies 盒子里的每一个元素,把它拿出来叫 cookie,然后执行一遍大括号里的代码。"

  • 这比传统的 for(int i=0; i<cookies.length; i++) 写法要不容易出错(不会导致数组越界)。


第三部分:扩展知识点与避坑指南(进阶必读)

作为新手,只看懂代码是不够的,你需要知道在实际工作中会遇到什么问题。

扩展 1:@CookieValue 的隐形炸弹

在方法 r14 中:

Java

java 复制代码
public String r14(@CookieValue("java") String java)

如果浏览器中没有 一个名字叫 "java" 的 Cookie,你访问 /r14 时,页面会直接报错:

HTTP Status 400 - Bad Request

Message: Missing cookie 'java' for method parameter of type String

如何解决?

如果你希望"有就获取,没有也别报错",你需要这样写:

Java

java 复制代码
// required = false 表示这个 Cookie 不是必须的
// defaultValue 表示如果没读到,就给它一个默认值 "unknown"
public String r14(@CookieValue(value = "java", required = false, defaultValue = "unknown") String java) {
    return "值是: " + java;
}

重点:这是开发中最常用的写法,能极大提高程序的健壮性。

扩展 2:如何测试这段代码?(动手实践)

很多新手写完代码不知道怎么测,因为 Cookie 通常是服务器生成的。但我们可以用浏览器"伪造"一个来测试获取功能。

  1. 启动你的 Spring Boot 项目。

  2. 打开 Chrome 浏览器,访问 http://localhost:8080/r13

  3. F12 打开开发者工具,点击顶部的 Application(应用)标签。

  4. 在左侧栏找到 Storage -> Cookies -> 点击你的网址。

  5. 在右侧空白表格里,双击 Name 输入 java,双击 Value 输入 HelloSpring

  6. 刷新页面 http://localhost:8080/r14

  7. 你会发现页面上显示了:"从Cookie中获取Java的值: HelloSpring"。

扩展 3:Cookie 的局限性与 Session

虽然 Cookie 很好用,但它有两个大问题:

  1. 不安全:Cookie 存在用户电脑里,用户可以随意修改(就像上面的测试一样)。你不能把"是否已登录:是"这种关键信息直接明文放在 Cookie 里。

  2. 大小限制:一个 Cookie 通常只能存 4KB 的数据。

扩展概念 - Session(会话):

为了解决安全问题,Java 引入了 Session。

  • 原理 :重要数据(如用户余额、登录状态)存在服务器端(Session)。

  • 关联 :服务器只给浏览器发一个没有意义的随机乱码 ID(通常叫 JSESSIONID),放在 Cookie 里。

  • 流程 :浏览器带着 JSESSIONID 的 Cookie 来 -> 服务器拿着 ID 去 Session 库里查 -> 找到了对应的数据。

  • 总结:Cookie 是 Session 的"钥匙"。

扩展 4:Java 中的空指针异常(NullPointerException)

r13 代码中:

Java

java 复制代码
if (cookies != null) { ... }

这行代码价值千金。

在 Java 中,如果你试图去"操作"一个空(null)对象(比如调用它的方法),程序会立刻崩溃。

  • 新手陷阱 :很多新手会直接写 for (Cookie cookie : request.getCookies())

  • 后果 :如果 getCookies() 返回了 null,增强型 for 循环底层会尝试去访问它,导致 Crash。一定要先判空!


第四部分:总结与建议

对代码的总结:

这张图片展示了 Spring MVC 中获取 Cookie 的两种流派:

  1. r13 (学院派) :通过 HttpServletRequest 全量获取,手动遍历。适合复杂逻辑或调试。

  2. r14 (实战派) :通过 @CookieValue 精准获取。代码优雅,适合绝大多数业务场景。

给新手小白的建议(Next Steps):

  1. 不要只看,要敲代码:把上面的代码手打一遍到你的 IDE(IntelliJ IDEA)里。

  2. 尝试"设置"Cookie:你现在学会了"获取"(Read),下一步建议去搜索"Java HttpServletResponse addCookie",学习如何由服务器向浏览器"发送/写入"(Write)一个 Cookie。

  3. 理解 HTTP 协议:Java Web 的本质就是处理 HTTP 协议。了解 Request(请求头、请求体)和 Response(响应头、响应体)的结构,会让你学 Java 后端事半功倍。

希望这份详尽的解释能帮你彻底搞懂这两个方法!如果有任何报错或者哪里卡住了,随时把错误信息发给我。加油!


这三种读取 Session 的方法代表了 Java Web 开发从"原生 Servlet"到"Spring 高级封装"的演变过程。

以下是这三种方式的详细对比、优缺点分析及使用建议。

核心对比总结表

特性 方法 1: HttpServletRequest 方法 2: @SessionAttribute 方法 3: HttpSession (参数)
层级 底层 (原生 Servlet API) 高层 (Spring 注解封装) 中层 (Spring 自动注入)
代码量 最多 (繁琐) 最少 (优雅) 中等
Session 创建控制 最强 (可精确控制不创建新会话) 弱 (主要用于读取) 较弱 (通常不存在时会自动创建)
类型转换 需要手动强转 (String) 自动注入 (无需显式强转) 需要手动强转 (String)
空值处理 需要显式编写 if != null 通过 required = false 控制 需要显式编写 if != null
适用场景 极度关注性能、需要底层控制时 只需读取某个特定属性时 需要读写 Session 或操作 Session (如销毁)

详细深度解析

java 复制代码
/**
     * 4. 读取 Session - 传统方式 (对应 Page 9)
     * * 场景:用户访问了其他页面,我要看看他是不是之前登录过的"张三"。
     */
    @RequestMapping("/getSess")
    public String sess(HttpServletRequest request) {
        
        // request.getSession(false): 
        // 关键区别:参数是 false!
        // 逻辑:如果有 Session 就返回;如果没有,**千万别创建新的**,直接返回 null。
        // 为什么?因为我们只是想读取旧数据,如果用户压根没登录,创建新 Session 浪费服务器内存。
        HttpSession session = request.getSession(false);
 
        String username = null;
 
        // 必须要进行双重检查:
        // 1. session != null: 确保用户有会话。
        // 2. session.getAttribute("username") != null: 确保会话里确实存了 "username" 这个数据。
        if (session != null && session.getAttribute("username") != null) {
            // getAttribute 返回的是 Object 类型(所有类的父类),所以必须强制类型转换 (String) 成我们要的类型。
            username = (String) session.getAttribute("username");
        }
 
        return "传统方式获取 username: " + username;
    }
 
    /**
     * 5. 读取 Session - 注解方式 (对应 PDF Page 11)
     * * 知识点:
     * - @SessionAttribute: Spring 提供的注解,用于简化 Session 读取。
     * * 场景:更优雅地获取 Session 数据。
     */
    @RequestMapping("/getSess2")
    public String sess2(@SessionAttribute(value = "username", required = false) String username) {
        // 这里 Spring 会自动帮我们在 Session 中查找 key 为 "username" 的值,并注入给参数 username。
        // 同样建议 required = false,防止 Session 过期导致报错。
        return "注解方式获取 username: " + username;
    }
 
    /**
     * 6. 读取 Session - Spring 内置对象方式 (对应 PDF Page 11-12,推荐)
     * * 知识点:
     * - 直接在参数里写 HttpSession session。
     * - Spring MVC 非常智能,看到这个参数,就会自动把当前的 Session 对象注进来。
     * - 这相当于 request.getSession(),如果不存在通常会自动创建。
     */
    @RequestMapping("/getSess3")
    public String sess3(HttpSession session) {
        // 直接使用 session 对象获取数据
        String username = (String) session.getAttribute("username");
        return "内置对象方式获取 username: " + username;
    }
1. 传统方式 (HttpServletRequest)

这是最"原教旨主义"的写法,直接操作 Servlet API。

  • 核心逻辑request.getSession(false)

  • 优点

    • 内存控制最精细 :通过传入 false,你可以明确告诉服务器:"如果用户没登录(没 Session),千万别给我创建一个新的。"这对防止恶意爬虫制造大量垃圾 Session(导致内存溢出)非常有用。

    • 全能 :拿到了 request 对象,除了操作 Session,还能顺便获取 IP 地址、User-Agent 等信息。

  • 缺点

    • 啰嗦 :你需要写多层 if 判断(判断 Session 是否为空,判断 Attribute 是否为空)。

    • 不优雅:业务代码中充斥着非业务逻辑的底层 API 调用。

2. 注解方式 (@SessionAttribute)

这是 Spring MVC 特有的"声明式"写法,由 Spring 框架在后台帮你完成查找和注入。

  • 核心逻辑@SessionAttribute(value = "username", required = false)

  • 优点

    • 最简洁:一行代码搞定,直接把 Session 中的值变成了方法参数。

    • 语义清晰 :一看参数就知道这个方法依赖 Session 中的 username

    • 解耦:你的方法不再依赖 Servlet API(方便进行单元测试)。

  • 缺点

    • 功能受限 :它只能用来读取 数据。如果你想 setAttribute(存数据)或者 invalidate(注销/销毁 Session),这个注解做不到,你还是得把 HttpSession 对象拿出来。

    • 异常风险 :如果忘记写 required = false 且 Session 中没有该数据,Spring 会直接抛出 400 Bad Request 异常,导致页面报错。

3. Spring 内置对象方式 (HttpSession)

这是目前最常用的折中方案。Spring 检测到方法参数中有 HttpSession,会自动将当前的会话对象注入进来。

  • 核心逻辑 :直接使用参数 session

  • 优点

    • 方便且强大 :既比 request 方式少写代码,又拥有 HttpSession 的全部控制权(读、写、销毁)。

    • 符合直觉:大多数业务场景下,我们假设用户访问需要 Session 的页面时,Session 应该是存在的(或者框架自动创建)。

  • 缺点

    • 隐式创建 :通常情况下,Spring 注入这个对象等同于调用 getSession(true)。如果你的系统对"很多未登录用户访问此接口"极其敏感(担心创建过多空 Session 占用内存),这种方式不如方法 1 精确。

    • 类型转换 :依然需要手动 (String) 强转。


场景化建议:我该用哪个?

  1. 场景 A:你需要判断用户是否登录,且不想浪费内存(比如首页、公共页)

    • 👉 首选 方法 1 (HttpServletRequest)

    • 理由 :利用 getSession(false) 避免为游客创建无用的 Session 对象。

  2. 场景 B:用户已经登录,你需要在一个详情页仅仅读取一下用户的名字或 ID

    • 👉 首选 方法 2 (@SessionAttribute)

    • 理由:代码最干净,不需要关心 Session 对象本身,只关心数据。

  3. 场景 C:用户登录、注销,或者购物车操作(既要读又要写)

    • 👉 首选 方法 3 (HttpSession)

    • 理由 :你需要完整的 Session 对象来执行 session.setAttribute()session.invalidate(),这种方式比 request 方式更简单。


下一步建议:

既然你已经掌握了 Session 的读取,是否需要我为你展示如何优雅地通过拦截器(Interceptor)来统一处理 Session 登录检查? 这样就不需要在每个 Controller 方法里重复写判断逻辑了。

相关推荐
semantist@语校1 小时前
第五十一篇|构建日本语言学校数据模型:埼玉国际学院的城市结构与行为变量分析
java·大数据·数据库·人工智能·百度·ai·github
LSL666_1 小时前
4 Maven 项目构建与配置要点总结
java·tomcat·maven
好好研究1 小时前
MyBatis框架 - 延迟加载+一/二级缓存
java·数据库·mybatis
曹牧1 小时前
C#中的StartsWith
java·服务器·c#
薛定谔的猫19821 小时前
docker 安装redis
java·spring·mybatis
q***42821 小时前
解决bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException
java·数据库·sql
q***D4431 小时前
【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目
java·spring boot·spring
s***55811 小时前
SpringBoot整合JWT
java·spring boot·后端