Cookie/Session vs JWT 双 Token:登录认证方案的演进与对比

4 阅读8分钟

Cookie/Session vs JWT 双 Token:登录认证方案的演进与对比

登录认证是 Web 应用的核心能力,它决定了用户能否安全访问受保护的资源。随着开发模式从传统单体应用向前后端分离、多端适配(浏览器 / 移动端)演进,登录认证方案也从经典的 Cookie/Session 体系,发展出更适配现代场景的 JWT 双 Token 方案。本文将先拆解 Cookie/Session 登录的核心原理并结合代码解析,再介绍 JWT 双 Token 的实现逻辑,最后对比两种方案的优劣与适用场景。

一、Cookie/Session 登录:传统认证的核心逻辑

1. 核心原理

Cookie/Session 是基于 “服务端状态存储” 的认证模式,核心是用 Session 存储用户身份,用 Cookie 传递 Session 标识,完整流程如下:

  1. 用户提交用户名 / 密码登录,服务端校验凭证合法性;
  2. 校验通过后,服务端生成唯一的SessionID,并在服务端内存 / 存储中创建Session(会话),保存用户身份信息(如 ID、角色);
  3. 服务端通过Set-Cookie响应头,将SessionID写入浏览器 Cookie;
  4. 后续用户发起请求时,浏览器会自动携带该 Cookie(SessionID);
  5. 服务端通过SessionID查询对应的Session,验证用户身份后放行;
  6. 用户登出时,服务端销毁Session并清除浏览器 Cookie。

这种模式的核心是 “服务端持有用户状态”,浏览器的 Cookie 仅作为SessionID的 “搬运工”。

2. 代码解析:Cookie/Session 登录的实现

以下结合提供的server.js(服务端)和index.html(前端)代码,拆解完整实现逻辑:

(1)服务端核心配置(server.js)
  • 基础准备:模拟用户数据库、内存级 Session 存储、中间件配置
// 模拟用户数据 
const usersDB = [
    {id: 1,username:"admin",password:"123",role:"admin"},
    {id: 2,username:"user",password:"123",role:"user"}
]; 
// 内存存储Session(key: SessionID, value: 用户信息) 
const sessionStore = {}; 
// 中间件:解析JSON请求体、URL编码请求体、Cookie 
app.use(express.json()); 
app.use(express.urlencoded({extended:true})); 
app.use(cookieParser());

Session 中间件:解析并验证 SessionID:该中间件负责从 Cookie 中提取SessionID,并查询sessionStore获取用户信息,挂载到req.user供后续使用:

function sessionMiddleware(req,res,next){
    const sessionId = req.cookies['sessionId']; // 从Cookie取SessionID
    if(!sessionId) return next(); // 无SessionID直接放行(后续守卫会拦截)
    const sessionData = sessionStore[sessionId]; // 查询Session
    if(sessionData){
        req.user = sessionData; // 挂载用户信息到请求对象
    }else{
        res.clearCookie('sessionId'); // Session失效,清除Cookie
    }
    next();
}

权限守卫:校验登录状态: 拦截未登录用户访问受保护接口,返回 401 未授权:

function authGuard(req,res,next){
    if(req.user) return next(); // 已登录则放行
    return res.status(401).json({error:'未授权,请先登录',code:'NO_SESSION'});
}

登录接口:生成 Session 并写入 Cookie:校验用户名密码后,生成唯一SessionID(uuid),存入sessionStore,并通过res.cookie设置 HttpOnly Cookie:

app.post('/api/login',(req,res) => {
    const { username,password } = req.body;
    const user = usersDB.find(u => u.username === username && u.password === password);
    if(!user) return res.status(401).json({error:"用户名或密码错误"});
    
    // 生成唯一SessionID,存储用户信息到Session
    const sessionId = uuidv4();
    sessionStore[sessionId] = {id:user.id,username:user.role,loginTime:new Date()};
    
    // 设置Cookie:HttpOnly防XSS、maxAge有效期1小时、全局路径
    res.cookie('sessionId',sessionId,{httpOnly:true,maxAge: 1000*60*60,path: '/'});
    res.json({message:'登录成功',user:sessionStore[sessionId]});
})

受保护接口:验证 Session 后返回用户信息:结合sessionMiddlewareauthGuard,仅允许已登录用户访问:

app.get('/api/profile',sessionMiddleware,authGuard,(req,res) => [
    res.json({message:'这是受保护的个人信息',data:req.user})
])

登出接口:销毁 Session 并清除 Cookie

app.post('/api/logout',(req,res) => {
    const sessionId = req.cookies['sessionId'];
    if(sessionId) {
        delete sessionStore[sessionId]; // 销毁服务端Session
        res.clearCookie('sessionId'); // 清除浏览器Cookie
    }
    res.json({message:'已退出登录'});
})
(2)前端核心逻辑(index.html)

前端无需手动处理 Cookie(浏览器自动携带),核心逻辑是:

  • 登录:提交用户名密码到/api/login,依赖浏览器自动保存 Cookie;
  • 状态检查:页面加载时调用/api/profile,通过接口响应判断是否登录,切换页面展示;
  • 访问受保护接口:直接请求/api/profile,浏览器自动携带 Cookie,无需手动传递;
  • 登出:调用/api/logout,清除服务端 Session 和浏览器 Cookie 后刷新状态。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Session 登录演示</title>
    <style>
        body { font-family: sans-serif; padding: 20px; max-width: 600px; margin: 0 auto; }
        .box { border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; border-radius: 5px; }
        .hidden { display: none; }
        button { cursor: pointer; padding: 8px 15px; }
        input { padding: 8px; margin-right: 5px; }
        #msg { color: red; margin-top: 10px; }
        #userInfo { color: green; }
    </style>
</head>
<body>

    <h2>🔐 原生 JS Session 登录演示</h2>

    <!-- 登录区域 -->
    <div id="loginBox" class="box">
        <h3>请登录</h3>
        <input type="text" id="username" placeholder="用户名 (admin)" value="admin">
        <input type="password" id="password" placeholder="密码 (123)" value="123">
        <button onclick="handleLogin()">登录</button>
        <p id="loginMsg"></p>
    </div>

    <!-- 用户信息区域 (受保护) -->
    <div id="userBox" class="box hidden">
        <h3>✅ 已登录</h3>
        <p id="userInfo">加载中...</p>
        <button onclick="fetchProfile()">刷新个人信息 (调用受保护接口)</button>
        <button onclick="handleLogout()" style="background:#fdd;">退出登录</button>
    </div>

    <script>
        // 1. 登录函数
        async function handleLogin() {
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const msgEl = document.getElementById('loginMsg');

            try {
                const res = await fetch('/api/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username, password })
                    // 注意:这里不需要手动设置 Cookie,浏览器收到响应后会自动保存 HttpOnly Cookie
                });

                const data = await res.json();

                if (res.ok) {
                    msgEl.style.color = 'green';
                    msgEl.innerText = '登录成功!正在跳转...';
                    checkStatus(); // 刷新界面状态
                } else {
                    msgEl.style.color = 'red';
                    msgEl.innerText = data.error || '登录失败';
                }
            } catch (e) {
                msgEl.innerText = '网络错误';
            }
        }

        // 2. 获取受保护数据 (演示中间件和守卫)
        async function fetchProfile() {
            const infoEl = document.getElementById('userInfo');
            infoEl.innerText = '正在请求服务器...';
            
            try {
                // 浏览器会自动带上之前保存的 sessionId Cookie
                const res = await fetch('/api/profile'); 
                const data = await res.json();

                if (res.ok) {
                    infoEl.innerText = `欢迎, ${data.data.username}! 角色: ${data.data.role}`;
                } else {
                    infoEl.innerText = `获取失败: ${data.error}`;
                    if(data.code === 'NO_SESSION') checkStatus(); // 如果没登录,刷新界面
                }
            } catch (e) {
                infoEl.innerText = '请求出错';
            }
        }

        // 3. 登出函数
        async function handleLogout() {
            await fetch('/api/logout', { method: 'POST' });
            checkStatus();
            alert('已退出');
        }

        // 4. 检查当前状态 (页面加载时调用)
        async function checkStatus() {
            const loginBox = document.getElementById('loginBox');
            const userBox = document.getElementById('userBox');
            const infoEl = document.getElementById('userInfo');

            // 尝试访问受保护接口来检测是否登录
            const res = await fetch('/api/profile');
            
            if (res.ok) {
                const data = await res.json();
                loginBox.classList.add('hidden');
                userBox.classList.remove('hidden');
                infoEl.innerText = `当前用户: ${data.data.username} (角色: ${data.data.role})`;
            } else {
                loginBox.classList.remove('hidden');
                userBox.classList.add('hidden');
            }
        }

        // 初始化检查
        checkStatus();
    </script>
</body>
</html>

3. Cookie/Session 的局限性

尽管 Cookie/Session 实现简单、安全性(HttpOnly Cookie)较高,但存在明显短板:

  • 内存 / 存储压力:Session 存储在服务端(示例中为内存),高并发场景下会占用大量服务器资源;
  • 分布式适配差:多服务器部署时,需同步 Session(如 Redis 共享),增加架构复杂度;
  • 多端适配局限:Cookie 仅在浏览器中自动携带,移动端 App(iOS/Android)无 Cookie 机制,需手动处理SessionID,适配成本高;
  • 跨域限制:Cookie 受同源策略限制,跨域场景需配置 CORS+Cookie 跨域,复杂度高。

二、JWT 双 Token 登录:现代前后端分离的优选方案

JWT(JSON Web Token)是一种无状态的认证方案,核心是将用户信息加密到 Token 中,服务端无需存储,仅通过验签即可验证身份。为解决单一 Token“过期不可控、泄露风险高” 的问题,衍生出 “Access Token + Refresh Token” 双 Token 模式。

1. 核心原理

(1)JWT 基础结构

JWT 由三部分组成(以.分隔):

  • Header:声明加密算法(如 HS256)和 Token 类型;
  • Payload:存储用户核心信息(如 ID、角色)、过期时间(exp),不存敏感信息(如密码);
  • Signature:服务端用密钥对 Header+Payload 加密生成,用于验签(防止 Token 被篡改)。
(2)双 Token 流程
  1. 用户登录:提交用户名密码,服务端校验通过后生成双 Token;

    • Access Token:短期有效(如 15 分钟),用于接口认证,轻量;
    • Refresh Token:长期有效(如 7 天),用于刷新 Access Token,存储在服务端(如数据库)做风控;
  2. 前端存储 Token:将双 Token 存储在 localStorage / 移动端缓存(非 Cookie);

  3. 接口请求:前端在请求头(如Authorization: Bearer {Access Token})携带 Access Token;

  4. 服务端验签:解码 Token,验证签名和过期时间,无需查存储;

  5. Token 刷新:Access Token 过期时,前端用 Refresh Token 请求/refresh-token接口,服务端校验 Refresh Token 合法性后,生成新的 Access Token;

  6. 登出:服务端将 Refresh Token 加入黑名单(或直接失效),前端清除本地 Token。

2. JWT 双 Token 的核心优势

  • 无状态:服务端无需存储用户状态(仅需存储 Refresh Token 做风控),降低服务器压力,天然适配分布式 / 微服务架构;
  • 多端适配:Token 可通过请求头传递,适配浏览器、移动端 App、小程序等所有终端;
  • 跨域友好:无需依赖 Cookie,跨域请求仅需在请求头携带 Token,配置简单;
  • 性能高效:服务端仅需验签(本地计算),无需查库 / 缓存,响应更快。

三、Cookie/Session 与 JWT 双 Token 的核心对比

表格

维度Cookie/Session 登录方案JWT 双 Token 登录方案
状态性有状态(服务端存储 Session)无状态(Access Token 无存储,Refresh Token 可选存储)
存储位置Session:服务端内存 / Redis;SessionID:浏览器 CookieAccess Token:前端本地;Refresh Token:服务端数据库(可选)
分布式适配需共享 Session(如 Redis),复杂度高天然支持,无需额外配置
多端适配仅适配浏览器(Cookie 自动携带),移动端适配差适配所有终端(浏览器 / APP / 小程序)
安全性Cookie 可设 HttpOnly 防 XSS;SessionID 泄露易被盗用Access Token 存储在前端(如 localStorage)有 XSS 风险;Refresh Token 可做风控(如 IP 绑定)
过期 / 销毁可主动销毁 Session,即时生效Access Token 过期不可主动撤销(需配合黑名单);Refresh Token 可主动失效
性能需查询 Session 存储,高并发下性能下降仅验签计算,性能稳定高效
跨域支持需配置 CORS+Cookie 跨域,复杂度高仅需请求头携带 Token,跨域配置简单

四、总结

Cookie/Session 是传统单体应用的经典方案,实现简单、浏览器端安全性高,但受限于 “有状态” 和 “Cookie 依赖”,难以适配高并发、分布式、多端的现代场景;

JWT 双 Token 方案以 “无状态” 为核心,解决了 Cookie/Session 的分布式、多端适配痛点,是前后端分离、微服务架构的优选,但需注意 Token 存储的 XSS 风险(可配合 HttpOnly Cookie 存储 Refresh Token)、Token 不可主动撤销(可通过黑名单机制补充)等问题。

实际开发中,需根据场景选择:传统单体应用可沿用 Cookie/Session;前后端分离、多端适配、分布式架构优先选择 JWT 双 Token 方案。