Browse Source

更新鉴权为 Cookie 令牌方案

admincjy 1 month ago
parent
commit
3d92a8536b
5 changed files with 60 additions and 120 deletions
  1. 25 21
      CLAUDE.md
  2. 0 65
      package-lock.json
  3. 0 1
      package.json
  4. 1 1
      public/js/reader.js
  5. 34 32
      server.js

+ 25 - 21
CLAUDE.md

@@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 ## 项目概述
 
 CJYDocs 是一个基于 Node.js + Express 的轻量级 Markdown 文档管理和渲染系统,采用前后端分离架构。核心特点包括:
-- 🔐 **登录认证系统** - 基于 express-session 的会话管理,7天免登录
+- 🔐 **登录认证系统** - 基于 Cookie 令牌的轻量认证,7天免登录
 - 通过解析 `index.md` 配置文件实现文档的层级化管理
 - 支持在线编辑功能,智能光标定位
 - 全文搜索、TOC 目录、响应式设计
@@ -132,40 +132,41 @@ const DOM = {
 **安全机制**:
 - 所有文档和图片 API 都需要登录认证(`requireAuth` 中间件)
 - 用户输入经过 `sanitizePath()` 清理和 `validatePath()` 验证,防止路径遍历攻击
-- Session 配置为 HttpOnly Cookie,防止 XSS 攻击
+- 认证令牌通过 HttpOnly Cookie 存储(`cjydocs.token`),防止 XSS 攻击
 
 **缓存策略**:
-`index.md` 解析结果缓存5秒(`server.js:14-16`),减少文件系统读取频率。
+`index.md` 解析结果缓存5秒(`server.js:31-33`),减少文件系统读取频率。
 
 ### 关键功能实现
 
-#### 0. 登录认证系统
-`server.js:10-167` - 完整的会话管理和认证系统:
+#### 0. 登录认证系统 (Cookie 令牌)
+`server.js:5-195` - 基于 Cookie 令牌的认证系统:
 
 **认证流程**:
 ```
-用户访问 / 或 /reader.html → 检查 session.isAuthenticated
+用户访问 / 或 /reader.html → 检查 `cjydocs.token` Cookie 是否匹配固定令牌
     ↓ (未登录)
 重定向到 /login.html
 用户输入密码 → POST /api/login
-验证密码 (cjy@0526) → 创建 session
+验证密码 (cjy@0526) → 设置 token Cookie
 重定向到首页 → 可以访问所有文档
 ```
 
-**Session 配置** (`server.js:48-61`):
+**Token Cookie 配置** (`server.js:14-21`):
+- Cookie 名称: `cjydocs.token`
 - 有效期: 7天 (`maxAge: 7 * 24 * 60 * 60 * 1000`)
-- Cookie 名称: `cjydocs.sid`
 - HttpOnly: true (防止 XSS)
 - SameSite: lax (允许合理的跨站请求)
-- Rolling: true (每次请求重置过期时间)
+- Secure: false (生产环境应启用 HTTPS 并设为 true)
+- Path: `/`
 
-**认证中间件** (`server.js:64-76`):
+**认证中间件** (`server.js:118-131`):
 ```javascript
 function requireAuth(req, res, next) {
-  if (req.session && req.session.isAuthenticated) {
+  if (isAuthenticated(req)) {
     return next();
   }
   // API 请求返回 401,页面请求重定向
@@ -180,6 +181,10 @@ function requireAuth(req, res, next) {
 - 硬编码在 `server.js:11` (`PASSWORD = 'cjy@0526'`)
 - 生产环境建议改用环境变量: `process.env.PASSWORD`
 
+**令牌配置**:
+- 固定令牌在 `server.js:5` (`SECERT_TOKEN = 'cjy123'`)
+- Cookie 名称与参数在 `TOKEN_COOKIE_NAME/TOKEN_COOKIE_OPTIONS`
+
 #### 1. Markdown 渲染
 使用 `marked.js` + `highlight.js`,配置在 `reader.js:25-38`
 
@@ -268,7 +273,7 @@ saveDocument() → PUT /api/doc/:category/:docName
 ```
 cjydocs/
 ├── server.js              # Express 后端(499行)
-│                          # - 登录认证和会话管理
+│                          # - 登录认证 (Cookie 令牌)
 │                          # - 所有 API 端点(GET/PUT/POST)
 │                          # - index.md 解析逻辑(带5秒缓存)
 │                          # - 安全验证函数(sanitizePath, validatePath)
@@ -335,12 +340,12 @@ cjydocs/
 
 ## 安全注意事项
 
-### 1. 认证和会话安全
-**Session 配置**:
+### 1. 认证和 Cookie 安全
+**Token Cookie 配置**:
 - HttpOnly Cookie - 防止 JavaScript 访问(XSS 防护)
-- SameSite: lax - 防止 CSRF 攻击
-- 会话密钥应使用环境变量,不要硬编码
+- SameSite: lax - 降低 CSRF 风险
 - 生产环境建议启用 HTTPS 并设置 `secure: true`
+- 固定令牌不应硬编码,建议使用环境变量或更安全的签名/随机令牌方案
 
 **密码安全**:
 - 当前使用硬编码密码 `cjy@0526`,仅适用于开发/个人使用
@@ -398,11 +403,10 @@ if (!validatePath(docPath, docsDir)) {
 检查:
 1. 浏览器是否启用了 Cookie
 2. 浏览器控制台是否有 CORS 错误
-3. Session 配置是否正确(特别是跨域场景)
+3. Cookie 配置是否正确(特别是跨域/HTTPS 场景)
 
-#### Session 频繁过期
-- 检查 `server.js:48-61` 中的 session 配置
-- `rolling: true` 应该会在每次请求时重置过期时间
+#### Cookie 频繁失效
+- 检查 `server.js:14-21` 中的 `TOKEN_COOKIE_OPTIONS` 配置
 - 确保浏览器没有频繁清除 Cookie
 
 ### 文档显示为空或 404

+ 0 - 65
package-lock.json

@@ -11,7 +11,6 @@
       "dependencies": {
         "cors": "^2.8.5",
         "express": "^4.18.2",
-        "express-session": "^1.18.2",
         "marked": "^11.1.0"
       },
       "devDependencies": {
@@ -386,40 +385,6 @@
         "url": "https://opencollective.com/express"
       }
     },
-    "node_modules/express-session": {
-      "version": "1.18.2",
-      "resolved": "https://registry.npmmirror.com/express-session/-/express-session-1.18.2.tgz",
-      "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
-      "license": "MIT",
-      "dependencies": {
-        "cookie": "0.7.2",
-        "cookie-signature": "1.0.7",
-        "debug": "2.6.9",
-        "depd": "~2.0.0",
-        "on-headers": "~1.1.0",
-        "parseurl": "~1.3.3",
-        "safe-buffer": "5.2.1",
-        "uid-safe": "~2.1.5"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/express-session/node_modules/cookie": {
-      "version": "0.7.2",
-      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
-      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/express-session/node_modules/cookie-signature": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz",
-      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
-      "license": "MIT"
-    },
     "node_modules/fill-range": {
       "version": "7.1.1",
       "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
@@ -876,15 +841,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/on-headers": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz",
-      "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/parseurl": {
       "version": "1.3.3",
       "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
@@ -948,15 +904,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/random-bytes": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/random-bytes/-/random-bytes-1.0.0.tgz",
-      "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
@@ -1245,18 +1192,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/uid-safe": {
-      "version": "2.1.5",
-      "resolved": "https://registry.npmmirror.com/uid-safe/-/uid-safe-2.1.5.tgz",
-      "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
-      "license": "MIT",
-      "dependencies": {
-        "random-bytes": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/undefsafe": {
       "version": "2.0.5",
       "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz",

+ 0 - 1
package.json

@@ -17,7 +17,6 @@
   "dependencies": {
     "cors": "^2.8.5",
     "express": "^4.18.2",
-    "express-session": "^1.18.2",
     "marked": "^11.1.0"
   },
   "devDependencies": {

+ 1 - 1
public/js/reader.js

@@ -1154,7 +1154,7 @@ function setEditorCursorToLine(lineNumber) {
     // 计算目标位置的字符索引
     let charPosition = 0;
     for (let i = 0; i < lineNumber && i < lines.length; i++) {
-        charPosition += lines[i].length + 1; // +1 for newline
+        charPosition += lines[i].length + 1; // +1 表示换行符
     }
 
     // 设置光标位置

+ 34 - 32
server.js

@@ -2,7 +2,7 @@ const express = require('express');
 const path = require('path');
 const fs = require('fs').promises;
 const cors = require('cors');
-const session = require('express-session');
+const SECERT_TOKEN = 'cjy123';
 
 const app = express();
 const PORT = 3000;
@@ -10,6 +10,16 @@ const PORT = 3000;
 // 硬编码的密码
 const PASSWORD = 'cjy@0526';
 
+// 令牌 Cookie 配置
+const TOKEN_COOKIE_NAME = 'cjydocs.token';
+const TOKEN_COOKIE_OPTIONS = {
+  maxAge: 7 * 24 * 60 * 60 * 1000,
+  httpOnly: true,
+  secure: false,
+  sameSite: 'lax',
+  path: '/'
+};
+
 // 目录常量
 const DIRS = {
   public: path.join(__dirname, 'public'),
@@ -43,8 +53,26 @@ function validatePath(fullPath, baseDir) {
 }
 
 // 检查是否已认证
+function parseCookies(cookieHeader) {
+  if (!cookieHeader || typeof cookieHeader !== 'string') {
+    return {};
+  }
+
+  return cookieHeader.split(';').reduce((acc, part) => {
+    const [rawKey, ...rawValue] = part.trim().split('=');
+    if (!rawKey) return acc;
+    acc[decodeURIComponent(rawKey)] = decodeURIComponent(rawValue.join('='));
+    return acc;
+  }, {});
+}
+
+function getCookie(req, name) {
+  const cookies = parseCookies(req.headers.cookie);
+  return cookies[name];
+}
+
 function isAuthenticated(req) {
-    return req.session && req.session.isAuthenticated === true;
+    return getCookie(req, TOKEN_COOKIE_NAME) === SECERT_TOKEN;
 }
 
 // 统一的错误响应助手
@@ -85,21 +113,6 @@ app.use(cors({
 app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB
 app.use(express.urlencoded({ limit: '10mb', extended: true }));
 
-// Session 中间件
-app.use(session({
-  secret: 'cjydocs-secret-key-2024', // 会话密钥
-  resave: false,
-  saveUninitialized: false,
-  cookie: {
-    maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
-    httpOnly: true,
-    secure: false, // 如果使用 HTTPS,设置为 true
-    sameSite: 'lax', // 允许跨站点请求携带 cookie
-    path: '/' // Cookie 路径
-  },
-  name: 'cjydocs.sid', // Session cookie 名称
-  rolling: true // 每次请求时重置过期时间
-}));
 
 // 登录验证中间件
 function requireAuth(req, res, next) {
@@ -162,16 +175,9 @@ app.post('/api/login', (req, res) => {
   }
 
   if (password === PASSWORD) {
-    req.session.isAuthenticated = true;
+    res.cookie(TOKEN_COOKIE_NAME, SECERT_TOKEN, TOKEN_COOKIE_OPTIONS);
 
-    // 保存 session 后再响应
-    req.session.save((err) => {
-      if (err) {
-        console.error('Session保存失败:', err);
-        return ErrorResponse.serverError(res, '登录失败,请重试');
-      }
-      res.json({ success: true, message: '登录成功' });
-    });
+    res.json({ success: true, message: '登录成功' });
   } else {
     return ErrorResponse.unauthorized(res, '密码错误');
   }
@@ -179,12 +185,8 @@ app.post('/api/login', (req, res) => {
 
 // API: 登出
 app.post('/api/logout', (req, res) => {
-  req.session.destroy((err) => {
-    if (err) {
-      return ErrorResponse.serverError(res, '登出失败');
-    }
-    res.json({ success: true, message: '登出成功' });
-  });
+  res.clearCookie(TOKEN_COOKIE_NAME, { path: '/' });
+  res.json({ success: true, message: '登出成功' });
 });
 
 // API: 检查登录状态