🍪 前后端相连小项目实战: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吧!🍪✨

相关推荐
expect7g6 分钟前
Flink-Checkpoint-1.源码流程
后端·flink
天天向上10247 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
00后程序员12 分钟前
Fiddler中文版如何提升API调试效率:本地化优势与开发者实战体验汇总
后端
芬兰y23 分钟前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁30 分钟前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry30 分钟前
Fetch 笔记
前端·javascript
拾光拾趣录31 分钟前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟32 分钟前
vue3,你看setup设计详解,也是个人才
前端
Lefan36 分钟前
一文了解什么是Dart
前端·flutter·dart
用户81221993672238 分钟前
C# .Net Core零基础从入门到精通实战教程全集【190课】
后端