🍪 前后端相连小项目实战:Cookic的作用

前言

大家好!今天我们要聊的是一个让前后端"牵手成功"的关键技术------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);
    }
});

🚨 易错点警示:

  1. 忘记credentials :使用fetch时如果不设置credentials: 'include',Cookie不会被发送
  2. XSS攻击:永远不要在前端存储敏感信息,Cookie应该设置HttpOnly
  3. 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');
});

🚨 后端易错点:

  1. Cookie安全设置:忘记设置HttpOnly和Secure(HTTPS下)会让Cookie容易被窃取
  2. 会话管理:内存存储会话重启会丢失,生产环境要用数据库或Redis
  3. 密码存储:示例中明文存储密码,实际应用一定要加盐哈希
  4. 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更轻盈

  1. 最小化Cookie大小:只存储必要信息(如session ID)
  2. 合理设置过期时间:平衡安全性与用户体验
  3. 使用子域名:static.example.com不用携带主站Cookie
  4. CDN优化:静态资源使用无Cookie域名

🌟 最佳实践总结

  1. 前端

    • 使用credentials: 'include'确保Cookie发送
    • 敏感操作添加CSRF令牌
    • 合理处理登录状态UI变化
  2. 后端

    • 设置安全的Cookie属性
    • 会话信息服务器端存储
    • 实现完整的登录/登出流程
    • 添加输入验证和错误处理
  3. 安全

    • 永远不要信任客户端数据
    • HTTPS是必须的
    • 定期更换会话密钥

🎯 扩展思考

  1. JWT vs Session Cookie:各有什么优劣?
  2. OAuth/SSO:第三方登录如何实现?
  3. 无状态认证:如何在微服务架构中处理认证?

🏁 结语

Cookie虽小,却是Web开发的基石之一。就像现实生活中的会员卡,用好了能让用户体验流畅自然,用不好则可能带来安全隐患。希望这篇"饼干制作指南"能帮助你在前后端联调的道路上越走越顺!

记住:好的开发者就像优秀的糕点师,既要让"饼干"美味可口,又要保证它安全卫生。现在,去烘焙属于你的完美Cookie吧!🍪✨

相关推荐
dy171742 分钟前
element-plus表格默认展开有子的数据
前端·javascript·vue.js
2501_915918414 小时前
Web 前端可视化开发工具对比 低代码平台、可视化搭建工具、前端可视化编辑器与在线可视化开发环境的实战分析
前端·低代码·ios·小程序·uni-app·编辑器·iphone
程序员爱钓鱼4 小时前
Go语言实战案例 — 工具开发篇:实现一个图片批量压缩工具
后端·google·go
程序员的世界你不懂5 小时前
【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇
java·前端·数据库
索迪迈科技5 小时前
网络请求库——Axios库深度解析
前端·网络·vue.js·北京百思可瑞教育·百思可瑞教育
gnip5 小时前
JavaScript二叉树相关概念
前端
attitude.x6 小时前
PyTorch 动态图的灵活性与实用技巧
前端·人工智能·深度学习
β添砖java6 小时前
CSS3核心技术
前端·css·css3
ChinaRainbowSea6 小时前
7. LangChain4j + 记忆缓存详细说明
java·数据库·redis·后端·缓存·langchain·ai编程
舒一笑6 小时前
同步框架与底层消费机制解决方案梳理
后端·程序员