Appearance
基于 Cloudflare Access 的零信任访问控制:从灰度发布到正式上线的完美方案
适用场景
- 预发布环境 / 灰度验证环境 / 内部测试环境
- 核心需求:需要对未完成的产品进行访问控制,仅允许团队成员访问,但不希望在应用代码中实现登录逻辑,也不希望 Nginx 基础认证与应用认证发生冲突。
在 SaaS 产品的开发过程中,我们常常面临一个两难困境:如何保护未完成的预览版产品,又不干扰正常的开发调试?
传统的 Nginx 基础认证(Basic Auth)虽然简单,但会与应用的 Authorization: Bearer Token 认证发生冲突。更麻烦的是,当产品正式上线时,我们往往需要修改 Nginx 配置、重启服务,甚至改代码才能移除这道“临时墙”。
本文介绍一种基于 Cloudflare Access 的零信任访问控制方案,它能在 DNS 解析之后、请求到达源站之前建立一道可随时开关的认证墙,实现从“灰度发布”到“正式上线”的无缝切换,无需任何代码或配置变更。
1. 问题背景:传统方案的痛点
1.1 架构场景
假设我们有一个标准的 SaaS 项目架构:
| 子域名 | 用途 | 技术栈 |
|---|---|---|
dash.myapp.com | SaaS 控制台(前端) | 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/月 | ✅ 完全免费 |
激活步骤:
- 进入 Cloudflare 控制台 → Zero Trust
- 选择 Zero Trust Free 计划
- 确认支付信息(不会扣费,仅用于风控)
- 激活成功,即可开始配置
2.3 与传统方案的本质区别
| 对比维度 | Nginx 基础认证 | Cloudflare Access |
|---|---|---|
| 认证位置 | 源站 Nginx 层 | Cloudflare 边缘节点 |
是否占用 Authorization 头 | ✅ 是,导致冲突 | ❌ 否,完全不修改请求头 |
| 上线切换方式 | 修改 Nginx 配置并重启 | 控制台一键关闭策略 |
| 认证方式 | 用户名/密码 | 支持 GitHub、Google、OTP、邮箱验证码 |
| 团队管理 | 手动编辑 .htpasswd | Web 界面管理,支持邮箱域授权 |
| 审计日志 | ❌ 无 | ✅ 完整记录 |
2.4 为什么要统一管理两个子域名?
在灰度发布阶段,我们需要同时保护:
- 控制台:防止外部用户访问未完成的页面
- API:防止未授权的 API 调用
Cloudflare Access 允许我们为两个子域名分别创建策略,但使用统一的管理界面,实现:
- 统一的身份认证源
- 统一的团队权限管理
- 统一的上线切换操作
3. 架构设计
3.1 整体架构图
3.2 核心设计原则
- 认证与业务解耦:访问控制完全在 Cloudflare 层完成,源站 Nginx 和应用无需感知
- 统一策略管理:两个子域名在同一个控制台管理,上线时可一键切换
- 零配置上线:正式上线时只需关闭 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 计划:
- 登录 Cloudflare 控制台,点击左侧菜单的 Zero Trust
- 如果是首次使用,会看到计划选择页面
- 选择 Zero Trust Free 计划(
$0/seat/month,50 人免费) - 确认支付信息(需要绑定信用卡或 PayPal,仅用于风控验证,不会扣费)
- 点击 Activate 完成激活
激活成功后,即可开始配置 Access 应用。
5.2 为 dash.myapp.com 配置交互式登录保护
步骤一:创建 Self-hosted 应用
- 进入 Zero Trust → Access → Applications
- 点击 Add an application → 选择 Self-hosted
步骤二:填写应用基本信息
| 字段 | 填写内容 | 说明 |
|---|---|---|
| Application name | dash-myapp | 应用名称,便于识别 |
| Subdomain | dash | 子域名部分 |
| Domain | myapp.com | 主域名 |
| Path | 留空(或填 *) | 保护所有路径 |
填写完成后,点击 Add public hostname。
步骤三:配置访问策略
- 向下滚动到 Access policies 区域
- 点击 Add a policy
- 填写策略信息:
| 字段 | 值 | 说明 |
|---|---|---|
| Policy name | Allow Team Members | 策略名称 |
| Action | Allow | 允许符合条件的请求 |
| Selector | Emails Ending In | 选择邮箱后缀匹配 |
| Value | @myapp.com | 你的团队邮箱域 |
- 点击 Save policy
- 点击页面底部的 Save 完成应用创建
验证配置:在浏览器无痕模式访问 https://dash.myapp.com,应该自动跳转到 Cloudflare Access 登录页面,输入 @myapp.com 邮箱后接收验证码即可登录。
5.3 为 api.myapp.com 配置 Service Token(服务间调用)
对于 API 子域名,我们需要支持两种访问方式:
- 服务间调用:前端服务(Nuxt 后端)静默调用,无需人工交互
- 团队成员访问:开发人员通过浏览器调试 API
这需要配置 Service Token 和两条访问策略。
步骤一:创建 Service Token
- 在 Access 左侧菜单,点击 Service Credentials → Service Tokens
- 点击 Create Service Token
- 填写信息:
| 字段 | 填写内容 | 说明 |
|---|---|---|
| Name | dash-apiserver | Token 名称,建议与调用方关联 |
| Duration | 2 years | 有效期,建议设长一些 |
- 点击 Generate token
- ⚠️ 关键步骤:立即保存生成的
Client ID和Client Secret!
- Client ID 格式示例:
888/104a908c1a61de52c89237/26bee0.access - Client Secret 是一长串随机字符:
9eca11cf456625c4ac861f327eee7988... - Secret 只显示一次,关闭页面后无法再次查看!
步骤二:创建 API 应用
- 回到 Access → Applications
- 点击 Create new application → Self-hosted
- 填写应用信息:
| 字段 | 填写内容 |
|---|---|
| Application name | api-myapp |
| Subdomain | api |
| Domain | myapp.com |
| Path | 留空(或填 *) |
点击 Add public hostname。
步骤三:配置第一条策略(Service Token 放行)
- 点击 Add a policy
- 填写策略信息:
| 字段 | 值 | 说明 |
|---|---|---|
| Policy name | Allow Service Token | 策略名称 |
| Action | Service Auth | ⚠️ 注意:不是 Allow! |
| Selector | Service Token | 选择服务令牌 |
| Value | dash-apiserver | 选择刚创建的 Token |
- 点击 Save policy
为什么 Action 要选
Service Auth?Allow策略会要求同时满足身份认证(如邮箱登录),导致 Service Token 请求被重定向到登录页。Service Auth专门用于服务间调用,不会要求额外认证。
步骤四:配置第二条策略(团队成员访问)
- 再次点击 Add a policy
- 填写策略信息:
| 字段 | 值 |
|---|---|
| Policy name | Allow Team Members |
| Action | Allow |
| Selector | Emails Ending In |
| Value | @myapp.com |
- 点击 Save policy
步骤五:调整策略顺序
在策略列表中,确保 Allow Service Token 策略在第一位,Allow Team Members 在第二位。
策略顺序影响匹配逻辑:Cloudflare Access 按从上到下的顺序评估,匹配到第一条后停止。将 Service Token 策略放在第一位,可以确保携带 Token 的请求优先被放行。
步骤六:保存应用
点击页面底部的 Save 完成应用创建。
5.4 测试验证
测试 1:浏览器访问(团队成员)
在浏览器无痕模式访问 https://api.myapp.com/health:
- 自动跳转到 Cloudflare Access 登录页
- 输入
@myapp.com邮箱 - 接收验证码并登录
- ✅ 成功看到 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 policy 和 Save |
| 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=9eca11cf456625c4ac861f327eee798824496842d718f1b248b3c36936a2b2386. 上线切换操作
当产品准备正式上线时:
| 操作 | 步骤 | 效果 |
|---|---|---|
关闭 dash 保护 | Access → Applications → 点击应用 → 将状态从 On 改为 Off | 所有人直接访问控制台,无需登录 |
关闭 api 保护 | Access → Applications → 点击应用 → 将状态从 On 改为 Off | API 不再要求 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,我们实现了一套完美的灰度发布访问控制方案:
- 灰度阶段:Access 在边缘节点保护
dash.和api.,团队成员通过邮箱验证登录 - 正式上线:只需在控制台关闭策略,产品立即对全网开放
- 全程无需修改 Nginx 或应用代码,认证层与应用层完全解耦
快速记忆
认证交给 Cloudflare,Nginx 只做代理,上线一键关闭,全程无需改代码。Free 计划完全够用,Service Token 要用 Service Auth!
附录:占位符替换清单
| 占位符 | 示例值 | 说明 |
|---|---|---|
myapp.com | yourdomain.com | 主域名 |
dash.myapp.com | preview.yourdomain.com | 控制台域名 |
api.myapp.com | api.yourdomain.com | API 域名 |
@myapp.com | @company.com | 团队邮箱域 |
dash-apiserver | frontend-backend-token | Service Token 名称 |