diff --git a/.dockerignore b/.dockerignore index 138f8bb..fc0263c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -59,9 +59,11 @@ docs *.md !VERSION.txt -# 脚本(保留 docker-entrypoint.sh) -scripts +# 脚本(只保留生产构建和容器启动必需入口) +scripts/* !scripts/docker-entrypoint.sh +!scripts/build-obfuscated.js +!scripts/obfuscate-config.js # 临时文件 uploads diff --git a/Dockerfile b/Dockerfile index d8aba9e..e8b0b62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -158,7 +158,7 @@ RUN if [ ! -d "node_modules/.prisma" ]; then \ fi # 构建 Next.js 应用(standalone 模式) - # 商业交付镜像必须在构建阶段暴露失败,不能吞掉构建错误继续出包。 + # 商业交付镜像必须完成服务端混淆;混淆失败不能继续出包。 RUN set -ex && \ if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \ export PRISMA_QUERY_ENGINE_LIBRARY="${PRISMA_ENGINE_CACHE_PATH}" && \ @@ -172,7 +172,7 @@ RUN if [ ! -d "node_modules/.prisma" ]; then \ else \ unset PRISMA_SCHEMA_ENGINE_BINARY; \ fi && \ - npm run build + npm run build:prod # 验证构建产物(允许 ESLint 警告,但不允许致命错误) RUN if [ ! -f ".next/standalone/server.js" ]; then \ diff --git a/__tests__/production-build-hardening.test.ts b/__tests__/production-build-hardening.test.ts new file mode 100644 index 0000000..4adb5db --- /dev/null +++ b/__tests__/production-build-hardening.test.ts @@ -0,0 +1,37 @@ +import fs from 'fs' +import path from 'path' + +describe('production build hardening', () => { + const root = process.cwd() + const dockerfile = fs.readFileSync(path.join(root, 'Dockerfile'), 'utf8') + const dockerignore = fs.readFileSync(path.join(root, '.dockerignore'), 'utf8') + const buildScript = fs.readFileSync(path.join(root, 'scripts/build-obfuscated.js'), 'utf8') + + it('Docker 生产镜像必须走混淆构建', () => { + expect(dockerfile).toContain('npm run build:prod') + expect(dockerfile).not.toMatch(/\bnpm run build\s*$/m) + }) + + it('Docker 构建上下文必须包含混淆脚本和配置', () => { + expect(dockerignore).toContain('scripts/*') + expect(dockerignore).toContain('!scripts/build-obfuscated.js') + expect(dockerignore).toContain('!scripts/obfuscate-config.js') + expect(dockerignore).toContain('!scripts/docker-entrypoint.sh') + }) + + it('混淆依赖缺失时不能静默跳过', () => { + expect(buildScript).toContain('生产混淆不能跳过') + expect(buildScript).toContain('process.exit(1)') + expect(buildScript).not.toContain('跳过混淆步骤') + }) + + it('混淆配置保持保守,避免破坏 Next.js 服务端产物', () => { + const { obfuscatorOptions } = require('../scripts/obfuscate-config') + + expect(obfuscatorOptions.compact).toBe(true) + expect(obfuscatorOptions.stringArray).toBe(true) + expect(obfuscatorOptions.controlFlowFlattening).toBe(false) + expect(obfuscatorOptions.deadCodeInjection).toBe(false) + expect(obfuscatorOptions.renameGlobals).toBe(false) + }) +}) diff --git a/docs/商业化地基排查问题清单_20260524.md b/docs/商业化地基排查问题清单_20260524.md index 7347c3c..da9dd53 100644 --- a/docs/商业化地基排查问题清单_20260524.md +++ b/docs/商业化地基排查问题清单_20260524.md @@ -1238,6 +1238,8 @@ NAS 私有化系统可能在平板或窄屏浏览器里使用。模板编辑器 - `docker-compose.source-test.yml` 的 MinIO 和 MinIO Client 默认值已改成固定版本,源码测试入口不再默认使用 `latest`。 - `docs/deployment/部署说明.md` 已把 `APP_IMAGE` 必填、HTTP Registry 前置配置、版本化发布命令和完整 `APP_IMAGE` 回滚写成主线步骤。 - `Dockerfile` 已取消 `npm run build` 失败后继续出镜像的逻辑;构建失败会直接阻断镜像产出。 +- `Dockerfile` 已改为生产镜像执行 `npm run build:prod`,构建后会对服务端 chunks 和许可证 API 产物做混淆;混淆失败会直接阻断镜像产出,避免“脚本存在但客户镜像未启用”的防拆空档。 +- `scripts/build-obfuscated.js`、`scripts/obfuscate-config.js`、`.dockerignore`:混淆依赖缺失不再静默跳过;Docker 构建上下文只放行生产构建和容器启动必需脚本,确保混淆配置能进入构建阶段。 - `deploy-new-fixed.ps1`、`发布管理.ps1`、`license-server/deploy.ps1` 已加默认停止闸门;不追加明确确认参数时不会继续执行旧服务器、旧 Harbor、许可证部署或 Docker 清理逻辑。 - `flnxi-agent/README.md` 已把 Node 版 Agent 定位改成历史实现说明,不再把 Docker 运行命令写成新客户推荐安装方式。 diff --git a/scripts/README.md b/scripts/README.md index 0532039..aa5ef14 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -38,7 +38,7 @@ npm run verify:template-data - `build-material-image.ps1` 构建主应用镜像。它属于历史/内部维护备用入口,保留旧的测试/正式分层规则,但仍带 Harbor / `latest` 旧口径;客户正式交付、升级、回滚优先用根目录的 `deploy-registry.ps1`,不要把它当成客户升级主线。若确需用它推送旧 Harbor,必须显式追加 `-AcknowledgeLegacyHarborPush`。 - `build-obfuscated.js` - 生产构建后的混淆处理 + 生产构建后的服务端混淆处理。客户交付镜像通过 `Dockerfile` 执行 `npm run build:prod` 时会自动调用;混淆依赖或配置缺失会直接失败,不能静默跳过。 - `docker-entrypoint.sh` 主应用容器启动入口 - `generate-shared-test-env.ps1` diff --git a/scripts/build-obfuscated.js b/scripts/build-obfuscated.js index ff3f9ab..06e5958 100644 --- a/scripts/build-obfuscated.js +++ b/scripts/build-obfuscated.js @@ -20,9 +20,9 @@ let JavaScriptObfuscator; try { JavaScriptObfuscator = require('javascript-obfuscator'); } catch { - console.log('⚠️ javascript-obfuscator 未安装,跳过混淆步骤'); - console.log(' 如需启用混淆,请运行: npm install -D javascript-obfuscator'); - process.exit(0); + console.error('❌ javascript-obfuscator 未安装,生产混淆不能跳过'); + console.error(' 请先运行: npm install'); + process.exit(1); } const { obfuscatorOptions } = require('./obfuscate-config'); diff --git a/scripts/obfuscate-config.js b/scripts/obfuscate-config.js new file mode 100644 index 0000000..4e58fd8 --- /dev/null +++ b/scripts/obfuscate-config.js @@ -0,0 +1,27 @@ +/** + * Production obfuscation options. + * + * Keep this conservative: the goal is to raise reverse-engineering cost for + * server-side license and paid-feature code without making Next.js output + * brittle or dramatically increasing bundle size. + */ +const obfuscatorOptions = { + compact: true, + controlFlowFlattening: false, + deadCodeInjection: false, + debugProtection: false, + disableConsoleOutput: false, + identifierNamesGenerator: 'hexadecimal', + renameGlobals: false, + rotateStringArray: true, + selfDefending: false, + simplify: true, + splitStrings: false, + stringArray: true, + stringArrayEncoding: ['base64'], + stringArrayThreshold: 0.35, + transformObjectKeys: false, + unicodeEscapeSequence: false, +} + +module.exports = { obfuscatorOptions }