【硬核干货】Session、Cookie、Token 区别与实战:一篇讲透

下面我用清晰的方式,把 SessionCookieToken 三者的关系、区别、适用场景一次性讲透。你看完就能直接用在面试和实际项目里。

一、三者的本质与核心原理

1. Cookie

  • 本质:浏览器本地存储的一小段文本。
  • 核心特性:由服务器通过 Set-Cookie 响应头下发;浏览器在同域请求中会自动携带;支持 DomainPathExpires/Max-AgeHttpOnly(防 JS 读取)、Secure(仅 HTTPS 传输)、SameSite(防跨站携带)等属性限制。
  • 作用:只是一个存储和传递的载体,本身不负责登录状态逻辑。默认仅同域名自动携带,遵循严格的同源策略。

2. Session

  • 本质:服务器端保存的用户会话数据。
  • 核心结构:服务器生成唯一 sessionId,在内存/Redis/数据库中维护映射 sessionId → { userId, 权限, 登录时间… },并将 sessionId 放入 Cookie(默认键名 JSESSIONID)发给浏览器。
  • 工作方式:浏览器带 Cookie → 服务器拿到 sessionId → 查服务器存储 → 认出你是谁。会话过期或注销时,服务端销毁 Session,客户端清空 Cookie。

3. Token(以最常见的 JWT 为例)

  • 本质:一串经过签名、自包含身份信息的字符串。
  • 核心结构Header.Payload.Signature(头.载荷.签名)。载荷(Payload)里直接包含 userId、过期时间、角色等声明。
  • 工作方式无状态。服务器不需要存任何会话数据。标准传递位置为请求头 Authorization: Bearer <token>。服务器只做两件事:验证签名是否合法 + 检查是否过期。

二、核心区别对比表

对比维度 Session-Cookie 机制 Token(JWT)机制
状态保存位置 服务器端(内存/Redis/DB) 无状态,信息存在 Token 本身
存储依赖 强依赖服务端存储,需考虑集群共享 不依赖服务端存储,天然水平扩展
跨域/分布式支持 麻烦,需配置共享 Cookie 或粘滞会话 天然支持跨域、多服务、微服务网关
客户端传递方式 浏览器自动携带 Cookie 前端手动放入 Header 或 URL 参数
CSRF 风险 较高(浏览器自动带,需额外防护) 极低(不会自动带,需前端显式附加)
移动端/小程序支持 不友好(需手动管理 Cookie 容器) 非常友好(直接存字符串,随请求附加)
过期与踢人控制 服务器可随时强制下线,立即生效 签发后无法中途作废(需黑名单或双 Token 机制)
性能开销 每次请求需查存储(IO 开销) 仅做签名验签(CPU 计算,无 IO)

三、完整登录流程对比

1. Session-Cookie 登录流程

  1. 客户端第一次请求服务端,服务端验证账号密码后生成唯一 sessionId,存入服务端内存/Redis。
  2. 响应头自动写入 Set-Cookie,将 sessionId 下发到浏览器本地存储。
  3. 后续同域请求,浏览器自动携带对应 Cookie。
  4. 服务端拦截请求,提取 Cookie 中的 sessionId,匹配服务端会话数据,识别用户身份。
  5. 用户登出或会话超时,服务端销毁 Session,客户端清除 Cookie。

2. Token 登录流程

  1. 用户输入账号密码发起登录请求。
  2. 服务器验证通过,将用户信息、签发时间、过期时间打包,用密钥签名生成 JWT。
  3. 服务器将 Token 返回给前端(通常在响应体 JSON 中)。
  4. 前端将 Token 存入 localStorage、Vuex/Pinia 或 App 安全存储区。
  5. 后续请求前端通过拦截器手动附加到请求头:Authorization: Bearer <token>
  6. 网关或业务服务验签 + 查有效期,合法则放行。

四、CSRF 攻击原理、风险与防护方案

1. 为什么 Session-Cookie 机制天然高危

跨站请求伪造(CSRF)利用的是浏览器自动携带同源 Cookie的特性。攻击者无需窃取 Cookie,只需诱导已登录用户在恶意页面触发请求,浏览器就会自动带上合法 SessionID。服务端仅校验 Session 合法性,不校验请求是否由用户主动发起,导致攻击成功。

2. 攻击完整流程

  1. 用户正常登录业务网站 A,浏览器保存 A 的 Cookie(含 SessionID)。
  2. 用户未登出 A,打开恶意网站 B。
  3. B 页面构造隐藏请求(表单/图片/iframe 接口请求),目标地址指向 A 的敏感接口。
  4. 浏览器发起请求时,自动携带网站 A 的 Cookie。
  5. 服务端拿到合法 SessionID,误认为是用户本人操作,执行转账、改密等敏感操作。

3. 主流防护方案

  • Token 校验(最常用):后端下发随机 CSRF-Token,前端每次敏感请求手动携带(Header 或表单参数),后端二次校验。恶意跨站请求无法获取页面内的 Token,直接拦截。
  • SameSite Cookie 属性:配置 SameSite=Strict(仅同站携带)或 SameSite=Lax(默认,限制跨域 GET)。从根源限制跨域自动传 Cookie。
  • 请求来源校验:校验 Origin / Referer 请求头,拒绝非法跨域来源(辅助防护)。
  • 敏感操作二次验证:关键接口增加验证码、短信验证或密码二次确认。

五、跨域与分布式集群下的痛点与解决方案

1. 跨域场景痛点

Cookie 遵循同源策略(协议+域名+端口一致)。前端 a.com 调用后端 api.b.com 时,浏览器默认不携带 b.com 的 Cookie,Session 直接失效。即使配置 CORS,不开启凭证模式也无法传递 Cookie。

跨域 Cookie 必要配置:

  1. 后端 CORS 允许:Access-Control-Allow-Credentials: true
  2. 前端请求开启凭证:浏览器 xhr.withCredentials = true 或 Axios withCredentials: true
  3. Cookie 必须配置:SameSite=None; Secure
  4. 禁止配置 Access-Control-Allow-Origin: *,必须明确指定具体域名。

2. 分布式/集群部署 Session 共享问题

传统 Session 存在单机内存,负载均衡转发请求到不同服务器会导致会话丢失(用户请求 A 生成 Session,第二次请求被分到 B,B 无该 Session,强制掉线)。

分布式 Session 解决方案:

  • 中心化存储(主流):将 Session 统一存入 Redis/数据库,所有业务节点共享同一份会话数据。无论请求转发到哪台机器,均能通过 sessionId 查询会话。Spring Boot 可通过 @EnableRedisHttpSession 一键集成。
  • IP 哈希负载均衡:根据客户端 IP 固定转发到同一台服务器。缺点:单点故障风险高、扩容不灵活,仅适用于小众场景。
  • Cookie 序列化存储(Client Session):将用户会话加密后直接存在 Cookie,服务端无状态。本质已接近 JWT 思想。

六、移动端与小程序的兼容性困境

1. 原生 App(Android/iOS)

原生 App 无浏览器同源 Cookie 自动管理机制,不会自动存储、自动携带 Cookie。传统 Session-Cookie 方案在 App 中需手动管理 Cookie 容器,开发成本高且兼容性差。

2. 微信/支付宝/抖音小程序

小程序运行环境为独立渲染引擎,非标准浏览器。默认不自动持久化、不自动携带第三方 Cookie。request 请求不会像浏览器一样自动保存 Set-Cookie,跨域名业务下原生 Session-Cookie 方案直接失效。

3. 兼容改造结论

小程序/移动端业务应淘汰 Cookie-Session,优先使用 Token 方案。Token 放入请求头 Authorization,前端手动携带,全端统一,彻底摆脱环境兼容问题。若强制使用 Cookie,需开启 withCredentials: true 并配置 SameSite=None; Secure,但限制繁琐,生产环境极不推荐。


七、优缺点对比与架构选型指南

Session-Cookie

优点:服务器可随时强制踢人、修改权限;逻辑成熟稳定;对前端透明。
缺点:分布式需做共享;跨域配置繁琐;CSRF 风险高;移动端支持差。
适用场景:传统单体网站、企业后台管理系统(B/S 架构);对在线状态强管控、权限频繁变更的系统。

Token(JWT)

优点:无状态,任意加机器;天然跨域,契合微服务与 API 开放平台;移动端/小程序/SPA 极其友好;无 CSRF 风险。
缺点:签发后无法直接作废(需黑名单或 Access+Refresh 双机制);载荷不宜过大;前端需处理存储与刷新逻辑。
适用场景:前后端分离项目、微服务架构、API 网关鉴权;App、小程序、H5 跨端项目;第三方授权登录。

架构选型总结:

  • 传统单体 Web 项目(PC 浏览器):可用 Cookie-Session,配合 SameSite + CSRF-Token。
  • 小程序 / App / 前后端分离 / 跨域项目:统一使用 JWT / 自定义 Token。
  • 分布式集群 Web 项目:保留 Session 模式 → 改造为 Redis 共享 Session。
  • 高安全敏感系统:禁止单纯依赖 Cookie 会话,必须双校验:Token + 接口签名 + 来源校验。

八、Spring Boot 核心实战代码(开箱即用)

注意:以下代码基于 Spring Boot 2.x。若你使用 Spring Boot 3.x,请将代码中的 import javax.* 替换为 import jakarta.*,其余逻辑完全一致。

共用依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

1. Session-Cookie 版(最简可测)

import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("/session")
public class SessionController {

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password, HttpSession session) {
        if ("admin".equals(username) && "123456".equals(password)) {
            session.setAttribute("loginUser", username);
            // 生产环境建议配置:session.setMaxInactiveInterval(1800); // 30分钟过期
            return "登录成功,SessionId:" + session.getId();
        }
        return "账号或密码错误";
    }

    @GetMapping("/userInfo")
    public String userInfo(HttpSession session) {
        Object user = session.getAttribute("loginUser");
        return user != null ? "当前登录用户:" + user : "未登录,请先访问 /session/login";
    }

    @PostMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate(); // 立即销毁会话
        return "已安全登出";
    }
}

测试方式:浏览器或 Postman 访问 /login 后,直接访问 /userInfo,浏览器会自动携带 Cookie,无需额外操作。

2. JWT Token 版(标准 Bearer 规范)

<!-- JWT 依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {
    // 生产环境务必放配置文件或密钥管理服务,至少 32 字节
    private static final String SECRET = "mySecretKey123456789012345678901234"; 
    private static final long EXPIRE_MS = 1000 * 60 * 30; // 30分钟

    private Key getKey() {
        return Keys.hmacShaKeyFor(SECRET.getBytes());
    }

    public String createToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_MS))
                .signWith(getKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public String parseSubject(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        if ("admin".equals(username) && "123456".equals(password)) {
            return jwtUtil.createToken(username);
        }
        return "账号或密码错误";
    }

    @GetMapping("/userInfo")
    public String userInfo(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return "未携带合法 Token,格式应为: Authorization: Bearer <token>";
        }
        
        String token = authHeader.substring(7);
        try {
            String username = jwtUtil.parseSubject(token);
            return "当前登录用户:" + username;
        } catch (ExpiredJwtException e) {
            return "Token 已过期,请重新登录";
        } catch (JwtException e) {
            return "Token 签名无效或已被篡改";
        }
    }
}

测试方式:

  1. POST /token/login 获取 Token 字符串。
  2. GET /token/userInfo,Header 添加:Authorization: Bearer 你的Token字符串

九、面试高频考点与生产避坑指南

面试官常问 标准回答与生产实践
JWT 被泄露了怎么办? 缩短 Access Token 有效期(如 15 分钟);引入 Refresh Token 机制(存 Redis,可主动撤销);敏感操作要求二次验证(密码/短信)。
Cookie 和 Token 能一起用吗? 可以。现代架构常用 HttpOnly + Secure + SameSite=Strict 的 Cookie 存 Refresh Token,前端 JS 只拿 Access Token 放内存,兼顾安全与体验。
微服务怎么统一鉴权? 网关层(Spring Cloud Gateway / Nginx)统一验签 JWT,解析出用户信息后放入请求头(如 X-User-Id),透传给下游业务服务,业务服务零鉴权逻辑。
Token 太大影响性能? Payload 只放核心标识(userId, roles),权限详情去 Redis/DB 按需查询。JWT 不是数据库,严禁塞入大量业务数据。
Session 共享怎么做? 使用 Spring Session + Redis。配置 @EnableRedisHttpSession,底层自动将 HttpSession 代理到 Redis,业务代码零改动,无缝支持集群。

十、总结

Cookie 是运输工具,负责凭证传递;Session 是服务器存的账本,负责有状态管理;Token 是自带签名、防伪的通行证,负责无状态鉴权。

  • 传统后台与强管控系统:优先选择 Session + Redis 共享
  • 前后端分离、微服务、多端统一项目:优先选择 JWT + Refresh Token 机制。
  • 无论采用哪种方案,生产环境必须遵循三条底线:传输全程走 HTTPS;敏感接口配置防重放与限流策略;核心认证逻辑必须结合业务场景做二次校验与安全加固。
Logo

一站式 AI 云服务平台

更多推荐