人人都会AI编程

文件上传、Cookie、Session 原生实现

更新时间:2026-07-10

前面的内容已经覆盖了 HTTP 服务器的搭建、请求与响应的构造,以及客户端请求的发起。但在实际 Web 开发中,有三个功能几乎每个应用都会用到:接收客户端上传的文件维持会话状态的 Cookie,以及基于 Cookie 的服务端 Session 管理。主流框架(Express、Koa 等)都封装了成熟的时间,但在某些高性能场景或学习底层原理时,了解 Node.js 原生实现仍然是必要的。

本节将完全基于 httpfspath 等内置模块,实现一个支持文件上传、Cookie 读写和 Session 存储的简易 HTTP 服务,不引入任何第三方依赖。


1. 文件上传:手动解析 multipart/form-data

当 HTML 表单设置 enctype="multipart/form-data" 时,浏览器会将文件内容连同普通字段一同包装成多部分格式(multipart),通过 HTTP 请求体发送。Node.js 的 http 模块不会自动解析这种格式,我们需要从 TCP 字节流中一步步提取数据。

multipart 数据格式

一个上传文件的请求体大致如下:

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"

zhangsan
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png

<二进制文件内容>
------WebKitFormBoundary7MA4YWxkTrZu0gW--

整个请求体被一个随机生成的 boundary 分割成多个部分。每个部分包含头部信息(name、filename、Content-Type 等),然后是一段数据体。最后一个 boundary 后跟 -- 表示结束。

原生解析步骤

  1. 从请求头 content-type 中提取 boundary 字符串。
  2. 将请求体读取为 Buffer,按照 boundary 切割成多个部分。
  3. 遍历每个部分,解析出字段名、文件名和内容。

下面是完整实现:

const http = require('http');
const fs = require('fs');
const path = require('path');

function parseMultipart(req, options = {}) {
  const contentType = req.headers['content-type'];
  if (!contentType || !contentType.includes('multipart/form-data')) {
    throw new Error('Not a multipart request');
  }

  // 获取 boundary
  const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/);
  if (!boundaryMatch) throw new Error('No boundary found');
  const boundary = boundaryMatch[1] || boundaryMatch[2];
  const boundaryBuffer = Buffer.from('--' + boundary);

  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on('data', chunk => chunks.push(chunk));
    req.on('end', () => {
      const body = Buffer.concat(chunks);
      const fields = {};
      const files = [];

      // 按 boundary 切割各个部分
      let start = body.indexOf(boundaryBuffer) + boundaryBuffer.length;
      let end = body.indexOf(boundaryBuffer, start);

      while (end !== -1) {
        const part = body.slice(start, end - 2); // 去掉尾部的 \r\n
        start = end + boundaryBuffer.length;
        end = body.indexOf(boundaryBuffer, start);

        // 分离头部和体部
        const headerEnd = part.indexOf('\r\n\r\n');
        if (headerEnd === -1) continue;

        const headerText = part.slice(0, headerEnd).toString();
        const content = part.slice(headerEnd + 4);

        // 解析头部信息
        const nameMatch = headerText.match(/name="([^"]+)"/);
        const filenameMatch = headerText.match(/filename="([^"]+)"/);
        const contentTypeMatch = headerText.match(/Content-Type:\s*(.+)/i);

        const fieldName = nameMatch ? nameMatch[1] : null;
        if (!fieldName) continue;

        if (filenameMatch) {
          // 这是一个文件字段
          const filename = filenameMatch[1];
          const fileType = contentTypeMatch ? contentTypeMatch[1].trim() : 'application/octet-stream';
          const uploadDir = options.uploadDir || './uploads';

          if (!fs.existsSync(uploadDir)) {
            fs.mkdirSync(uploadDir, { recursive: true });
          }

          const savePath = path.join(uploadDir, Date.now() + '_' + filename);
          fs.writeFileSync(savePath, content);

          files.push({
            fieldName,
            filename,
            contentType: fileType,
            size: content.length,
            path: savePath
          });
        } else {
          // 普通文本字段
          fields[fieldName] = content.toString().trim();
        }
      }

      resolve({ fields, files });
    });

    req.on('error', reject);
  });
}

使用示例

const server = http.createServer(async (req, res) => {
  if (req.method === 'POST' && req.url === '/upload') {
    try {
      const result = await parseMultipart(req, { uploadDir: './uploads' });
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        message: 'Upload success',
        fields: result.fields,
        files: result.files
      }));
    } catch (err) {
      res.writeHead(500);
      res.end('Upload failed: ' + err.message);
    }
  } else {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(`
      <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="text" name="username" placeholder="用户名"><br>
        <input type="file" name="avatar"><br>
        <button type="submit">上传</button>
      </form>
    `);
  }
});

server.listen(3000);

运行后访问 http://localhost:3000,选择文件并提交,服务器就会在 uploads 目录下保存上传的文件,并返回 JSON 结果。

原生实现虽然直接,但需要处理 chunk 拼接、boundary 解析、文件写入等细节。生产环境通常建议使用成熟的库(如 busboyformidablemulter),它们在流式解析、内存控制、大文件处理方面已经做得非常成熟。


2. Cookie 操作:服务端读写与安全属性

Cookie 是浏览器存储的一小段文本数据(通常不超过 4KB),每次请求时浏览器会自动将匹配域名的 Cookie 发送到服务器,从而实现状态保持、用户识别等功能。

读取客户端发送的 Cookie

HTTP 请求头中的 Cookie 字段是一串键值对:

Cookie: theme=dark; lang=zh-CN; remember=1

可以编写一个工具函数将其解析为 JavaScript 对象:

function parseCookies(req) {
  const cookieHeader = req.headers.cookie;
  const cookies = {};
  if (cookieHeader) {
    cookieHeader.split(';').forEach(cookie => {
      const parts = cookie.split('=');
      const key = parts[0].trim();
      const value = parts[1] ? parts[1].trim() : '';
      cookies[key] = value;
    });
  }
  return cookies;
}

设置 Cookie 到客户端

服务端通过响应头 Set-Cookie 来指示浏览器保存 Cookie。一个 Cookie 可以携带多种属性:过期时间 Expires / Max-Age、路径 Path、域名 DomainSecure(仅 HTTPS)、HttpOnly(禁止 JS 访问)、SameSite 等。

我们可以封装一个设置 Cookie 的函数:

function setCookie(res, name, value, options = {}) {
  const defaults = {
    path: '/',
    httpOnly: true,       // 防止 XSS 攻击窃取 Cookie
    sameSite: 'Lax',      // 防范 CSRF
    secure: false         // 生产环境应设为 true
  };

  const opts = { ...defaults, ...options };
  let cookieStr = `${name}=${encodeURIComponent(value)}`;

  if (opts.maxAge) {
    cookieStr += `; Max-Age=${opts.maxAge}`;
  }
  if (opts.expires) {
    cookieStr += `; Expires=${opts.expires.toUTCString()}`;
  }
  if (opts.path) {
    cookieStr += `; Path=${opts.path}`;
  }
  if (opts.domain) {
    cookieStr += `; Domain=${opts.domain}`;
  }
  if (opts.secure) {
    cookieStr += `; Secure`;
  }
  if (opts.httpOnly) {
    cookieStr += `; HttpOnly`;
  }
  if (opts.sameSite) {
    cookieStr += `; SameSite=${opts.sameSite}`;
  }

  // 可能同时存在多个 Set-Cookie 头,使用数组
  const existing = res.getHeader('Set-Cookie') || [];
  const newHeaders = Array.isArray(existing) ? existing : [existing];
  newHeaders.push(cookieStr);
  res.setHeader('Set-Cookie', newHeaders);
}

Cookie 使用示例

const server = http.createServer((req, res) => {
  const cookies = parseCookies(req);

  if (req.url === '/login') {
    // 假设登录成功,设置一个 sessionId Cookie
    setCookie(res, 'sessionId', 'abc123xyz', {
      maxAge: 3600,      // 1小时
      httpOnly: true,
      sameSite: 'Lax'
    });
    res.end('Set cookie: sessionId');
  } else if (req.url === '/dashboard') {
    const sessionId = cookies.sessionId;
    if (sessionId === 'abc123xyz') {
      res.end('You are logged in!');
    } else {
      res.writeHead(403);
      res.end('Forbidden');
    }
  } else {
    res.end('Hello');
  }
});

server.listen(3000);

原生实现 Cookie 功能完全可行,但在实际项目中 Cookie 的签名、加密、防篡改等需求会进一步增加复杂度。框架中的 cookie-parser 等中间件已经解决了这些问题。


3. Session 实现:基于 Cookie 的会话管理

Session 是为了解决 HTTP 无状态特性而产生的服务端存储机制。服务端为每个用户生成一个唯一标识(sessionId),通过 Cookie 发送给客户端;后续请求中浏览器携带该标识,服务端再从存储(内存、Redis、数据库)中取出对应的会话数据。

原生实现步骤

  1. 创建一个 sessions Map 作为存储容器。
  2. 当用户首次访问时,生成一个随机 sessionId,设置 Cookie,并在 sessions 中初始化一个空对象。
  3. 后续请求根据 Cookie 中的 sessionId 查找对应会话数据。
  4. 提供简单的过期清理机制(可以结合定时器或惰性删除)。
const crypto = require('crypto');

class SessionManager {
  constructor(options = {}) {
    this.store = new Map();               // 简单内存存储
    this.maxAge = options.maxAge || 3600000; // 默认1小时
    this.cleanupInterval = options.cleanupInterval || 300000; // 每5分钟清理一次过期会话
    this._startCleanup();
  }

  // 生成随机 sessionId
  generateId() {
    return crypto.randomBytes(16).toString('hex');
  }

  // 获取或创建会话
  getSession(req, res) {
    const cookies = parseCookies(req);
    let sessionId = cookies.sessionId;
    let session = null;

    if (sessionId && this.store.has(sessionId)) {
      session = this.store.get(sessionId);
      // 检查是否过期
      if (Date.now() > session._expires) {
        this.store.delete(sessionId);
        sessionId = null;
        session = null;
      }
    }

    if (!session) {
      sessionId = this.generateId();
      session = { _expires: Date.now() + this.maxAge };
      this.store.set(sessionId, session);

      // 设置 Cookie
      setCookie(res, 'sessionId', sessionId, {
        maxAge: this.maxAge / 1000,
        httpOnly: true,
        sameSite: 'Lax'
      });
    }

    // 返回会话对象引用,方便业务代码直接修改
    return session;
  }

  // 销毁会话
  destroy(sessionId) {
    this.store.delete(sessionId);
  }

  // 清理过期会话
  _startCleanup() {
    setInterval(() => {
      const now = Date.now();
      for (const [id, session] of this.store.entries()) {
        if (now > session._expires) {
          this.store.delete(id);
        }
      }
    }, this.cleanupInterval);
  }
}

Session 使用示例:简易登录

const sessionManager = new SessionManager({ maxAge: 600000 }); // 10分钟

const server = http.createServer((req, res) => {
  const session = sessionManager.getSession(req, res);

  if (req.url === '/login' && req.method === 'POST') {
    // 简化的登录过程(实际应验证用户名密码)
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      const params = new URLSearchParams(body);
      if (params.get('user') === 'admin' && params.get('pass') === '123456') {
        session.user = {
          name: 'admin',
          role: 'admin'
        };
        res.writeHead(302, { Location: '/dashboard' });
        res.end();
      } else {
        res.end('Login failed');
      }
    });
  } else if (req.url === '/dashboard') {
    if (session.user) {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(`<h1>Welcome, ${session.user.name}!</h1><a href="/logout">Logout</a>`);
    } else {
      res.writeHead(403);
      res.end('Please login first');
    }
  } else if (req.url === '/logout') {
    // 获取 sessionId 并销毁
    const cookies = parseCookies(req);
    if (cookies.sessionId) {
      sessionManager.destroy(cookies.sessionId);
    }
    setCookie(res, 'sessionId', '', { maxAge: 0 }); // 清除客户端Cookie
    res.writeHead(302, { Location: '/' });
    res.end();
  } else {
    res.end(`
      <form method="post" action="/login">
        <input name="user" placeholder="User"><br>
        <input name="pass" type="password" placeholder="Password"><br>
        <button type="submit">Login</button>
      </form>
    `);
  }
});

server.listen(3000);

这个示例完整实现了基于内存的 Session 管理,包括登录状态保持、访问控制和退出登录。


4. 原生实现的现实考量

通过上面的例子可以看到,完全依赖 Node.js 原生模块实现文件上传、Cookie 和 Session 在技术上是可行的,并且能够帮助我们:

  • 深入理解 HTTP 协议的数据传输机制。
  • 掌握二进制数据处理和流解析的技巧。
  • 在受限环境(如嵌入式设备)中减少依赖。

但在实际生产项目中,这种做法通常不建议

  • 手动解析 multipart 对内存使用不够高效,无法处理超大文件的上传。
  • Cookie 操作缺少签名和加密,容易导致安全漏洞(如会话劫持)。
  • 内存 Session 无法跨进程共享,且重启即丢失。

因此,绝大多数 Node.js 应用都会选择成熟的中间件:

  • 文件上传:multerformidablebusboy
  • Cookie 解析:cookie-parser
  • Session 管理:express-session(可对接 Redis、MongoDB 等存储)

下一章我们将介绍这些框架级中间件的用法,帮助你在保证安全性和可维护性的前提下,快速实现这些基础功能。