afiqandico13/tokokita
GitHub: afiqandico13/tokokita
TokoKita 是一个以安全加固为核心特色的 Node.js + Express 电商示例应用,演示了如何在完整的购物车与后台管理业务中落地多种 Web 安全防护措施。
Stars: 0 | Forks: 0
# TokoKita — 仿 Indomaret 风格的电商网站 🧺




## 功能
**顾客:**
- 按分类浏览商品
- 添加到购物车,更新数量,移除商品
- 填写姓名 / 电话 / 地址进行结账
- 结账成功后自动扣减库存
**管理员**(`/admin/login`,默认账号 `admin` / `admin123`):
- 基于 session 的登录
- 带有商品/订单/收入统计的数据看板
- 商品 CRUD(支持图片上传)
- 分类 CRUD
- 查看所有订单 + 更新状态(`pending → diproses → dikirim → selesai → cancelled`)
**视觉打磨:**
- 带有交互式 3D 旋转购物篮的 Hero 区域
- 鼠标悬停时带有 3D 倾斜效果的商品卡片
- 橄榄绿配色方案,使用 Fraunces + Inter 字体排版
## 快速开始
```
# 1. 安装依赖
npm install
# 2. 启动 server
npm start
# 3. 打开 browser
# Customer shop: http://localhost:3000
# Admin login: http://localhost:3000/admin/login
```
默认管理员:`admin` / `admin123` — **在部署到公共服务器之前,请务必修改。**
## 技术栈
| 层级 | 选型 | 原因 |
|---|---|---|
| 运行时 | Node.js ≥ 18 | 现代语法、fetch、原生测试运行器 |
| 框架 | Express 5 | 极简、知名度高 |
| 模板 | EJS | 服务端渲染,默认自动转义 |
| 数据库 | sql.js (基于 WebAssembly 的 SQLite) | 零原生编译 — 随处运行 |
| Session | express-session | 服务端,签名 cookie |
| 密码 | bcryptjs | 纯 JS 实现 bcrypt(无需原生构建) |
| 上传 | multer | 事实上的 multipart 解析器 |
## 🔒 安全加固 — 我做了哪些强化
这原本只是一个基础的 CRUD 示例。随后我进行了手动安全审计,并应用了以下修复:
### 1. 安全响应头 (`helmet`)
- 限制 script/style 来源的 Content-Security-Policy(允许 Three.js CDN)
- X-Frame-Options: SAMEORIGIN(防点击劫持)
- X-Content-Type-Options: nosniff
- Strict-Transport-Security(仅限生产环境)
- Referrer-Policy: no-referrer
### 2. CSRF 防护(双重提交 Cookie 模式)
- 存储在 `req.session.csrfToken` 中的每个 session 独有的 token
- 通过 `req.body._csrf` 对每个非 multipart 的 POST 请求进行验证
- 辅助局部视图 `views/partials/csrf.ejs` 会自动包含在所有 11 个表单中
- 登录时轮换 token(`req.session.regenerate`)以防 session 固定攻击
### 3. Session 加固
- 密钥来自 `process.env.SESSION_SECRET`(开发环境下回退为随机的 48 字节十六进制字符串)
- Cookie:`httpOnly`、`secure`(生产环境)、`sameSite: 'lax'`、4 小时过期
- 自定义 Cookie 名称(`toko.sid`,而非默认的 `connect.sid`)
- 登录/登出时重新生成 session ID
### 4. 速率限制 (`express-rate-limit`)
- `/admin/login`:每个 IP 每 15 分钟 5 次尝试
- 返回 `Retry-After` + `RateLimit-*` 标准响应头
### 5. 文件上传校验 (`multer`)
- MIME 类型白名单:`image/jpeg`、`image/png`、`image/webp`、`image/gif`
- 扩展名白名单(纵深防御 — 仅靠 MIME 类型是不够的)
- 文件名使用 `crypto.randomUUID()`(无用户控制的路径)
- 大小限制为 2 MB
- 这会阻止经典的**通过上传 `.html`/`.svg` payload 进行的存储型 XSS**
### 6. 开放重定向修复
`/cart/add` 最初会重定向到 `req.get('referer')` — 这允许攻击者构造来自 `evil.com` 的链接,在受害者加入购物车后对其进行重定向。现在会验证 referer 的主机是否与请求的主机匹配。
### 7. 计时安全的密码比较
始终调用 `bcrypt.compareSync`(如果未找到用户,则使用一个虚拟哈希),这样响应时间就不会暴露哪些用户名是存在的。
### 8. 原子化库存扣减
竞态条件修复:`UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`
如果在结账过程中库存耗尽,将返回 `rowsModified = 0`。结合 transaction wrapper,可以防止超卖。
### 9. 输入长度校验
所有文本字段都在服务端进行截断(姓名:200 字符,电话:30,地址:500,分类:100,顾客字段类似)。防止通过 10 MB 的提交导致数据库膨胀。
### 10. 订单状态白名单
`POST /admin/orders/:id/status` 在更新之前,会针对一个枚举值验证新状态 — 防止任意状态字符串注入。
### 11. 全局错误处理器
生产环境的响应会隐藏堆栈跟踪;开发模式则会显示它们。404 会返回一个样式化的错误页面,而不是泄露内部信息。
## 已知的权衡
**Multipart CSRF:** CSRF 检查对 `multipart/form-data` 是跳过的,因为 multer 会消耗 body 流,并与基于 body-parser 的 token 验证相冲突。补偿性控制措施:
- `SameSite=Lax` cookie(浏览器会阻止跨站 POST)
- 每个管理员上传路由上都带有 `requireAdmin` 中间件
- 文件类型校验作为主要防御手段
对于高安全性的部署,请通过自定义 HTTP header(`X-CSRF-Token`)而不是 body 字段来发送 CSRF token,这可以通过表单的 JS 来设置。
## 项目结构
```
toko-app/
├── server.js # Express app + all routes + security middleware
├── package.json
├── db/
│ ├── database.js # sql.js wrapper with transaction() + atomic decrement
│ └── toko.sqlite # Created on first run (auto-seeded with sample data)
├── public/
│ ├── css/style.css # Olive-green palette, Fraunces + Inter fonts
│ ├── js/
│ │ ├── hero-3d.js # Three.js rotating shopping basket
│ │ └── tilt.js # 3D tilt effect on product cards
│ └── uploads/ # Product images (gitignored)
└── views/
├── index.ejs, cart.ejs, checkout.ejs, order-success.ejs
├── error.ejs
├── partials/
│ ├── header.ejs, footer.ejs
│ └── csrf.ejs # CSRF hidden input — include in every form
└── admin/
├── login.ejs, dashboard.ejs, products.ejs, orders.ejs, order-detail.ejs
└── _layout_top.ejs, _layout_bottom.ejs
```
## 生产环境检查清单
在将其部署到公共服务器之前,请执行以下操作:
1. **设置 `SESSION_SECRET`** 为一个稳定的随机值:
export SESSION_SECRET=*** -e "console.log(require('crypto').randomBytes(48).toString('hex'))")
2. **设置 `NODE_ENV=production`** 以启用 `secure` cookie + HSTS
3. **修改默认管理员密码** — 编辑 `db/database.js` 的 `seedAdmin()`
或添加一个密码修改 UI
4. **放置在 HTTPS 反向代理之后**(nginx / Caddy / Cloudflare)并设置
`TRUST_PROXY=1`,以便 `req.ip` 能反映真实的客户端 IP
5. **考虑迁移**:从 `sql.js`(内存 + 持久化)迁移到
`better-sqlite3`(原生,适当的文件锁定)以应对生产环境的流量
6. **添加支付集成**(Midtrans、Xendit、Stripe)以实现真实的结账
7. **添加顾客账户**,以便可以按顾客查询订单历史
## 如果有更多时间,我会改进的地方
- 面向顾客的订单历史 + 状态跟踪
- 新订单的电子邮件/短信通知
- 商品搜索 + 按价格筛选
- 图片变体(缩略图,响应式 srcset)
- 自动化测试(`node:test` + supertest)
- GitHub Actions CI,在 push 时进行 lint + 冒烟测试
- Docker Compose 实现一键部署
## 作者
**Afiq Andico Pangimpian** — IT 专家 & 极客,印度尼西亚巴厘岛。
- GitHub: [@afiqandico13](https://github.com/afiqandico13)
- 联系方式: afiqandico13@gmail.com
作为一个作品集项目而构建,旨在通过一个具体、可运行的应用程序来演示全栈 Web 安全知识,而不是抽象的例子。
## 许可证
MIT — 查看 [LICENSE](LICENSE)。
标签:Express, GitHub Advanced Security, GNU通用公共许可证, MITM代理, Node.js, SQLite, Three.js, 安全加固, 电子商务, 自定义脚本