拔刀 發表於 2026-4-17 11:46:00

记一次通过宝塔和 Docker 部署 Next.js 项目时关闭 Basic Auth 的过程

<h3 id="背景">背景</h3>
<p>这次在一台已经安装宝塔面板的 Linux 服务器上部署一个 Next.js 项目。</p>
<p>项目本身是:</p>
<ul>
<li>
<p>Next.js 14</p>
</li>
<li>
<p>Docker Compose 部署</p>
</li>
<li>
<p>Nginx 反向代理</p>
</li>
<li>
<p><code>/admin</code>、<code>/api/content</code>、<code>/api/upload</code>、<code>/api/upload-logo</code>这些路径通过<code>middleware</code>做了 HTTP Basic Auth 保护</p>
</li>
</ul>
<p>部署完成后,前台页面可以打开,但是访问后台时浏览器会弹出原生登录框,并提示“您与此网站的连接不是私密连接”。</p>
<p>这次排查的目标很明确:<strong>先把项目正常部署起来,然后关闭 Basic Auth,避免浏览器再弹原生认证框。</strong></p>
<h3 id="一最开始遇到的问题是什么">一、最开始遇到的问题是什么</h3>
<p>当时访问的是:</p>
<pre><code class="language-text">http://&lt;IP&gt;:8080/admin
</code></pre>
<p>浏览器会弹出 Basic Auth 登录框,并提示连接不私密。</p>
<p>这里要先分清楚两件事:</p>
<ol>
<li>
<p>这不是程序报错</p>
</li>
<li>
<p>这是浏览器对“HTTP 明文连接下输入账号密码”的安全提醒</p>
</li>
</ol>
<p>只要继续使用:</p>
<pre><code class="language-text">HTTP + Basic Auth
</code></pre>
<p>这个提示就很难消除。</p>
<h3 id="二项目原来的鉴权是怎么做的">二、项目原来的鉴权是怎么做的</h3>
<p>项目里用的是 Next.js <code>middleware</code>,大致逻辑如下:</p>
<pre><code class="language-ts">import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'

const authRealm = 'Portfolio Admin'

const unauthorizedResponse = () =&gt;
new NextResponse('Authentication required', {
    status: 401,
    headers: {
      'WWW-Authenticate': `Basic realm="${authRealm}", charset="UTF-8"`,
    },
})

export function middleware(request: NextRequest) {
const username = process.env.ADMIN_USERNAME
const password = process.env.ADMIN_PASSWORD

// 读取 authorization 头并校验
// 校验失败则返回 401
}

export const config = {
matcher: [
    '/admin',
    '/admin/:path*',
    '/api/content',
    '/api/content/:path*',
    '/api/upload',
    '/api/upload/:path*',
    '/api/upload-logo',
    '/api/upload-logo/:path*',
],
}
</code></pre>
<p>从这里可以看出:</p>
<ul>
<li>
<p><code>/admin</code>受 Basic Auth 保护</p>
</li>
<li>
<p>内容保存接口和上传接口也一起受保护</p>
</li>
</ul>
<p>所以只要这个中间件还生效,访问<code>/admin</code>就一定会弹浏览器原生认证框。</p>
<h3 id="三第一次尝试想通过环境变量关闭-basic-auth">三、第一次尝试:想通过环境变量关闭 Basic Auth</h3>
<p>一开始我的想法是,不直接删掉鉴权逻辑,而是加一个开关,例如:</p>
<pre><code class="language-ts">const authEnabled = process.env.ADMIN_AUTH_ENABLED !== 'false'

if (!authEnabled) {
return NextResponse.next()
}
</code></pre>
<p>然后在生产环境变量里加:</p>
<pre><code class="language-text">ADMIN_AUTH_ENABLED=false
</code></pre>
<p>这个思路本身没有问题,优点也很明显:</p>
<ul>
<li>
<p>改动小</p>
</li>
<li>
<p>可回滚</p>
</li>
<li>
<p>后续想重新开启鉴权时,只需要改环境变量</p>
</li>
</ul>
<p>但是实际上,容器重建后访问<code>/admin</code>仍然返回<code>401</code>,说明 Basic Auth 依旧在生效。</p>
<h3 id="四为什么环境变量方案没有达到预期">四、为什么环境变量方案没有达到预期</h3>
<p>这一步最麻烦的地方在于:<strong>源码看起来已经改对了,环境变量也已经写进<code>.env.production</code>了,但运行结果不对。</strong></p>
<p>当时做了几轮确认:</p>
<ol>
<li>
<p>确认服务器源码目录里的<code>src/middleware.ts</code>已经包含<code>ADMIN_AUTH_ENABLED</code></p>
</li>
<li>
<p>确认<code>.env.production</code>已经有:</p>
<pre><code class="language-text">ADMIN_AUTH_ENABLED=false
</code></pre>
</li>
<li>
<p>确认容器确实已经重建</p>
</li>
<li>
<p>确认容器内<code>ADMIN_USERNAME</code>和<code>ADMIN_PASSWORD</code>仍然存在</p>
</li>
<li>
<p>继续检查<code>.next</code>里的编译产物,确认中间件打包后的代码里也确实出现了<code>ADMIN_AUTH_ENABLED</code></p>
</li>
</ol>
<p>到这里说明一个问题:<strong>这不是“代码没同步上去”,也不是“环境变量没传进去”,而是这个关闭开关在实际构建和运行链路里没有按预期生效。</strong></p>
<h3 id="五第二次尝试直接移除-basic-auth-逻辑">五、第二次尝试:直接移除 Basic Auth 逻辑</h3>
<p>因为这次的目标不是“做一个长期可配置的后台认证方案”,而是“先把浏览器原生 Basic Auth 登录框关掉”,所以后面改成了更直接的处理方式:</p>
<p>把<code>middleware.ts</code>改成无条件放行。</p>
<p>最终改成这样:</p>
<pre><code class="language-ts">import { NextResponse } from 'next/server'

export function middleware() {
return NextResponse.next()
}

export const config = {
matcher: [
    '/admin',
    '/admin/:path*',
    '/api/content',
    '/api/content/:path*',
    '/api/upload',
    '/api/upload/:path*',
    '/api/upload-logo',
    '/api/upload-logo/:path*',
],
}
</code></pre>
<p>这里有一个细节:</p>
<ul>
<li>
<p><code>matcher</code>还保留着</p>
</li>
<li>
<p>只是这些路径命中后不再做认证,而是直接放行</p>
</li>
</ul>
<p>这样做的好处是:</p>
<ul>
<li>
<p>不影响中间件匹配范围</p>
</li>
<li>
<p>不需要额外改页面或接口代码</p>
</li>
<li>
<p>行为足够直接,结果容易验证</p>
</li>
</ul>
<p>最后重新构建镜像并启动容器后正常。</p>
<h3 id="六最终结论">六、最终结论</h3>
<p>一句话总结:</p>
<blockquote>
<p>浏览器里的 Basic Auth 登录框,本质上是服务端返回了<code>401 + WWW-Authenticate</code>;只要中间件不再返回这个响应,弹框自然就消失了。</p>
</blockquote>
<p>当然,这只是这次的临时方案,如果要保证后台安全则需要留下认证。</p><br><br>
来源:https://www.cnblogs.com/Higurashi-kagome/p/19882396
頁: [1]
查看完整版本: 记一次通过宝塔和 Docker 部署 Next.js 项目时关闭 Basic Auth 的过程