【 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管理一些临时状态。

相关推荐
拿破轮33 分钟前
使用通义灵码解决复杂正则表达式替换字符串的问题.
java·服务器·前端
Aerelin1 小时前
爬虫playwright入门讲解
前端·javascript·html·playwright
5***o5001 小时前
前端在移动端中的NativeBase
前端
灵魂学者1 小时前
Vue3.x —— 父子通信
前端·javascript·vue.js·github
1***Q7841 小时前
前端跨域解决方案
前端
小雨青年2 小时前
MateChat 进阶实战:打造零后端、隐私安全的“端侧记忆”智能体
前端·华为·ai·华为云·状态模式
勇气要爆发2 小时前
问:ES5和ES6的区别
前端·ecmascript·es6
永不停歇的蜗牛3 小时前
Maven的POM文件相关标签作用
服务器·前端·maven
芳草萋萋鹦鹉洲哦3 小时前
【vue/js】文字超长悬停显示的几种方式
前端·javascript·vue.js