Cookie、Session、JWT 的区别?

Cookie 是存储在客户端的数据,Session 是存储在服务端的用户会话数据。Session 通常依赖 Cookie 来传递 SessionID。JWT 是一种无状态的令牌,包含用户信息和签名,服务端无需存储状态。

在分布式场景下,Session 需要解决共享问题(如 Redis 存储),而 JWT 天然支持分布式。但 JWT 无法主动失效,需要通过黑名单等机制实现。

一、为什么需要这些技术?

HTTP 协议是无状态的,服务器无法维持会话状态,无法识别两次请求是否来自同一个用户。为了实现用户身份识别和状态管理,诞生了 Cookie、Session、JWT 等技术。

二、Cookie 详解

2.1 什么是 Cookie?

Cookie 是服务器发送到浏览器并保存在客户端的一小段数据,浏览器在后续请求中会自动携带该 Cookie。

大小限制说明:每个域名的 Cookie 总大小不超过 4KB(包括所有 Cookie)。单个 Cookie 建议 < 4KB 以兼容所有浏览器。Chrome 实际允许单个 Cookie 超过 4KB,但为兼容性考虑应保持在限制内。

属性 说明
Name=Value Cookie 的键值对
Expires/Max-Age 过期时间,不设置则为会话 Cookie
Domain Cookie 生效的域名
Path Cookie 生效的路径
Secure 仅在 HTTPS 下传输
HttpOnly 禁止 JavaScript 访问,防 XSS
SameSite 防 CSRF 攻击(Strict/Lax/None)
java 复制代码
// 设置 Cookie
@GetMapping("/setCookie")
public String setCookie(HttpServletResponse response) {
    Cookie cookie = new Cookie("userId", "12345");
    cookie.setMaxAge(3600);        // 1小时过期
    cookie.setPath("/");
    cookie.setHttpOnly(true);      // 防止XSS
    cookie.setSecure(true);        // 仅HTTPS
    response.addCookie(cookie);
    return "Cookie设置成功";
}

// 读取 Cookie
@GetMapping("/getCookie")
public String getCookie(@CookieValue(value = "userId", defaultValue = "") String userId) {
    return "userId: " + userId;
}

优点:

  • 实现简单,浏览器自动管理
  • 减轻服务器存储压力

缺点:

  • 容量限制(4KB)
  • 安全性较低,容易被篡改
  • 存在 CSRF 攻击风险
  • 不支持跨域(跨域需额外配置)
攻击类型 描述 防御方式
XSS 通过脚本窃取 Cookie 设置 HttpOnly 属性
CSRF 跨站请求伪造 设置 SameSite 属性
Cookie Bombing 填充大量 Cookie 导致请求头过大 服务端校验 Cookie 数量和大小
中间人攻击 传输过程被截获 设置 Secure 属性 + HTTPS

2.7 SameSite 属性详解

浏览器兼容性 :Chrome 80+ 默认将 SameSite 设为 Lax,需注意跨站场景。

说明 适用场景
Strict 完全禁止第三方 Cookie 银行等高安全站点
Lax 允许导航到目标网址的 GET 请求携带(默认值 大多数网站
None 允许跨站发送,必须配合 Secure 嵌入式应用、OAuth

Java 中设置 SameSite(Servlet 4.0+):

java 复制代码
// 方式一:通过 ResponseHeader
response.setHeader("Set-Cookie", "userId=12345; Path=/; HttpOnly; Secure; SameSite=Lax");
yaml 复制代码
# 方式二:Spring Boot 配置 (application.yml)
server.servlet.session.cookie.same-site=lax

三、Session 详解

3.1 什么是 Session?

Session 是保存在服务器端的用户会话数据,通过 SessionID(通常存储在 Cookie 中)来标识用户。

3.2 Session 工作原理

3.3 Java 操作 Session 示例

java 复制代码
// 设置 Session
@GetMapping("/setSession")
public String setSession(HttpServletRequest request) {
    HttpSession session = request.getSession();
    session.setAttribute("user", new User("张三", 25));
    session.setMaxInactiveInterval(1800);  // 30分钟过期
    return "Session设置成功,ID: " + session.getId();
}

// 读取 Session
@GetMapping("/getSession")
public String getSession(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if (session != null) {
        User user = (User) session.getAttribute("user");
        return "用户: " + user.getName();
    }
    return "Session不存在";
}

// 销毁 Session(登出)
@GetMapping("/logout")
public String logout(HttpServletRequest request) {
    HttpSession session = request.getSession(false);
    if (session != null) {
        session.invalidate();
    }
    return "登出成功";
}

3.4 Session 的存储方式

方式 说明 适用场景
内存存储 默认方式,存在JVM内存 单机应用
文件存储 序列化到磁盘(开销大,不推荐高并发) 小型应用
数据库存储 存储到MySQL等 需持久化场景
Memcached存储 分布式内存缓存 分布式系统
Redis存储 分布式缓存,支持持久化 分布式系统首选

注意 :存入 Session 的对象需实现 Serializable 接口(Redis/文件存储场景)。

3.5 分布式 Session 解决方案

分布式 Session 方案性能对比:

方案 性能影响 扩展性 成本
Sticky Session 负载均衡器压力大,单机故障影响大
Session 复制 网络同步开销高
Redis 集中存储 读写延迟低(毫秒级) 中(需Redis集群)
JWT 无存储开销,验证快(签名校验<1ms)

Spring Session + Redis 配置:

java 复制代码
// 1. 添加依赖
// spring-boot-starter-data-redis
// spring-session-data-redis

// 2. 配置
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
@Configuration
public class SessionConfig {
    // Redis连接配置...
}

3.6 Session 安全攻击与防御

攻击类型 描述 防御方式
Session Fixation 攻击者固定 SessionID 诱导用户登录 登录后重置 SessionID(Spring Security 默认支持)
Session Hijacking 窃取用户 SessionID HTTPS + HttpOnly Cookie
Session 过期攻击 利用长期有效的 Session 设置合理过期时间 + 滑动过期

登录后重置 SessionID(防 Session Fixation):

java 复制代码
@PostMapping("/login")
public String login(HttpServletRequest request) {
    // 验证用户...
    
    // 登录成功后重置 SessionID,防止 Session Fixation 攻击
    HttpSession oldSession = request.getSession(false);
    if (oldSession != null) {
        oldSession.invalidate();
    }
    HttpSession newSession = request.getSession(true);
    newSession.setAttribute("user", authenticatedUser);
    return "登录成功";
}

3.7 Session 在分布式环境下有什么问题?怎么解决?

问题: 用户请求可能被分发到不同服务器,而 Session 默认存储在单机内存中,导致 Session 不共享。

解决方案:

  1. Session 粘滞(Sticky Session):Nginx 配置 ip_hash,同一用户固定访问同一台服务器
  2. Session 复制:服务器之间同步 Session(Tomcat 集群支持)
  3. Session 集中存储 :使用 Redis 集中存储 Session(推荐
  4. 使用 JWT:改为无状态认证,彻底解决问题

3.8 Session 的优缺点

优点:

  • 数据存储在服务端,安全性高
  • 可存储任意类型数据,无大小限制

缺点:

  • 占用服务器内存
  • 分布式环境需额外处理(Session共享)
  • 依赖 Cookie 传递 SessionID

四、JWT 详解

4.1 什么是 JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全传输信息的紧凑的、自包含的令牌。

4.2 JWT 结构

JWT 由三部分组成,用 . 分隔:

erlang 复制代码
Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuW8oOS4iSIsImlhdCI6MTUxNjIzOTAyMn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

① Header(头部)

json 复制代码
{
  "alg": "HS256",   // 签名算法
  "typ": "JWT"      // 令牌类型
}

② Payload(载荷)

json 复制代码
{
  "sub": "1234567890",     // 主题(用户ID)
  "name": "张三",           // 自定义数据
  "iat": 1516239022,       // 签发时间
  "exp": 1516242622,       // 过期时间
  "role": "admin"          // 自定义:用户角色
}

③ Signature(签名)

scss 复制代码
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)

4.3 JWT 工作原理

安全警告:Payload 是 Base64 编码而非加密,任何人都可解码查看。绝对不要存储密码、身份证号等敏感信息!

4.4 Java 操作 JWT 示例(使用 jjwt)

Maven 依赖:

xml 复制代码
<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>

JwtUtil 工具类:

java 复制代码
@Component
public class JwtUtil {
    
    private static final String SECRET_KEY = "your-256-bit-secret-key-here-must-be-long";
    private static final long EXPIRATION = 24 * 60 * 60 * 1000; // 24小时
    
    // 生成 JWT
    public String generateToken(String userId, String role) {
        return Jwts.builder()
                .setSubject(userId)
                .claim("role", role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }
    
    // 解析 JWT
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    // 验证 JWT
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    // 获取用户ID
    public String getUserId(String token) {
        return parseToken(token).getSubject();
    }
}

登录接口示例:

java 复制代码
@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        // 验证用户名密码(省略)
        User user = userService.authenticate(request.getUsername(), request.getPassword());
        if (user != null) {
            String token = jwtUtil.generateToken(user.getId(), user.getRole());
            return ResponseEntity.ok(Map.of("token", token));
        }
        return ResponseEntity.status(401).body("认证失败");
    }
}

JWT 过滤器:

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtUtil.validateToken(token)) {
                String userId = jwtUtil.getUserId(token);
                // 设置认证信息到 SecurityContext
                UsernamePasswordAuthenticationToken auth = 
                    new UsernamePasswordAuthenticationToken(userId, null, getAuthorities(token));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(request, response);
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(String token) {
        String role = (String) jwtUtil.parseToken(token).get("role");
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role));
    }
}

4.5 JWT 的优缺点

优点:

  • 无状态:服务端不需存储,天然支持分布式
  • 跨语言:标准化格式,任何语言都可解析
  • 自包含:载荷包含用户信息,减少数据库查询
  • 支持跨域:可放在请求头中传递

缺点:

  • 无法主动失效:一旦签发,在过期前无法作废(可用黑名单解决)
  • 载荷暴露:Base64 编码而非加密,不要存敏感数据
  • Token 较长:比 SessionID 占用更多带宽

4.6 JWT 安全攻击与防御

攻击类型 描述 防御方式
算法降级攻击 将 alg 设为 none 绕过验证 解析时强制指定算法
密钥爆破 暴力破解弱密钥 使用 256 位以上强密钥
Token 泄露 XSS/中间人攻击获取 Token HTTPS + HttpOnly Cookie 存储
Token 重放 截获 Token 重复使用 短过期时间 + Refresh Token

4.7 如何防止 JWT 被盗用?

  1. 使用 HTTPS:防止传输过程被窃取
  2. 设置合理过期时间:建议 Access Token 15分钟~2小时
  3. Refresh Token 机制:短期 Access Token + 长期 Refresh Token
  4. 绑定客户端特征:IP、User-Agent 等
  5. 敏感操作二次验证:修改密码、支付等需再次输入密码

4.8 双 Token 机制(Access Token + Refresh Token)

流程说明:

  1. 登录成功 → 返回 Access Token(15分钟)+ Refresh Token(7天)
  2. 请求接口 → 携带 Access Token
  3. Access Token 过期 → 用 Refresh Token 换取新的 Access Token
  4. Refresh Token 过期 → 重新登录

优势:Access Token 短期有效降低泄露风险,Refresh Token 避免频繁登录。

注意:双 Token 机制通常用于 JWT 或 OAuth2,也可与 Session 结合使用。

4.9 JWT 主动失效解决方案

方案一:Token 黑名单

java 复制代码
// 登出时将 Token 加入 Redis 黑名单
public void logout(String token) {
    Claims claims = jwtUtil.parseToken(token);
    long ttl = claims.getExpiration().getTime() - System.currentTimeMillis();
    redisTemplate.opsForValue().set("blacklist:" + token, "1", ttl, TimeUnit.MILLISECONDS);
}

// 验证时检查黑名单
public boolean validateToken(String token) {
    if (redisTemplate.hasKey("blacklist:" + token)) {
        return false;
    }
    // 正常验证...
}

方案二:Token 版本号

java 复制代码
// 用户表增加 token_version 字段
// JWT 中存储版本号,验证时比对数据库版本
// 修改密码时更新版本号,使旧 Token 失效 

性能提示:高频验证场景下,黑名单查询会有 Redis 开销。可考虑使用布隆过滤器优化。

五、三者的区别

5.1 Cookie、Session、JWT 的区别

特性 Cookie Session JWT
存储位置 客户端 服务端 客户端
安全性 较低 中(需HTTPS)
跨域支持 不支持 不支持 支持
分布式支持 N/A 需要额外处理 天然支持
服务器压力 占用内存
主动失效 可以 可以 不支持(需黑名单)
数据大小 ≤4KB 无限制 建议≤8KB
适用场景 简单状态保持 传统Web应用 前后端分离/微服务
维度 Cookie Session
存储位置 客户端浏览器 服务器端
安全性 较低,可被查看篡改 较高,数据在服务端
生命周期 可设置长期有效 默认会话结束失效
存储限制 4KB左右 理论无限制
关系 可独立使用 通常依赖Cookie存SessionID

5.3 JWT 和 Session 如何选择?

场景 推荐方案
传统 MVC 单体应用 Session
前后端分离 JWT
微服务架构 JWT
需要频繁主动踢出用户 Session
移动端 APP JWT
对安全性要求极高 Session + 二次验证

5.4 认证流程对比

Session 模式:

scss 复制代码
登录 → 服务器创建Session → 返回SessionID(Cookie) 
     → 请求携带Cookie → 服务器查询Session → 验证成功

JWT 模式:

markdown 复制代码
登录 → 服务器生成JWT → 返回Token
     → 请求携带Token → 服务器验证签名+解析 → 验证成功

5.5 有无状态的本质区别


六、总结

技术 一句话总结
Cookie 客户端存储的小型文本,浏览器自动携带
Session 服务端存储用户状态,依赖 Cookie 传递 SessionID
JWT 自包含的无状态令牌,天然支持分布式

使用建议:

  • Cookie 务必设置 HttpOnlySecureSameSite
  • JWT 密钥要足够长(256位以上)且定期更换
  • 敏感数据不要存储在 JWT 的 Payload 中
  • 生产环境必须使用 HTTPS
  • 合理设置 Token 过期时间

参考

相关推荐
青莲84321 小时前
Java内存模型(JMM)与JVM内存区域完整详解
android·前端·面试
用户03048059126321 小时前
Spring Boot 配置文件加载大揭秘:优先级覆盖与互补合并机制详解
java·后端
青莲84321 小时前
Java内存回收机制(GC)完整详解
java·前端·面试
gAlAxy...1 天前
5 种 SpringBoot 项目创建方式
java·spring boot·后端
CCPC不拿奖不改名1 天前
python基础:python语言中的函数与模块+面试习题
开发语言·python·面试·职场和发展·蓝桥杯
回家路上绕了弯1 天前
定时任务实战指南:从单机到分布式,覆盖Spring Scheduler/Quartz/XXL-Jo
分布式·后端
神奇小汤圆1 天前
MySQL索引明明建了,查询还是慢,排查发现踩了这些坑
后端
帅气的你1 天前
高并发下的防并发实战:C端/B端项目并发控制完全指南
后端
Ahtacca1 天前
解决服务间通信难题:Spring Boot 中 HttpClient 的标准使用姿势
java·spring boot·后端