Skip to content

基于 Cloudflare Access 的零信任访问控制:从灰度发布到正式上线的完美方案

适用场景

  • 预发布环境 / 灰度验证环境 / 内部测试环境
  • 核心需求:需要对未完成的产品进行访问控制,仅允许团队成员访问,但不希望在应用代码中实现登录逻辑,也不希望 Nginx 基础认证与应用认证发生冲突。

在 SaaS 产品的开发过程中,我们常常面临一个两难困境:如何保护未完成的预览版产品,又不干扰正常的开发调试?

传统的 Nginx 基础认证(Basic Auth)虽然简单,但会与应用的 Authorization: Bearer Token 认证发生冲突。更麻烦的是,当产品正式上线时,我们往往需要修改 Nginx 配置、重启服务,甚至改代码才能移除这道“临时墙”。

本文介绍一种基于 Cloudflare Access 的零信任访问控制方案,它能在 DNS 解析之后、请求到达源站之前建立一道可随时开关的认证墙,实现从“灰度发布”到“正式上线”的无缝切换,无需任何代码或配置变更

1. 问题背景:传统方案的痛点

1.1 架构场景

假设我们有一个标准的 SaaS 项目架构:

子域名用途技术栈
dash.myapp.comSaaS 控制台(前端)Nuxt.js
api.myapp.com后端 API 服务Golang

1.2 传统方案的问题

问题一:Nginx 基础认证与应用认证冲突

  • Nginx 基础认证占用 Authorization: Basic 请求头
  • SaaS 应用认证需要 Authorization: Bearer 请求头
  • 两者冲突,导致应用无法正常登录

问题二:上线时需要修改配置

  • 需要手动编辑 Nginx 配置文件
  • 删除或注释 auth_basic 相关指令
  • 执行 nginx -s reload 重启服务
  • 有操作风险,且不够优雅

问题三:管理体验差

  • 团队成员增删需要手动修改 .htpasswd 文件
  • 无法按邮箱域(如 @company.com)批量授权
  • 没有审计日志,不知道谁在什么时候访问过

2. 解决方案:Cloudflare Access 零信任访问控制

2.1 什么是 Cloudflare Access?

Cloudflare Access 是 Cloudflare 零信任架构的核心组件。它可以在请求到达源站之前,在 Cloudflare 边缘节点上进行身份验证和访问控制。

工作原理

2.2 方案选择:Zero Trust Free 完全够用

很多人担心 Cloudflare Access 需要付费。实际上,Zero Trust Free 计划 提供了完整的 Access 功能:

功能Free 计划是否满足需求
Self-hosted 应用✅ 支持✅ 满足
邮箱域策略✅ 支持✅ 满足
Service Token✅ 支持✅ 满足
Seat 数量50 人免费✅ 团队够用
费用$0/月✅ 完全免费

激活步骤

  1. 进入 Cloudflare 控制台 → Zero Trust
  2. 选择 Zero Trust Free 计划
  3. 确认支付信息(不会扣费,仅用于风控)
  4. 激活成功,即可开始配置

2.3 与传统方案的本质区别

对比维度Nginx 基础认证Cloudflare Access
认证位置源站 Nginx 层Cloudflare 边缘节点
是否占用 Authorization✅ 是,导致冲突❌ 否,完全不修改请求头
上线切换方式修改 Nginx 配置并重启控制台一键关闭策略
认证方式用户名/密码支持 GitHub、Google、OTP、邮箱验证码
团队管理手动编辑 .htpasswdWeb 界面管理,支持邮箱域授权
审计日志❌ 无✅ 完整记录

2.4 为什么要统一管理两个子域名?

在灰度发布阶段,我们需要同时保护:

  • 控制台:防止外部用户访问未完成的页面
  • API:防止未授权的 API 调用

Cloudflare Access 允许我们为两个子域名分别创建策略,但使用统一的管理界面,实现:

  • 统一的身份认证源
  • 统一的团队权限管理
  • 统一的上线切换操作

3. 架构设计

3.1 整体架构图

3.2 核心设计原则

  1. 认证与业务解耦:访问控制完全在 Cloudflare 层完成,源站 Nginx 和应用无需感知
  2. 统一策略管理:两个子域名在同一个控制台管理,上线时可一键切换
  3. 零配置上线:正式上线时只需关闭 Access 策略,无需修改任何服务器配置

4. Nginx 配置(简化后)

4.1 dash.myapp.com 配置

nginx
# HTTP 重定向到 HTTPS
server {
    listen 80;
    server_name dash.myapp.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS 配置
server {
    listen 443 ssl http2;
    server_name dash.myapp.com;

    ssl_certificate /etc/nginx/ssl/myapp.com.pem;
    ssl_certificate_key /etc/nginx/ssl/myapp.com.key;

    # SSL 优化配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;

    # 日志
    access_log /var/log/nginx/dash.myapp.com-access.log;
    error_log /var/log/nginx/dash.myapp.com-error.log;

    # Cloudflare 真实 IP(与 Access 配合使用)
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 131.0.72.0/22;
    real_ip_header CF-Connecting-IP;

    location / {
        # 注意:没有任何 auth_basic 配置
        # 认证完全由 Cloudflare Access 处理
        proxy_pass http://127.0.0.1:3100;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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_set_header X-Auth-User-Email $http_cf_access_authenticated_user_email;
    }

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
        proxy_pass http://127.0.0.1:3100;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

4.2 api.myapp.com 配置

nginx
# HTTP 重定向到 HTTPS
server {
    listen 80;
    server_name api.myapp.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS 配置
server {
    listen 443 ssl http2;
    server_name api.myapp.com;

    ssl_certificate /etc/nginx/ssl/myapp.com.pem;
    ssl_certificate_key /etc/nginx/ssl/myapp.com.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;

    access_log /var/log/nginx/api.myapp.com-access.log;
    error_log /var/log/nginx/api.myapp.com-error.log;

    # Cloudflare 真实 IP
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 131.0.72.0/22;
    real_ip_header CF-Connecting-IP;

    location / {
        # 同样没有任何 auth_basic 配置
        proxy_pass http://127.0.0.1:2759;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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;

        # 传递 Service Token 认证的用户信息
        proxy_set_header X-Auth-User-Email $http_cf_access_authenticated_user_email;
    }

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
        proxy_pass http://127.0.0.1:2759;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # 健康检查免认证
    location = /health {
        proxy_pass http://127.0.0.1:2759;
        proxy_set_header Host $host;
    }
}

5. Cloudflare Access 配置实战

5.1 激活 Zero Trust Free 计划

在开始配置之前,首先需要激活 Cloudflare Zero Trust Free 计划:

  1. 登录 Cloudflare 控制台,点击左侧菜单的 Zero Trust
  2. 如果是首次使用,会看到计划选择页面
  3. 选择 Zero Trust Free 计划($0/seat/month,50 人免费)
  4. 确认支付信息(需要绑定信用卡或 PayPal,仅用于风控验证,不会扣费
  5. 点击 Activate 完成激活

激活成功后,即可开始配置 Access 应用。

5.2 为 dash.myapp.com 配置交互式登录保护

步骤一:创建 Self-hosted 应用

  1. 进入 Zero TrustAccessApplications
  2. 点击 Add an application → 选择 Self-hosted

步骤二:填写应用基本信息

字段填写内容说明
Application namedash-myapp应用名称,便于识别
Subdomaindash子域名部分
Domainmyapp.com主域名
Path留空(或填 *保护所有路径

填写完成后,点击 Add public hostname

步骤三:配置访问策略

  1. 向下滚动到 Access policies 区域
  2. 点击 Add a policy
  3. 填写策略信息:
字段说明
Policy nameAllow Team Members策略名称
ActionAllow允许符合条件的请求
SelectorEmails Ending In选择邮箱后缀匹配
Value@myapp.com你的团队邮箱域
  1. 点击 Save policy
  2. 点击页面底部的 Save 完成应用创建

验证配置:在浏览器无痕模式访问 https://dash.myapp.com,应该自动跳转到 Cloudflare Access 登录页面,输入 @myapp.com 邮箱后接收验证码即可登录。

5.3 为 api.myapp.com 配置 Service Token(服务间调用)

对于 API 子域名,我们需要支持两种访问方式:

  • 服务间调用:前端服务(Nuxt 后端)静默调用,无需人工交互
  • 团队成员访问:开发人员通过浏览器调试 API

这需要配置 Service Token两条访问策略

步骤一:创建 Service Token

  1. 在 Access 左侧菜单,点击 Service CredentialsService Tokens
  2. 点击 Create Service Token
  3. 填写信息:
字段填写内容说明
Namedash-apiserverToken 名称,建议与调用方关联
Duration2 years有效期,建议设长一些
  1. 点击 Generate token
  2. ⚠️ 关键步骤:立即保存生成的 Client IDClient Secret
  • Client ID 格式示例:888/104a908c1a61de52c89237/26bee0.access
  • Client Secret 是一长串随机字符:9eca11cf456625c4ac861f327eee7988...
  • Secret 只显示一次,关闭页面后无法再次查看!

步骤二:创建 API 应用

  1. 回到 AccessApplications
  2. 点击 Create new applicationSelf-hosted
  3. 填写应用信息:
字段填写内容
Application nameapi-myapp
Subdomainapi
Domainmyapp.com
Path留空(或填 *

点击 Add public hostname

步骤三:配置第一条策略(Service Token 放行)

  1. 点击 Add a policy
  2. 填写策略信息:
字段说明
Policy nameAllow Service Token策略名称
ActionService Auth⚠️ 注意:不是 Allow
SelectorService Token选择服务令牌
Valuedash-apiserver选择刚创建的 Token
  1. 点击 Save policy

为什么 Action 要选 Service AuthAllow 策略会要求同时满足身份认证(如邮箱登录),导致 Service Token 请求被重定向到登录页。 Service Auth 专门用于服务间调用,不会要求额外认证。

步骤四:配置第二条策略(团队成员访问)

  1. 再次点击 Add a policy
  2. 填写策略信息:
字段
Policy nameAllow Team Members
ActionAllow
SelectorEmails Ending In
Value@myapp.com
  1. 点击 Save policy

步骤五:调整策略顺序

在策略列表中,确保 Allow Service Token 策略在第一位Allow Team Members 在第二位。

策略顺序影响匹配逻辑:Cloudflare Access 按从上到下的顺序评估,匹配到第一条后停止。将 Service Token 策略放在第一位,可以确保携带 Token 的请求优先被放行。

步骤六:保存应用

点击页面底部的 Save 完成应用创建。

5.4 测试验证

测试 1:浏览器访问(团队成员)

在浏览器无痕模式访问 https://api.myapp.com/health

  1. 自动跳转到 Cloudflare Access 登录页
  2. 输入 @myapp.com 邮箱
  3. 接收验证码并登录
  4. ✅ 成功看到 API 响应:
    json
    {"service":"myapp-apiserver","status":"ok","timestamp":"2026-05-26T14:23:48Z"}

测试 2:Service Token 调用

使用 curl 命令测试服务间调用:

bash
curl -H "CF-Access-Client-Id: 888/104a908c1a61de52c89237/26bee0.access" \
     -H "CF-Access-Client-Secret: 9eca11cf456625c4ac861f327eee798824496842d718f1b248b3c36936a2b238" \
     https://api.myapp.com/health

预期结果:直接返回 200 和 JSON 响应,无重定向,无登录页。

测试 3:未授权访问

bash
curl https://api.myapp.com/health

预期结果:返回 302 重定向到 Cloudflare Access 登录页。

5.5 常见配置错误与解决

错误现象可能原因解决方案
curl 请求返回 302 重定向策略 Action 用了 Allow改为 Service Auth
curl 请求返回 302 重定向策略顺序错误将 Service Token 策略移到第一位
浏览器无法登录邮箱域输入错误确认是 @myapp.com 格式
配置不生效忘记保存确保点击了 Save policySave
Service Token 找不到未创建或已过期在 Service Credentials 中重新创建

5.6 前端服务配置

在 Nuxt 的后端代码中,使用 Service Token 调用 API:

javascript
// server/api/call-backend.js
export default defineEventHandler(async (event) => {
  const response = await $fetch('https://api.myapp.com/user/info', {
    headers: {
      'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID,
      'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET,
    }
  })
  return response
})

环境变量配置(.env):

ini
CF_ACCESS_CLIENT_ID=888/104a908c1a61de52c89237/26bee0.access
CF_ACCESS_CLIENT_SECRET=9eca11cf456625c4ac861f327eee798824496842d718f1b248b3c36936a2b238

6. 上线切换操作

当产品准备正式上线时:

操作步骤效果
关闭 dash 保护Access → Applications → 点击应用 → 将状态从 On 改为 Off所有人直接访问控制台,无需登录
关闭 api 保护Access → Applications → 点击应用 → 将状态从 On 改为 OffAPI 不再要求 Service Token 或邮箱认证

就这么简单。 无需修改任何 Nginx 配置,无需重启任何服务。

7. 灰度模式 vs 上线模式对比

访问者灰度模式(Access 开启)上线模式(Access 关闭)
团队成员(邮箱 @myapp.com)✅ 正常访问✅ 正常访问
外部用户(其他邮箱)❌ 被 Access 拦截✅ 正常访问
API 调用(无 Service Token)❌ 返回 302 重定向✅ 正常响应
前端服务调用(带 Service Token)✅ 正常调用✅ 正常调用
Nginx 配置无需改动无需改动
应用代码无需改动无需改动

8. 方案优势总结

优势说明
✅ 解决头冲突不占用 Authorization 头,SaaS 应用的 Token 认证完全不受影响
✅ 零配置上线上线时只需在 Cloudflare 控制台关闭策略,无需修改任何服务器配置
✅ 统一管理控制台和 API 两个子域名在同一个界面管理,规则清晰
✅ 团队友好支持邮箱域批量授权(如 @myapp.com),无需维护密码文件
✅ 审计日志完整记录谁、何时、从哪里登录,满足安全合规需求
✅ 成本低廉Cloudflare Access 提供 50 用户免费额度,完全够用
✅ 易于撤销团队成员离职,只需在 Access 控制台移除权限,无需修改服务器

9. 常见问题

Q1: 正式上线后,API 还想保留 Token 认证怎么办?

A: 完全没问题。Cloudflare Access 关闭后,Authorization 头不再被任何中间件占用,你的 Golang API 可以正常解析自己的 JWT Token。

Q2: Cloudflare Access 会影响 SEO 吗?

A: 灰度阶段不需要 SEO。正式上线后关闭 Access,不影响。或者你可以为搜索引擎配置 Bypass 规则。

Q3: Service Token 的安全性如何?

A: Service Token 建议只在前端服务端代码中使用,不要暴露在客户端。Client Secret 仅在创建时显示一次,需妥善保存。

Q4: 这套方案需要付费吗?

A: Cloudflare Access 提供 50 用户 免费额度,对于大多数团队灰度发布场景完全够用。

Q5: 为什么要选择 Zero Trust Free 而不是其他计划?

A: Free 计划已包含 Self-hosted 应用、Service Token、邮箱域策略等核心功能,50 人以内完全免费,无需付费升级。

Q6: Service Token 策略为什么要用 Service Auth 而不是 Allow

A: Allow 策略会要求同时满足身份认证(如邮箱登录),导致 Service Token 请求被重定向到登录页。Service Auth 专门用于服务间调用,不会要求额外认证。

10. 总结

通过 Cloudflare Access,我们实现了一套完美的灰度发布访问控制方案:

  1. 灰度阶段:Access 在边缘节点保护 dash.api.,团队成员通过邮箱验证登录
  2. 正式上线:只需在控制台关闭策略,产品立即对全网开放
  3. 全程无需修改 Nginx 或应用代码,认证层与应用层完全解耦

快速记忆

认证交给 Cloudflare,Nginx 只做代理,上线一键关闭,全程无需改代码。Free 计划完全够用,Service Token 要用 Service Auth!

附录:占位符替换清单

占位符示例值说明
myapp.comyourdomain.com主域名
dash.myapp.compreview.yourdomain.com控制台域名
api.myapp.comapi.yourdomain.comAPI 域名
@myapp.com@company.com团队邮箱域
dash-apiserverfrontend-backend-tokenService Token 名称