Cookie/Session vs JWT 双 Token:登录认证方案的演进与对比
登录认证是 Web 应用的核心能力,它决定了用户能否安全访问受保护的资源。随着开发模式从传统单体应用向前后端分离、多端适配(浏览器 / 移动端)演进,登录认证方案也从经典的 Cookie/Session 体系,发展出更适配现代场景的 JWT 双 Token 方案。本文将先拆解 Cookie/Session 登录的核心原理并结合代码解析,再介绍 JWT 双 Token 的实现逻辑,最后对比两种方案的优劣与适用场景。
一、Cookie/Session 登录:传统认证的核心逻辑
1. 核心原理
Cookie/Session 是基于 “服务端状态存储” 的认证模式,核心是用 Session 存储用户身份,用 Cookie 传递 Session 标识,完整流程如下:
- 用户提交用户名 / 密码登录,服务端校验凭证合法性;
- 校验通过后,服务端生成唯一的
SessionID,并在服务端内存 / 存储中创建Session(会话),保存用户身份信息(如 ID、角色); - 服务端通过
Set-Cookie响应头,将SessionID写入浏览器 Cookie; - 后续用户发起请求时,浏览器会自动携带该 Cookie(
SessionID); - 服务端通过
SessionID查询对应的Session,验证用户身份后放行; - 用户登出时,服务端销毁
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 后返回用户信息:结合sessionMiddleware和authGuard,仅允许已登录用户访问:
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 流程
-
用户登录:提交用户名密码,服务端校验通过后生成双 Token;
- Access Token:短期有效(如 15 分钟),用于接口认证,轻量;
- Refresh Token:长期有效(如 7 天),用于刷新 Access Token,存储在服务端(如数据库)做风控;
-
前端存储 Token:将双 Token 存储在 localStorage / 移动端缓存(非 Cookie);
-
接口请求:前端在请求头(如
Authorization: Bearer {Access Token})携带 Access Token; -
服务端验签:解码 Token,验证签名和过期时间,无需查存储;
-
Token 刷新:Access Token 过期时,前端用 Refresh Token 请求
/refresh-token接口,服务端校验 Refresh Token 合法性后,生成新的 Access Token; -
登出:服务端将 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:浏览器 Cookie | Access 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 方案。