【 Web认证 】Cookie、Session 与 JWT Token:Web 认证机制的原理、实现与对比

本项目演示了三种常见的Web认证方式:Cookie认证、Session认证和JWT Token认证。通过Spring Boot后端和Vue3前端的实现,展示了不同认证机制的工作原理和使用场景。

文章目录

    • 目录结构
    • [1. Cookie认证](#1. Cookie认证)
      • [1.1 工作原理](#1.1 工作原理)
      • [1.2 代码实现](#1.2 代码实现)
      • [1.3 优缺点](#1.3 优缺点)
    • [2. Session认证](#2. Session认证)
      • [2.1 工作原理](#2.1 工作原理)
      • [2.2 代码实现](#2.2 代码实现)
      • [2.3 优缺点](#2.3 优缺点)
    • [3. JWT Token认证](#3. JWT Token认证)
      • [3.1 工作原理](#3.1 工作原理)
      • [3.2 JWT Token结构](#3.2 JWT Token结构)
      • [3.3 代码实现](#3.3 代码实现)
      • [3.4 优缺点](#3.4 优缺点)
    • [4. 三种认证方式的比较](#4. 三种认证方式的比较)
    • [5. 安全最佳实践](#5. 安全最佳实践)
      • [5.1 Cookie安全](#5.1 Cookie安全)
      • [5.2 Session安全](#5.2 Session安全)
      • [5.3 JWT安全](#5.3 JWT安全)
    • [6. 运行项目](#6. 运行项目)
      • [6.1 后端运行](#6.1 后端运行)
      • [6.2 前端运行](#6.2 前端运行)
      • [6.3 测试账号](#6.3 测试账号)
    • [7. 总结](#7. 总结)

目录结构

plain 复制代码
auth-demo/
├── backend/         # Spring Boot后端项目
│   ├── src/main/java/com/example/authdemo/
│   │   ├── controller/   # 控制器
│   │   ├── model/        # 模型
│   │   ├── config/       # 配置
│   │   └── util/         # 工具类
│   └── pom.xml          # Maven配置
├── frontend/        # Vue3前端项目
│   ├── src/
│   │   ├── components/   # 组件
│   │   ├── services/     # API服务
│   │   └── App.vue       # 主应用
│   └── package.json      # NPM配置
└── README.md        # 本文档

1. Cookie认证

1.1 工作原理

Cookie认证是一种基于客户端存储的认证机制,主要通过以下步骤工作:

  1. 登录过程
    • 用户提交用户名和密码
    • 服务器验证凭据
    • 验证成功后,生成会话ID
    • 服务器将会话ID通过Set-Cookie响应头设置到客户端Cookie中
    • 会话ID与用户信息的映射存储在服务器端
  2. 请求验证
    • 客户端发起后续请求时,自动在请求头中携带Cookie
    • 服务器从Cookie中提取会话ID
    • 服务器验证会话ID的有效性
    • 如果会话ID有效,服务器识别用户身份并处理请求
  3. 登出过程
    • 客户端发起登出请求
    • 服务器从存储中删除会话ID
    • 服务器设置Cookie的过期时间为0,使Cookie失效

1.2 代码实现

后端实现

CookieAuthController.java

java 复制代码
package com.example.authdemo.controller;

import com.example.authdemo.model.User;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api/cookie-auth")
@CrossOrigin(origins = "http://localhost:5174", allowCredentials = "true")
public class CookieAuthController {

    // 模拟用户数据库
    private static final Map<String, User> users = new HashMap<>();
    // 模拟会话存储
    private static final Map<String, String> sessions = new HashMap<>();

    static {
        // 预创建测试用户
        users.put("admin", new User("admin", "password", "ADMIN"));
        users.put("user", new User("user", "password", "USER"));
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody User user, HttpServletResponse response) {
        // 验证用户
        User storedUser = users.get(user.getUsername());
        if (storedUser != null && storedUser.getPassword().equals(user.getPassword())) {
            // 生成会话ID
            String sessionId = UUID.randomUUID().toString();
            // 将会话ID与用户名关联
            sessions.put(sessionId, user.getUsername());
            
            // 创建Cookie
            Cookie cookie = new Cookie("sessionId", sessionId);
            cookie.setPath("/");
            cookie.setHttpOnly(true);
            cookie.setMaxAge(3600); // 1小时有效期
            
            // 添加Cookie到响应
            response.addCookie(cookie);
            
            Map<String, String> result = new HashMap<>();
            result.put("message", "Login successful");
            result.put("username", user.getUsername());
            return result;
        }
        
        Map<String, String> error = new HashMap<>();
        error.put("message", "Invalid username or password");
        return error;
    }

    @PostMapping("/logout")
    public Map<String, String> logout(HttpServletRequest request, HttpServletResponse response) {
        // 获取Cookie中的会话ID
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("sessionId".equals(cookie.getName())) {
                    // 从会话存储中移除
                    sessions.remove(cookie.getValue());
                    
                    // 使Cookie失效
                    cookie.setPath("/");
                    cookie.setMaxAge(0);
                    response.addCookie(cookie);
                    break;
                }
            }
        }
        
        Map<String, String> result = new HashMap<>();
        result.put("message", "Logout successful");
        return result;
    }

    @GetMapping("/profile")
    public Map<String, Object> getProfile(HttpServletRequest request) {
        // 验证Cookie中的会话ID
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("sessionId".equals(cookie.getName())) {
                    String username = sessions.get(cookie.getValue());
                    if (username != null) {
                        User user = users.get(username);
                        Map<String, Object> profile = new HashMap<>();
                        profile.put("username", user.getUsername());
                        profile.put("role", user.getRole());
                        profile.put("authenticated", true);
                        return profile;
                    }
                }
            }
        }
        
        Map<String, Object> error = new HashMap<>();
        error.put("message", "Not authenticated");
        error.put("authenticated", false);
        return error;
    }
}
前端实现

api.js

javascript 复制代码
export const cookieAuthApi = {
  // 登录
  login: async (username, password) => {
    return await api.post('/cookie-auth/login', { username, password }, {
      withCredentials: true // 允许携带cookie
    });
  },
  
  // 登出
  logout: async () => {
    return await api.post('/cookie-auth/logout', {}, {
      withCredentials: true
    });
  },
  
  // 获取用户信息
  getProfile: async () => {
    return await api.get('/cookie-auth/profile', {
      withCredentials: true
    });
  }
};

1.3 优缺点

优点

  • 实现简单,无需客户端特殊处理
  • 自动管理,浏览器自动携带Cookie
  • 可以设置HttpOnly标志,提高安全性

缺点

  • 受同源策略限制
  • 服务器需要存储会话状态,不利于水平扩展
  • 可能存在CSRF攻击风险

2. Session认证

2.1 工作原理

Session认证是基于服务器端存储的认证机制,Spring Boot默认提供了Session支持:

  1. 登录过程
    • 用户提交用户名和密码
    • 服务器验证凭据
    • 验证成功后,创建HttpSession对象
    • 将用户信息存储在Session对象中
    • 服务器自动设置JSESSIONID Cookie到客户端
  2. 请求验证
    • 客户端发起请求时携带JSESSIONID Cookie
    • 服务器根据JSESSIONID查找对应的HttpSession对象
    • 如果Session存在且有效,从Session中获取用户信息
  3. 登出过程
    • 客户端发起登出请求
    • 服务器调用session.invalidate()使Session失效
    • JSESSIONID Cookie也会相应失效

2.2 代码实现

后端实现

SessionAuthController.java

java 复制代码
package com.example.authdemo.controller;

import com.example.authdemo.model.User;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/session-auth")
@CrossOrigin(origins = "http://localhost:5174", allowCredentials = "true")
public class SessionAuthController {

    // 模拟用户数据库
    private static final Map<String, User> users = new HashMap<>();

    static {
        // 预创建测试用户
        users.put("admin", new User("admin", "password", "ADMIN"));
        users.put("user", new User("user", "password", "USER"));
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody User user, HttpSession session) {
        // 验证用户
        User storedUser = users.get(user.getUsername());
        if (storedUser != null && storedUser.getPassword().equals(user.getPassword())) {
            // 将用户信息存储到Session中
            session.setAttribute("user", storedUser);
            session.setMaxInactiveInterval(3600); // 1小时有效期
            
            Map<String, String> result = new HashMap<>();
            result.put("message", "Login successful");
            result.put("username", user.getUsername());
            result.put("sessionId", session.getId());
            return result;
        }
        
        Map<String, String> error = new HashMap<>();
        error.put("message", "Invalid username or password");
        return error;
    }

    @PostMapping("/logout")
    public Map<String, String> logout(HttpSession session) {
        // 使Session失效
        session.invalidate();
        
        Map<String, String> result = new HashMap<>();
        result.put("message", "Logout successful");
        return result;
    }

    @GetMapping("/profile")
    public Map<String, Object> getProfile(HttpSession session) {
        // 从Session中获取用户信息
        User user = (User) session.getAttribute("user");
        if (user != null) {
            Map<String, Object> profile = new HashMap<>();
            profile.put("username", user.getUsername());
            profile.put("role", user.getRole());
            profile.put("authenticated", true);
            profile.put("sessionId", session.getId());
            return profile;
        }
        
        Map<String, Object> error = new HashMap<>();
        error.put("message", "Not authenticated");
        error.put("authenticated", false);
        return error;
    }
}
前端实现

api.js

javascript 复制代码
export const sessionAuthApi = {
  // 登录
  login: async (username, password) => {
    return await api.post('/session-auth/login', { username, password }, {
      withCredentials: true
    });
  },
  
  // 登出
  logout: async () => {
    return await api.post('/session-auth/logout', {}, {
      withCredentials: true
    });
  },
  
  // 获取用户信息
  getProfile: async () => {
    return await api.get('/session-auth/profile', {
      withCredentials: true
    });
  }
};

2.3 优缺点

优点

  • Spring Boot自动管理,使用简单
  • 安全性较高,Session数据存储在服务器端
  • 可以存储复杂的用户信息

缺点

  • 服务器需要维护Session状态,水平扩展时需要考虑Session共享
  • 受同源策略限制
  • 可能存在CSRF攻击风险

3. JWT Token认证

3.1 工作原理

JWT (JSON Web Token) 是一种基于Token的无状态认证机制:

  1. 登录过程
    • 用户提交用户名和密码
    • 服务器验证凭据
    • 验证成功后,生成JWT Token(包含用户信息、过期时间等)
    • 服务器将Token返回给客户端
  2. 请求验证
    • 客户端将Token存储在localStorage或sessionStorage中
    • 发起请求时,在Authorization头中携带Token:Bearer {token}
    • 服务器接收到请求后,验证Token的签名和有效性
    • 验证通过后,从Token中提取用户信息
  3. 登出过程
    • 客户端删除存储的Token
    • 服务器无需特殊处理(无状态)

3.2 JWT Token结构

JWT由三部分组成,用点(.)连接:

plain 复制代码
header.payload.signature
  • Header:包含Token类型和签名算法
  • Payload:包含声明(claims),如用户ID、角色、过期时间等
  • Signature:使用密钥对前两部分进行签名,用于验证Token的完整性

3.3 代码实现

后端实现

JwtUtil.java

java 复制代码
public class JwtUtil {

    // 生成安全的密钥
    private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long EXPIRATION_TIME = 3600000; // 1小时,单位毫秒

    // 生成Token
    public static String generateToken(String username, String role) {
        return Jwts.builder()
                .setSubject(username)
                .claim("role", role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SECRET_KEY)
                .compact();
    }

    // 验证Token是否有效
    public static boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 其他方法...
}

JwtAuthController.java

java 复制代码
package com.example.authdemo.controller;

import com.example.authdemo.model.User;
import com.example.authdemo.util.JwtUtil;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/jwt-auth")
@CrossOrigin(origins = "http://localhost:5174")
public class JwtAuthController {

    // 模拟用户数据库
    private static final Map<String, User> users = new HashMap<>();

    static {
        // 预创建测试用户
        users.put("admin", new User("admin", "password", "ADMIN"));
        users.put("user", new User("user", "password", "USER"));
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody User user) {
        // 验证用户
        User storedUser = users.get(user.getUsername());
        if (storedUser != null && storedUser.getPassword().equals(user.getPassword())) {
            // 生成JWT Token
            String token = JwtUtil.generateToken(user.getUsername(), user.getRole());
            
            Map<String, String> result = new HashMap<>();
            result.put("message", "Login successful");
            result.put("username", user.getUsername());
            result.put("token", token);
            return result;
        }
        
        Map<String, String> error = new HashMap<>();
        error.put("message", "Invalid username or password");
        return error;
    }

    @GetMapping("/profile")
    public Map<String, Object> getProfile(@RequestHeader("Authorization") String authorizationHeader) {
        // 检查Authorization头
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            Map<String, Object> error = new HashMap<>();
            error.put("message", "Authorization header is required");
            error.put("authenticated", false);
            return error;
        }
        
        // 提取token
        String token = authorizationHeader.substring(7);
        
        // 验证token
        if (JwtUtil.validateToken(token)) {
            String username = JwtUtil.getUsernameFromToken(token);
            String role = JwtUtil.getRoleFromToken(token);
            
            Map<String, Object> profile = new HashMap<>();
            profile.put("username", username);
            profile.put("role", role);
            profile.put("authenticated", true);
            return profile;
        }
        
        Map<String, Object> error = new HashMap<>();
        error.put("message", "Invalid token");
        error.put("authenticated", false);
        return error;
    }

    @GetMapping("/validate")
    public Map<String, Boolean> validateToken(@RequestHeader("Authorization") String authorizationHeader) {
        Map<String, Boolean> result = new HashMap<>();
        
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            result.put("valid", false);
            return result;
        }
        
        String token = authorizationHeader.substring(7);
        result.put("valid", JwtUtil.validateToken(token));
        return result;
    }
}
前端实现

api.js

javascript 复制代码
export const jwtAuthApi = {
  // 登录
  login: async (username, password) => {
    return await api.post('/jwt-auth/login', { username, password });
  },
  
  // 获取用户信息
  getProfile: async (token) => {
    return await api.get('/jwt-auth/profile', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
  },
  
  // 验证token
  validateToken: async (token) => {
    return await api.get('/jwt-auth/validate', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
  }
};

authStorage.js

javascript 复制代码
// 认证存储管理
export const authStorage = {
  // 保存JWT token
  saveToken: (token) => {
    localStorage.setItem('jwt_token', token);
  },
  
  // 获取JWT token
  getToken: () => {
    return localStorage.getItem('jwt_token');
  },
  
  // 移除JWT token
  removeToken: () => {
    localStorage.removeItem('jwt_token');
  },
  
  // 其他方法...
};

3.4 优缺点

优点

  • 无状态,服务器不需要存储会话信息,便于水平扩展
  • 支持跨域,不依赖Cookie
  • 自包含,Token中包含所有必要信息

缺点

  • Token一旦签发,无法直接撤销(除非服务端维护黑名单)
  • Token可能较大,增加请求大小
  • 需要客户端手动管理Token的存储和发送

4. 三种认证方式的比较

特性 Cookie认证 Session认证 JWT Token认证
存储位置 客户端Cookie 服务器Session + 客户端Cookie 客户端localStorage/SessionStorage
状态管理 有状态 有状态 无状态
跨域支持 不支持 不支持 支持
水平扩展 困难 需要Session共享 简单
安全性 中等
实现复杂度 简单 简单(Spring Boot自动支持) 中等
适合场景 简单的Web应用 传统Web应用 前后端分离、微服务、移动应用

5. 安全最佳实践

5.1 Cookie安全

  • 设置HttpOnly标志,防止XSS攻击获取Cookie
  • 设置Secure标志,确保只通过HTTPS传输
  • 设置SameSite属性,防止CSRF攻击
  • 使用较短的过期时间

5.2 Session安全

  • 使用强随机数生成Session ID
  • 限制Session的生命周期
  • 登出时立即销毁Session
  • 考虑使用Redis等进行Session共享

5.3 JWT安全

  • 使用强密钥并定期轮换
  • 设置合理的过期时间
  • 敏感信息不放入Payload(因为Payload可以被解码)
  • 使用HTTPS传输Token
  • 实现Token刷新机制

6. 运行项目

6.1 后端运行

bash 复制代码
cd auth-demo/backend
mvn spring-boot:run

6.2 前端运行

bash 复制代码
cd auth-demo/frontend
npm install
npm run dev

6.3 测试账号

  • 用户名: admin, 密码: password (管理员权限)
  • 用户名: user, 密码: password (普通用户权限)

7. 总结

三种认证方式各有优缺点,选择哪种方式应该根据具体的应用场景:

  • Cookie认证:适合简单的Web应用,实现简单但扩展性有限
  • Session认证:适合传统的Web应用,Spring Boot原生支持,安全性较高
  • JWT Token认证:适合前后端分离、微服务架构和移动应用,无状态设计便于扩展

在实际项目中,还可以根据需要结合使用这些认证方式,例如使用JWT进行API认证,同时使用Session管理一些临时状态。

相关推荐
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte6 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc