前言
大家好!今天我们要聊的是一个让前后端"牵手成功"的关键技术------Cookie!这可不是你平时吃的饼干🍪,而是Web开发中不可或缺的身份验证小能手。准备好了吗?让我们开始这段美味的技术之旅!
🌐 HTTP协议:一个健忘的家伙
首先,我们得认识一下HTTP协议------这个Web世界的"社交达人"。不过这位朋友有个大毛病:记性特别差!每次见面都像第一次认识你一样,这就是所谓的"无状态"协议。
javascript
// HTTP的内心OS:
function handleRequest() {
// 每次请求都是全新的开始
console.log("你是谁?我们见过吗?");
}
这就像你去咖啡店☕,每次店员都问:"您是第一次来吗?"------即使你昨天才来过!为了解决这个问题,聪明的程序员们发明了Cookie这个小饼干。
🍪 Cookie是什么?
Cookie是存储在用户浏览器中的小文本文件,就像你的会员卡💳:
- 大小:通常只有几KB(真的是一块"小"饼干)
- 内容:身份令牌、用户偏好等
- 位置:乖乖待在浏览器里
- 工作方式:每次HTTP请求自动捎带上它

如果你还想更加了解相关的技术可以看看:前端数据存储总结:Cookie、localStorage、sessionStorage与IndexedDB的使用与区别
💻 前端代码:优雅的表单处理
让我们看看前端如何优雅地处理登录:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Storage</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>Cookie </h1>
<div id="app">
<!-- 是否已经登录?Cookie? 检查 -->
<section id="loginSection" >
<form id="loginForm">
<input type="text" id="username" placeholder="Username" required />
<input type="password" id="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
</section>
<section id="welcomeSection" style="display:none;">
<p>Welcome, <span id="userDisplay"></span></p>
<button id="logoutBtn">Logout</button>
</section>
<button id="loginBtn">Login</button>
</div>
<script src="./script.js"></script>
</body>
</html>
javascript
const loginForm = document.querySelector('#loginForm');
loginForm.addEventListener('submit', async (e) => {
e.preventDefault(); // 阻止表单默认提交行为
const username = document.querySelector('#username').value.trim();
const password = document.querySelector('#password').value.trim();
// 添加输入验证
if (!username || !password) {
alert('用户名和密码不能为空哦~');
return;
}
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password }),
credentials: 'include' // 重要!确保携带Cookie
});
if (!response.ok) throw new Error('登录失败');
const data = await response.json();
if (data.success) {
location.reload(); // 刷新页面更新状态
} else {
alert(data.msg || '登录失败,请重试');
}
} catch (err) {
console.error("登录出错:", err);
alert('登录出错了,请稍后再试~');
}
});
// 检查登录状态
async function checkLoginStatus() {
try {
const response = await fetch('/check-login', {
credentials: 'include' // 同样需要携带Cookie
});
const data = await response.json();
const loginSection = document.querySelector('#loginSection');
const welcomeSection = document.querySelector('#welcomeSection');
if (data.loginIn) {
loginSection.style.display = 'none';
welcomeSection.style.display = 'block';
document.querySelector('#userDisplay').textContent = data.username;
} else {
loginSection.style.display = 'block';
welcomeSection.style.display = 'none';
}
} catch (err) {
console.error("检查登录状态出错:", err);
}
}
// 页面加载时检查登录状态
document.addEventListener('DOMContentLoaded', checkLoginStatus);
// 添加登出功能
document.querySelector('#logoutBtn')?.addEventListener('click', async () => {
try {
const response = await fetch('/logout', {
method: 'POST',
credentials: 'include'
});
if (response.ok) location.reload();
} catch (err) {
console.error("登出出错:", err);
}
});
🚨 易错点警示:
- 忘记credentials :使用fetch时如果不设置
credentials: 'include'
,Cookie不会被发送 - XSS攻击:永远不要在前端存储敏感信息,Cookie应该设置HttpOnly
- CSRF防护:考虑添加CSRF令牌,后面会讲到
🖥️ 后端代码:Cookie大厨
让我们看看后端如何"烘焙"Cookie:
javascript
// 优化后的server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const querystring = require('querystring');
// 模拟用户数据库
const users = {
admin: { password: '123456', name: '管理员' },
user: { password: '654321', name: '普通用户' }
};
// 会话存储(生产环境请用Redis等)
const sessions = {};
const server = http.createServer(async (req, res) => {
const { method, url: reqUrl, headers } = req;
const { pathname, query } = url.parse(reqUrl);
const queryParams = querystring.parse(query);
// 静态文件服务
if (method === 'GET' && ['/', '/index.html'].includes(pathname)) {
serveStaticFile(res, 'public/index.html', 'text/html');
}
else if (method === 'GET' && pathname === '/style.css') {
serveStaticFile(res, 'public/style.css', 'text/css');
}
else if (method === 'GET' && pathname === '/script.js') {
serveStaticFile(res, 'public/script.js', 'text/javascript');
}
// 登录接口
else if (method === 'POST' && pathname === '/login') {
try {
const body = await parseRequestBody(req);
const { username, password } = body;
if (!username || !password) {
sendResponse(res, 400, { success: false, msg: '用户名和密码不能为空' });
return;
}
const user = users[username];
if (user && user.password === password) {
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2)}`;
sessions[sessionId] = { username, name: user.name };
res.writeHead(200, {
'Set-Cookie': `sessionId=${sessionId}; HttpOnly; Path=/; Max-Age=3600`, // 1小时过期
'Content-Type': 'application/json'
});
sendResponse(res, 200, {
success: true,
msg: '登录成功',
username: user.name
});
} else {
sendResponse(res, 401, { success: false, msg: '用户名或密码错误' });
}
} catch (err) {
console.error('登录处理错误:', err);
sendResponse(res, 500, { success: false, msg: '服务器错误' });
}
}
// 检查登录状态
else if (method === 'GET' && pathname === '/check-login') {
try {
const cookies = parseCookies(headers.cookie);
const sessionId = cookies.sessionId;
if (sessionId && sessions[sessionId]) {
const user = sessions[sessionId];
sendResponse(res, 200, {
loginIn: true,
username: user.name
});
} else {
sendResponse(res, 200, {
loginIn: false,
username: ''
});
}
} catch (err) {
console.error('检查登录状态错误:', err);
sendResponse(res, 500, { loginIn: false, username: '' });
}
}
// 登出接口
else if (method === 'POST' && pathname === '/logout') {
const cookies = parseCookies(headers.cookie);
const sessionId = cookies.sessionId;
if (sessionId) {
delete sessions[sessionId];
}
res.writeHead(200, {
'Set-Cookie': 'sessionId=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
'Content-Type': 'application/json'
});
sendResponse(res, 200, { success: true, msg: '登出成功' });
}
// 404处理
else {
res.writeHead(404);
res.end('Not Found');
}
});
// 辅助函数:提供静态文件
function serveStaticFile(res, filePath, contentType) {
const fullPath = path.join(__dirname, filePath);
fs.readFile(fullPath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
res.writeHead(404);
res.end('File not found');
} else {
res.writeHead(500);
res.end('Server error');
}
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
});
}
// 辅助函数:解析请求体
function parseRequestBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
}
// 辅助函数:解析Cookie
function parseCookies(cookieHeader) {
if (!cookieHeader) return {};
return cookieHeader.split(';').reduce((cookies, cookie) => {
const [name, value] = cookie.trim().split('=');
cookies[name] = value;
return cookies;
}, {});
}
// 辅助函数:发送JSON响应
function sendResponse(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
server.listen(8080, () => {
console.log('服务器运行在 http://localhost:8080');
});
🚨 后端易错点:
- Cookie安全设置:忘记设置HttpOnly和Secure(HTTPS下)会让Cookie容易被窃取
- 会话管理:内存存储会话重启会丢失,生产环境要用数据库或Redis
- 密码存储:示例中明文存储密码,实际应用一定要加盐哈希
- CSRF防护:缺少CSRF防护,后面会补充
🔒 安全加固:给你的Cookie穿上防弹衣
1. Cookie安全属性
javascript
// 不安全的Cookie
'Set-Cookie': 'sessionId=123456'
// 安全的Cookie
'Set-Cookie': `sessionId=${sessionId}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600`
- HttpOnly:阻止JavaScript访问,防XSS
- Secure:只在HTTPS下传输
- SameSite:防CSRF攻击
- Max-Age/Expires:控制生命周期
2. 添加CSRF防护
javascript
// 生成CSRF令牌
const generateCSRFToken = () => require('crypto').randomBytes(32).toString('hex');
// 登录接口修改
const csrfToken = generateCSRFToken();
sessions[sessionId] = { username, name: user.name, csrfToken };
res.writeHead(200, {
'Set-Cookie': [
`sessionId=${sessionId}; HttpOnly; Path=/; Max-Age=3600`,
`csrfToken=${csrfToken}; Path=/; Max-Age=3600`
],
'Content-Type': 'application/json'
});
// 前端需要将CSRF令牌放入请求头
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrfToken') // 从Cookie中获取
}
⚡ 性能优化:让Cookie更轻盈
- 最小化Cookie大小:只存储必要信息(如session ID)
- 合理设置过期时间:平衡安全性与用户体验
- 使用子域名:static.example.com不用携带主站Cookie
- CDN优化:静态资源使用无Cookie域名
🌟 最佳实践总结
-
前端:
- 使用
credentials: 'include'
确保Cookie发送 - 敏感操作添加CSRF令牌
- 合理处理登录状态UI变化
- 使用
-
后端:
- 设置安全的Cookie属性
- 会话信息服务器端存储
- 实现完整的登录/登出流程
- 添加输入验证和错误处理
-
安全:
- 永远不要信任客户端数据
- HTTPS是必须的
- 定期更换会话密钥
🎯 扩展思考
- JWT vs Session Cookie:各有什么优劣?
- OAuth/SSO:第三方登录如何实现?
- 无状态认证:如何在微服务架构中处理认证?
🏁 结语
Cookie虽小,却是Web开发的基石之一。就像现实生活中的会员卡,用好了能让用户体验流畅自然,用不好则可能带来安全隐患。希望这篇"饼干制作指南"能帮助你在前后端联调的道路上越走越顺!
记住:好的开发者就像优秀的糕点师,既要让"饼干"美味可口,又要保证它安全卫生。现在,去烘焙属于你的完美Cookie吧!🍪✨