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

相关推荐
coding随想7 分钟前
HTML5插入标记的秘密:如何高效操控DOM而不踩坑?
前端·html
༺ཌༀ傲世万物ༀད༻7 分钟前
前端与后端部署大冒险:Java、Go、C++三剑客
java·前端·golang
TheRedAce15 分钟前
状态未保存,拦截页面跳转通用方法
前端
袁煦丞16 分钟前
小雅全家桶+cpolar影音库自由随身:cpolar内网穿透实验室第519个成功挑战
前端·程序员·远程工作
前端Hardy20 分钟前
HTML&CSS:超丝滑抛物线飞入购物车效果
前端·javascript·css
VisuperviReborn21 分钟前
打造自己的前端监控---前端错误监控
前端·javascript·vue.js
泉城老铁23 分钟前
Spring Boot 应用打包部署到 Tomcat ,如何极致调优看这里
java·spring boot·后端
WindrunnerMax24 分钟前
从零实现富文本编辑器#6-浏览器选区与编辑器选区模型同步
前端·前端框架·github
汪子熙24 分钟前
聊一聊 TypeScript 里的类型别名
前端·javascript
crossoverJie27 分钟前
StarRocks 如何在本地搭建存算分离集群
数据库·后端