Dimitri's Journal工作日记 & 文档

严格服务端密码保护

Supabase、HttpOnly Cookie、Debian 13和PM2部署配置

保护机制

受保护页面使用以下流程:

  1. 浏览器只向 POST /api/auth/login 提交密码.
  2. Next.js服务端计算SHA-256并查询Supabase的 doc_passwords.
  3. 验证成功后, 服务端写入HMAC签名的HttpOnly Cookie.
  4. 文档Server Component读取并验证Cookie.
  5. 未认证时不渲染MDX正文.
  6. Markdown接口拒绝未认证请求.
  7. 公共搜索索引排除 protected: true 页面.

浏览器端不保存密码、Supabase密钥或认证token. Cookie有效期为24小时.

Supabase配置

在Supabase SQL Editor中执行项目根目录的 supabase-migration.sql.

Supabase要求:

  • doc_passwords 启用RLS.
  • 不为 anonauthenticated 创建访问策略.
  • Next.js仅使用 service_role 查询密码表.
  • service_role 密钥不能添加 NEXT_PUBLIC_ 前缀.

生成密码SHA-256:

node -e "console.log(require('crypto').createHash('sha256').update('替换为密码').digest('hex'))"

将生成结果写入全局密码:

INSERT INTO public.doc_passwords (slug, password_hash, label)
VALUES (NULL, '<SHA-256>', '全局密码');

如已有全局记录, 使用 UPDATE:

UPDATE public.doc_passwords
SET password_hash = '<SHA-256>', updated_at = NOW()
WHERE slug IS NULL;

Debian 13环境变量

在服务器 /opt/myself-docs/.env.local 中配置:

SUPABASE_URL=https://<project-id>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
AUTH_SECRET=<至少32字符的随机密钥>

生成 AUTH_SECRET:

openssl rand -hex 32

限制文件权限:

cd /opt/myself-docs
chmod 600 .env.local

不要再配置以下变量:

NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
NEXT_PUBLIC_DOCS_PASSWORD=

构建与PM2重启

环境变量改变后必须清理旧构建产物:

cd /opt/myself-docs
rm -rf .next
pnpm install --frozen-lockfile
pnpm test
pnpm types:check
pnpm build
pm2 restart myself-docs --update-env
pm2 save

PM2服务应使用Next.js服务端模式, 不能使用静态导出:

pm2 start pnpm --name myself-docs -- start

Nginx要求

Nginx必须反向代理到Next.js服务, 不能直接发布旧的静态HTML:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

如果配置了 proxy_cache, 必须对受保护页面和携带认证Cookie的请求禁用缓存. 当前Next.js受保护响应和Markdown响应使用动态渲染或 private, no-store.

验收

使用无痕窗口检查:

  • 访问受保护页面时只返回密码表单.
  • 页面HTML和RSC响应中搜索不到正文中的独特字符串.
  • 未登录访问对应 /llms.mdx/docs/.../content.md 返回401.
  • /api/search 结果中没有受保护页面.
  • 登录响应包含 Set-Cookie: docs_session=...; HttpOnly; Secure.
  • 浏览器JavaScript不能通过 document.cookie 读取 docs_session.
  • 修改Cookie后页面重新要求密码.
  • POST /api/auth/logout 后不能继续访问.

额外边界

网站保护无法覆盖公开Git仓库. 如果受保护MDX提交到公开仓库, 用户仍可直接从GitHub读取. 严格保护要求仓库为私有仓库, 或将私密内容移出公开仓库.

On this page