本地登录正常,公网却掉线?Express Session 经 cpolar HTTPS 访问的 Cookie 排障实战

本地登录正常,公网登录返回 200,刷新却变成 401?别急着换认证方案,也别删除 secure: true。先查三件事:服务器发没发 Cookie,浏览器收没收,下次请求带没带。

这篇只用虚构测试账号。cpolar 提供本地 3000 端口的公网 HTTPS 入口,Express 管协议识别、会话保存与撤销。页面和 API 同源,不引入 CORS、iframe 或第三方登录。

图1|原理示意:浏览器HTTPS经cpolar到本地HTTP;登录200不等于Cookie已保存,非实测截图。

两段连接各有协议判断。

1.1 只测同源页面,不混进跨站登录

链路可以写成下面这样:

text 复制代码
浏览器 -- HTTPS --> cpolar 公网入口
                     |
                  隧道连接
                     |
                 本机 cpolar
                     |
                  HTTP 请求
                     v
              127.0.0.1:3000

1.2 分清两个"安全"判断

浏览器依据自己访问的 URL 与 Cookie 属性决定是否收发。Express 则依据本地连接、受信代理传来的协议信息计算 req.protocolreq.secure

下面是本次故障组的关键配置,不是推荐的上线配置:

javascript 复制代码
app.set('trust proxy', false);
// 会话配置里仍然使用:
const cookie = { secure: true };

当本地连接是 HTTP 且代理不受信时,req.securefalse。此时 express-session 不会把 Secure 会话 Cookie 写入响应;登录逻辑自身返回 200,并不能证明浏览器已经获得登录凭据。

1.3 先约定实验边界

代码在 Node.js v26.8.1、Express 5.1.0、express-session 1.18.2 上通过本地 HTTP 自动化测试,覆盖 Cookie 属性、代理信任、CSRF、轮换、退出重放与过期。公网隧道和双设备部分是现场验收步骤,不冒充已完成的公网测量。协议头覆盖行为必须按第 5 节核查,通过后才提交密码。

2 准备环境:只建一个临时测试应用

2.1 建目录、固定依赖版本

以下使用 macOS/Linux Bash,Windows 可用 WSL。固定实验依赖,保留锁文件;复现时使用 npm ci

bash 复制代码
node --version
npm --version
curl --version
python3 --version
bash 复制代码
mkdir express-cookie-lab
cd express-cookie-lab
npm init -y
npm install --save-exact express@5.1.0 express-session@1.18.2
mkdir public
bash 复制代码
npm ls express express-session
bash 复制代码
npm ci

2.2 生成仅供本次实验使用的凭据

在终端 A 设置随机会话签名密钥和测试密码。密码不是你的工作账号密码,也不要把现有项目的密钥复制进来。

bash 复制代码
export SESSION_SECRET="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))")"
export DEMO_PASSWORD="$(node -e "process.stdout.write(require('node:crypto').randomBytes(24).toString('hex'))")"
export TTL_MS=300000
bash 复制代码
node -e "console.log({secretLength:process.env.SESSION_SECRET.length,passwordLength:process.env.DEMO_PASSWORD.length})"
bash 复制代码
umask 077
node -e "require('node:fs').writeFileSync('login.json',JSON.stringify({username:'demo',password:process.env.DEMO_PASSWORD}))"

不要创建真实用户表。这个应用没有注册、找回密码和生产级登录限流,只适合授权人员短时测试;公网入口不是长期服务地址。

3 写好应用:登录、查看、修改和退出都要能验

3.1 保存完整服务端代码

把下面内容保存为 server.cjs.cjs 明确使用 CommonJS,避免项目默认模块类型造成导入错误。应用只监听回环地址,公网只走本机隧道。

javascript 复制代码
const express = require('express');
const session = require('express-session');
const crypto = require('node:crypto');
const app = express();
const mode = process.env.MODE || 'local';
if (!['local', 'broken', 'secure'].includes(mode)) throw Error('bad MODE');
const origin = new URL(process.env.PUBLIC_ORIGIN || 'http://127.0.0.1:3000').origin;
const secret = process.env.SESSION_SECRET;
const password = process.env.DEMO_PASSWORD;
if (!secret || secret.length < 64 || !password || password.length < 20) {
  throw Error('set strong SESSION_SECRET and DEMO_PASSWORD');
}
const ttl = Number(process.env.TTL_MS || 300000);
if (!Number.isInteger(ttl) || ttl < 1000) throw Error('bad TTL_MS');
if (mode !== 'local' && !origin.startsWith('https://')) throw Error('HTTPS origin required');
app.disable('x-powered-by');
if (mode === 'secure') {
  app.set('trust proxy', ip => ip === '127.0.0.1' || ip === '::ffff:127.0.0.1');
}
app.use((req, res, next) => {
  res.set('Cache-Control', 'no-store');
  res.set('X-Content-Type-Options', 'nosniff');
  res.set('Referrer-Policy', 'no-referrer');
  next();
});
app.get('/health', (req, res) => res.json({ ok: true }));
app.get('/diag', (req, res) => {
  res.json({
    remoteAddress: req.socket.remoteAddress,
    encrypted: Boolean(req.socket.encrypted),
    forwardedProto: req.get('x-forwarded-proto') || null,
    protocol: req.protocol,
    secure: req.secure
  });
});
app.use(express.json({ limit: '2kb' }));
const cookieOptions = { path: '/', httpOnly: true, sameSite: 'lax', secure: mode !== 'local' };
app.use(session({
  name: 'lab.sid', secret, resave: false, saveUninitialized: false,
  cookie: { ...cookieOptions, maxAge: ttl }
}));
function equal(a, b) {
  if (typeof a !== 'string' || typeof b !== 'string') return false;
  const x = Buffer.from(a), y = Buffer.from(b);
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}
function sameOrigin(req, res, next) {
  if (req.get('origin') !== origin) return res.status(403).json({ error: 'origin' });
  next();
}
function auth(req, res, next) {
  if (!req.session.user) return res.status(401).json({ error: 'login_required' });
  if (Date.now() >= req.session.deadline) {
    return req.session.destroy(err => {
      if (err) return next(err);
      res.clearCookie('lab.sid', cookieOptions);
      res.status(401).json({ error: 'expired' });
    });
  }
  next();
}
function csrf(req, res, next) {
  if (!equal(req.get('x-csrf-token'), req.session.csrf)) {
    return res.status(403).json({ error: 'csrf' });
  }
  next();
}
app.post('/login', sameOrigin, (req, res, next) => {
  if (req.body?.username !== 'demo' || !equal(req.body?.password, password)) {
    return res.status(401).json({ error: 'credentials' });
  }
  req.session.regenerate(err => {
    if (err) return next(err);
    req.session.user = 'demo';
    req.session.csrf = crypto.randomBytes(32).toString('hex');
    req.session.deadline = Date.now() + ttl;
    req.session.save(err => {
      if (err) return next(err);
      res.json({ user: 'demo', csrf: req.session.csrf });
    });
  });
});
app.get('/me', auth, (req, res) => res.json({
  user: req.session.user, deadline: req.session.deadline
}));
app.get('/csrf', auth, (req, res) => res.json({ csrf: req.session.csrf }));
app.post('/change', sameOrigin, auth, csrf, (req, res, next) => {
  req.session.changes = (req.session.changes || 0) + 1;
  req.session.save(err => {
    if (err) return next(err);
    res.json({ changes: req.session.changes });
  });
});
app.post('/logout', sameOrigin, auth, csrf, (req, res, next) => {
  req.session.destroy(err => {
    if (err) return next(err);
    res.clearCookie('lab.sid', cookieOptions);
    res.sendStatus(204);
  });
});
app.use(express.static('public', { etag: false, maxAge: 0 }));
app.use((err, req, res, next) => {
  res.status(err.status === 400 ? 400 : 500).json({ error: 'request_failed' });
});
app.listen(3000, '127.0.0.1', () => console.log(`lab ready: ${mode}`));

3.2 解释几个不能省的动作

local 只供本机 HTTP 基线;broken 开启 Secure 却不信任代理;secure 限定回环来源。session 不设置 proxy: true,让它沿用 Express 的信任规则。

退出不是删一个前端变量:先 destroy() 删除服务端会话,成功后再 clearCookie()。清除时保留同样的名称、路径与属性,不把原来的 maxAge 传给清除操作。

3.3 准备同源测试页面

把下面文件保存为 public/index.html。页面不读取 HttpOnly Cookie;它只保存 CSRF token,并让浏览器按同源规则携带 Cookie。页面刷新后可点"恢复状态"重新获取当前会话的 CSRF token。

html 复制代码
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Session Cookie 实验</title>
<h1>Session Cookie 实验</h1>
<p>仅使用本次生成的测试密码。</p>
<input id="password" type="password" autocomplete="off" placeholder="测试密码">
<button id="login">登录</button>
<button id="me">恢复状态</button>
<button id="change">修改计数</button>
<button id="logout">退出</button>
<pre id="out"></pre>
<script>
let csrf = '';
const out = document.querySelector('#out');
async function call(path, method = 'GET', body) {
  const response = await fetch(path, {
    method,
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      ...(csrf ? { 'X-CSRF-Token': csrf } : {})
    },
    body: body ? JSON.stringify(body) : undefined
  });
  const data = response.status === 204 ? {} : await response.json();
  if (data.csrf) csrf = data.csrf;
  const { csrf: hidden, ...visible } = data;
  out.textContent = JSON.stringify({ status: response.status, ...visible }, null, 2);
  return response;
}
document.querySelector('#login').onclick = async () => {
  const input = document.querySelector('#password');
  await call('/login', 'POST', { username: 'demo', password: input.value });
  input.value = '';
};
document.querySelector('#me').onclick = async () => {
  const response = await call('/csrf');
  if (response.ok) await call('/me');
};
document.querySelector('#change').onclick = () => call('/change', 'POST');
document.querySelector('#logout').onclick = async () => {
  const response = await call('/logout', 'POST');
  if (response.ok) csrf = '';
};
</script>
</html>

4 跑通本地基线:先证明业务逻辑没有坏

4.1 本机启动,不开隧道

在终端 A 启动本地组,保持进程前台运行。后续切换组别时,只对这个进程按 Ctrl+C,再输入新的启动命令,不去清理系统中其他 Node 服务。

bash 复制代码
MODE=local PUBLIC_ORIGIN=http://127.0.0.1:3000 node server.cjs
bash 复制代码
umask 077
export BASE=http://127.0.0.1:3000
curl --fail-with-body -sS "$BASE/health"
json 复制代码
{"ok":true}

再确认匿名用户没有登录。这里只读状态码,不输出整套 HTTP 头,减少凭据被带进截图的机会。

bash 复制代码
curl -sS -o /dev/null -w '%{http_code}\n' "$BASE/me"

终端 B 进入同一目录执行 curl;匿名 /me 应为 401,带 Cookie 后应为 200。

bash 复制代码
curl --fail-with-body -sS -c local.cookies   -H "Origin: $BASE" -H 'Content-Type: application/json'   --data-binary @login.json "$BASE/login" -o local-login.json

携带 Cookie 访问身份接口:

bash 复制代码
curl --fail-with-body -sS -b local.cookies "$BASE/me"

图2|本地基线预期验收示意:登录后恢复状态应返回200与demo;仅展示Cookie属性,不展示凭据。

这里看第二次请求能否认出用户。

5 开启 cpolar:先测协议头,再提交密码

5.1 获取真实 HTTPS 地址

cpolar 官网下载页 按系统安装客户端,并完成自己的账号认证。本文不替换现有配置文件,不重启已经运行的后台隧道,也不要求在文章里粘贴 authtoken。

bash 复制代码
cpolar version

在独立终端 C 启动临时 HTTP 隧道,映射的仍是本地 HTTP 3000 端口;命令中的 http 不是要求浏览器使用明文访问。

bash 复制代码
cpolar http 3000

保留前台进程。从这次运行展示的转发地址里复制 HTTPS 地址,不要套用别人的域名,也不要拿 HTTP 地址代替。若已有实例限制提示,先核对现有运行方式,不要反复新开客户端。

bash 复制代码
printf '粘贴本次 HTTPS 地址:'
read -r BASE
export BASE
BASE="$(python3 - <<'PY'
import os
from urllib.parse import urlsplit
u = urlsplit(os.environ['BASE'].strip())
assert u.scheme == 'https' and u.hostname
assert not u.username and not u.password
assert u.path in ('', '/') and not u.query and not u.fragment
print('https://' + u.netloc)
PY
)"
export BASE
printf '%s\n' "$BASE"
bash 复制代码
curl --fail-with-body -sS "$BASE/health"
curl --fail-with-body -sS "$BASE/diag"

5.2 看四个字段,不猜代理层数

当前还是 local 组,因此没有开启代理信任。记录 remoteAddressencryptedforwardedProtosecure:本地回环直连应符合代码绑定方式,HTTP 回源的 encryptedfalse

公网 HTTPS 验收要求应用收到的 forwardedProto 为单值 https。如果为空、值不符,或来源并非此文约定的回环连接,停下来核查实际拓扑,不要加 trust proxy: true 把异常压下去。

还要检查公网用户能否污染该头。下面两次请求不携带任何凭据,只比较诊断响应:

bash 复制代码
curl --fail-with-body -sS -H 'X-Forwarded-Proto: http' "$BASE/diag"
curl --fail-with-body -sS -H 'X-Forwarded-Proto: https, http' "$BASE/diag"

两次结果里的 forwardedProto 都应保持由 HTTPS 入口确定的单值 https。如果用户输入穿透到应用,或被追加成多值,就不能把这个头直接当安全依据;应先修正受控代理的覆盖规则,再继续本实验。这里不提供一条"把所有请求强行改成 https"的快捷修复。

图3|隧道与诊断原理示意:四个字段须按现场响应验收;公网隧道尚未实测。

公网 HTTPS 与 HTTP 回源并不矛盾。

6 复现故障,再只修代理信任这一项

停止终端 A 的应用,在该终端交互输入同一个 HTTPS 地址,去掉末尾斜杠后启动故障组。这里的 origin 必须与终端 B 的 BASE 一致。

bash 复制代码
printf '粘贴同一个 HTTPS 地址,不带末尾斜杠:'
read -r PUBLIC_ORIGIN
export PUBLIC_ORIGIN
MODE=broken node server.cjs

终端 B 提交登录,响应头仅保存在本地文件,不直接打印。这个阶段清空旧 jar 的办法是换一个新文件名,而不是删除浏览器所有站点的数据。

bash 复制代码
curl -sS -D broken.headers -c broken.cookies   -H "Origin: $BASE" -H 'Content-Type: application/json'   --data-binary @login.json "$BASE/login" -o broken-login.json
bash 复制代码
python3 - <<'PY'
from pathlib import Path
lines = Path('broken.headers').read_text().splitlines()
print('session_cookie_headers =', sum(
    x.lower().startswith('set-cookie: lab.sid=') for x in lines
))
PY

在本节约定的 HTTP 回源、不信任代理条件下,计数为 0。继续请求 /me 应为 401:

bash 复制代码
curl -sS -b broken.cookies -o /dev/null   -w '%{http_code}\n' "$BASE/me"

6.2 修复组:保留 Secure,限定信任来源

只有第 5 节的来源与转发头验收通过,才进入这一组。在终端 A 停止故障组,使用原密钥、原 origin 启动:

bash 复制代码
MODE=secure node server.cjs
bash 复制代码
curl --fail-with-body -sS "$BASE/diag"
bash 复制代码
curl --fail-with-body -sS -D secure.headers -c secure.cookies   -H "Origin: $BASE" -H 'Content-Type: application/json'   --data-binary @login.json "$BASE/login" -o secure-login.json

此时 /diag 应显示 protocol: httpssecure: trueencrypted: false。下面只列 Cookie 属性,隐藏值;不设置 Domain,Cookie 仅属于当前主机。

bash 复制代码
python3 - <<'PY'
from pathlib import Path
items = [x for x in Path('secure.headers').read_text().splitlines()
         if x.lower().startswith('set-cookie: lab.sid=')]
assert len(items) == 1
attrs = [x.strip() for x in items[0].split(';')[1:]]
low = [x.lower() for x in attrs]
assert 'secure' in low and 'httponly' in low
assert 'samesite=lax' in low and 'path=/' in low
assert not any(x.startswith('domain=') for x in low)
print('lab.sid=REDACTED; ' + '; '.join(attrs))
PY
bash 复制代码
curl --fail-with-body -sS -b secure.cookies "$BASE/me"

应返回 demo。别通过关闭 Secure 获得这个结果,否则你只是改变了题目,没有修好代理协议识别。

图4|故障与修复预期对照:仅在来源及转发头核查通过后信任代理,保留Secure;非公网实测截图。

比较前后 Cookie 是否发出。

7 验收安全边界:能登录还不够

7.1 缺少 CSRF token 的写请求必须失败

提取 token 到变量,别输出它。SameSite 与 HttpOnly 都不能替代 CSRF 校验。

bash 复制代码
CSRF="$(python3 -c 'import json; print(json.load(open("secure-login.json"))["csrf"])')"

不带 token 发起修改,即使已经携带 Cookie,也应该返回 403。

bash 复制代码
curl -sS -b secure.cookies -X POST -H "Origin: $BASE"   -o /dev/null -w '%{http_code}\n' "$BASE/change"

补上本会话的 token 再请求。这个接口只修改测试计数,没有真实业务副作用。

bash 复制代码
curl --fail-with-body -sS -b secure.cookies -c secure.cookies   -X POST -H "Origin: $BASE" -H "X-CSRF-Token: $CSRF"   "$BASE/change"

备份 jar 后重新登录,regenerate() 应使旧 jar 返回 401,新 jar 返回 200。

bash 复制代码
cp secure.cookies before-login.cookies
curl --fail-with-body -sS -b secure.cookies -c secure.cookies   -H "Origin: $BASE" -H 'Content-Type: application/json'   --data-binary @login.json "$BASE/login" -o secure-login.json
bash 复制代码
curl -sS -b before-login.cookies -o /dev/null -w '%{http_code}\n' "$BASE/me"
curl -sS -b secure.cookies -o /dev/null -w '%{http_code}\n' "$BASE/me"

7.3 退出后重放:证明服务端真的撤销了会话

轮换后重新提取 token。退出应为 204,拿退出前的 jar 重放应为 401;只删浏览器 Cookie 不算撤销。

bash 复制代码
CSRF="$(python3 -c 'import json; print(json.load(open("secure-login.json"))["csrf"])')"
cp secure.cookies before-logout.cookies
curl -sS -b secure.cookies -c secure.cookies -X POST   -H "Origin: $BASE" -H "X-CSRF-Token: $CSRF"   -o /dev/null -w '%{http_code}\n' "$BASE/logout"
bash 复制代码
curl -sS -b before-logout.cookies -o /dev/null   -w '%{http_code}\n' "$BASE/me"

终端 A 停止当前进程后启动五秒组。终端 B 重新登录,五秒内 /me 应为 200;下面强制发送旧 Cookie,验证服务端拒绝而非 jar 自动过期。

bash 复制代码
TTL_MS=5000 MODE=secure node server.cjs
bash 复制代码
curl --fail-with-body -sS -c expiry.cookies   -H "Origin: $BASE" -H 'Content-Type: application/json'   --data-binary @login.json "$BASE/login" -o expiry-login.json
curl -sS -b expiry.cookies -o /dev/null -w '%{http_code}\n' "$BASE/me"
bash 复制代码
python3 - <<'PY'
from pathlib import Path
for line in Path('expiry.cookies').read_text().splitlines():
    if not line or (line.startswith('#') and not line.startswith('#HttpOnly_')):
        continue
    fields = line.split('   ')
    if len(fields) == 7 and fields[5] == 'lab.sid':
        Path('replay.headers').write_text('Cookie: lab.sid=' + fields[6] + '\n')
        break
else:
    raise SystemExit('session cookie absent')
PY
sleep 6
curl -sS -H @replay.headers -o /dev/null -w '%{http_code}\n' "$BASE/me"

期望 401。服务端会话存储中的过期检查与显式 deadline 都属于服务器侧约束,浏览器是否主动移除 Cookie 不再影响本次判断。完成后恢复五分钟组,再进行人工浏览器测试。

8 两台设备再走一遍:区分客户端与服务端故障

8.1 桌面与手机各自登录

终端 A 停止短时组,恢复正常实验时限。用当前 HTTPS 地址打开页面,桌面与手机各自输入这次生成的测试密码;不要拷贝电脑的 Cookie 到手机。

bash 复制代码
TTL_MS=300000 MODE=secure node server.cjs

手机切到移动网络,密码仅从本机受限文件取用,不发公共群聊。每台设备按"登录 → 恢复状态 → 刷新 → 恢复状态 → 修改计数 → 退出 → 恢复状态"走一遍。最后一次恢复状态应为 401。一台设备退出后另一台仍保持登录,是本例每个浏览器会话独立的设计,不是退出失效。

桌面浏览器在开发者工具里查看 Network 中 /login 的响应头,再看 Application/Storage 对应站点的 Cookie 表。HttpOnly Cookie 不出现在 document.cookie 里,不能据此认定它没有保存。

图5|双设备预期验收示意:会话相互独立,退出及旧Cookie重放应返回401;双设备尚未实测。

这里只撤销当前设备会话,不是全设备退出。

8.2 按证据选择排错入口

观察结果 先检查什么 本文的处理边界
登录 200,响应没有会话 Set-Cookie req.secure、Secure 设置、session 是否被修改 修代理信任,不关闭公网 Secure
有 Set-Cookie,浏览器不保存 开发者工具阻止原因、Domain、SameSite、实际协议 当前同源 HTTPS,不设 Domain
已保存但请求不携带 请求的主机、路径、协议、过期时间、credentials 保持同一个 origin,使用同源 fetch
请求带 Cookie 仍然 401 会话是否撤销、过期、重启、签名密钥与存储 核对服务端状态,不反复换浏览器

8.3 域名变化后重新登录,不扩大 Domain

换地址后同时更新 BASEPUBLIC_ORIGIN 并重新登录。同一主机的不同端口不隔离 Cookie,但属于不同 origin;localhost127.0.0.1 则是不同主机。

host-only Cookie 只属于设置它的主机。换成另一个公网主机名后,浏览器不会沿用前一个主机的 Cookie;修改端口不是同一回事,别混淆主机和 origin。

9 收尾:关掉实验入口,再谈生产化

9.1 清理只属于这次实验的资料

在终端 C 对本次前台隧道按 Ctrl+C,在终端 A 对本次 Node 进程按 Ctrl+C。不要用按进程名批量杀死的命令,免得影响同机其他隧道或开发服务。

bash 复制代码
ls -l login.json *.cookies *-login.json *.headers
bash 复制代码
rm -f login.json local.cookies local-login.json   broken.headers broken.cookies broken-login.json   secure.headers secure.cookies secure-login.json   before-login.cookies before-logout.cookies   expiry.cookies expiry-login.json replay.headers
unset CSRF BASE
bash 复制代码
unset SESSION_SECRET DEMO_PASSWORD PUBLIC_ORIGIN TTL_MS

9.2 MemoryStore 到这里为止

MemoryStore 只供单进程实验,重启丢失会话。生产使用共享存储并核查 TTL、撤销与并发语义。代码显式检查绝对 deadline,不把 Cookie 有效期当成唯一约束。

别扩大 Domain 到共享后缀,也别把 Cookie 拼进 URL。容器与多层代理须重新核查信任来源。正式部署移除或限制 /diag,补齐限流、审计、账号管理和安全响应头。生产密钥应安全持久配置,不能每次启动随机重建。curl 不执行浏览器全部 Cookie 策略,双设备验证不能省略。

验收完成后,本地应用、cpolar HTTPS 入口、受限代理信任和浏览器 Cookie 连成一条可解释的链路。登录只是起点,服务端轮换、过期与撤销同样有独立证据。

  • 看不到 Cookie 时,先分清"没发出""没保存""没带回",不要直接关闭 Secure。
  • trust proxy 前,核对直连来源与转发头覆盖行为;回环绑定不等于自动拥有可信代理。
  • 登录成功后继续验收 CSRF、旧 Cookie 重放和过期,退出按钮不是服务端撤销的证据。

参考资料:express-session 官方文档Express 反向代理指南MDN Set-Cookiecpolar 文档

相关推荐
秋秋小事1 小时前
node prisma+postgreSQL数据库事务
node.js
badhope3 小时前
C++、Node.js、Python 写最小 HTTP 服务谁快?我实测了 6 万次请求
node.js
console.log('npc')5 小时前
03 — 核心框架:App 与中间件
后端·node.js·express
云浪5 小时前
从 0 手写一个 MCP Server:让 Copilot 调用 Tool 完成四则运算
javascript·node.js·mcp
FungLeo18 小时前
成为全栈·Node 后端篇·点赞系统:幂等点赞与计数原子增减
node.js·成为全栈·点赞系统·幂等点赞·计数原子增减
故作春风20 小时前
elpis-core 核心从入门到理解
后端·架构·node.js
万敏20 小时前
Vue3 全栈实战第九周:Node.js + Express 后端从零搭建实战记录
vue.js·node.js·全栈
爱吃红星柚1 天前
【学习】Elpis 抽离与发布 npm 包
前端·前端框架·node.js
梦帮科技1 天前
Next.js 16 模块化单体实战:App Router、领域模块与 Route Handler 分层
css·数据结构·链表·正则表达式·node.js·json·html5