自己动手
$109 每个平台,一次性付费
WebViewGold 许可证:一个现成的 Xcode 或 Android Studio 项目。粘贴您的 URL,设置名称和图标,开启所需功能,构建并提交。永久免费更新。
在主页查看方案安全
把 Web 应用封装进原生外壳,并不能隐藏它的 JavaScript:同一个 bundle 从您的 URL 加载,任何人都能在浏览器中打开它。所以 Web 的规则同样适用。可公开的密钥(Supabase publishable 或 anon、Stripe pk_、Firebase Web 配置、OneSignal App ID)可以随应用发布;机密密钥(Supabase secret 或 service_role、Stripe sk_、OneSignal App API 密钥、服务账号、LLM API 密钥)则绝对不行。下面介绍如何区分它们、为什么行级安全(RLS)才是真正的锁,以及一套适用于任何编码代理的审查流程。
每个平台一次性 $109 起,或由我们的团队全程代劳,$698 起。
WebViewGold 应用是包裹着您已部署 URL 的原生外壳。用户运行的 JavaScript 从您的服务器下载,和在 Safari 或 Chrome 中完全一样。任何人都可以在笔记本电脑上打开您的域名,打开开发者工具,读取 bundle 中的每一个字符串,包括构建工具内联进去的环境变量。上架应用商店并不会改变这一点。
原生项目也不是保险库。Config.swift、Config.java、Info.plist、google-services.json 以及custom.js 和 custom.css 文件中的值最终都会进入 IPA 或 APK,而应用安装包就是一个归档文件,工具几秒钟就能解开。请把发送到设备上的一切都视为公开内容。专为客户端设计的密钥对此没有问题;机密密钥则不然。
构建工具让这种错误很容易发生。以 VITE_(Vite)或 NEXT_PUBLIC_(Next.js)为前缀的变量,按设计就会被内联进客户端 bundle。一个通过把密钥移进这类变量、或把它粘贴进 fetch 调用来“修复” 401 错误的代理,实际上已经把它公开了。代理这样做,是因为这样能让错误消失;必须由您的审查把它拦下来。
| 服务 | 可以放在客户端代码中 | 只能放在服务器端 | 真正保护您的是什么 |
|---|---|---|---|
| Supabase | Publishable 密钥(sb_publishable_...)或旧版 anon 密钥 | Secret 密钥(sb_secret_...)或旧版 service_role 密钥;两者都会绕过行级安全 | 每张表和每个存储桶上的 RLS 策略 |
| Firebase | Web 配置(apiKey、authDomain、projectId、appId) | 服务账号 JSON、Admin SDK 凭据 | Security Rules、App Check、Google Cloud 中的 API 密钥限制 |
| Stripe | Publishable 密钥(pk_live_...) | Secret 密钥(sk_live_...)、受限密钥(rk_live_...)、webhook 签名密钥(whsec_...) | 在您的服务器上创建 Checkout Session 和 PaymentIntent;验证 webhook |
| OneSignal | App ID(Config.swift / build.gradle 中的值) | App API 密钥(os_v2_app_...)、旧版 REST API 密钥、Organization API 密钥 | 从您的服务器向已保存的订阅 ID 发送推送 |
| RevenueCat | 公开的应用专属 SDK 密钥,即原生应用使用的那种 | 用于 REST 调用的 Secret API 密钥 | 通过 webhook 在服务器上检查权益(entitlements) |
| LLM API(Gemini、OpenAI、Anthropic) | 什么都不行 | 所有 API 密钥 | 一个验证用户身份并进行限流的服务器函数或边缘函数 |
同一个错误的三种写法,然后是修复方案。修复方式始终相同:客户端带着用户的会话调用您自己的接口,只有服务器持有机密密钥。
// WRONG: service_role key in the browser. Bypasses every RLS policy.
const admin = createClient(SUPABASE_URL, import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY);
// WRONG: Stripe secret key in a fetch from the client
await fetch("https://api.stripe.com/v1/checkout/sessions", {
method: "POST",
headers: { Authorization: "Bearer " + import.meta.env.VITE_STRIPE_SECRET_KEY }
});
// WRONG: sending pushes from the browser with the OneSignal App API key
await fetch("https://api.onesignal.com/notifications", {
method: "POST",
headers: { Authorization: "Key " + ONESIGNAL_APP_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ app_id: ONESIGNAL_APP_ID, contents: { en: "Hi" } })
});
// RIGHT: the client calls your endpoint with the user's session token
const res = await fetch("/api/checkout", {
method: "POST",
headers: { Authorization: "Bearer " + session.access_token, "Content-Type": "application/json" },
body: JSON.stringify({ priceId: "price_basic_monthly" })
});
// RIGHT: on the server (API route, Supabase Edge Function, Cloud Function)
// 1. verify the session token and derive the user id from it, never from the body
// 2. read STRIPE_SECRET_KEY / ONESIGNAL_APP_API_KEY / SUPABASE_SECRET_KEY from server env
// 3. call the provider, return only what the client needsSupabase 的 publishable 密钥之所以安全,只是因为由 Postgres 决定每个请求能看到什么:没有登录用户时以 anon 身份运行,有登录用户时以 authenticated 身份运行,再由策略过滤行。Secret 密钥以 service_role 身份运行,它拥有 BYPASSRLS(Supabase API 密钥文档)。暴露的 schema 中的每张表都需要启用 RLS,并配置类似下面的策略;Firebase 则通过 Security Rules 达到同样的效果。Supabase 检查清单和Firebase 检查清单涵盖了后端的其余部分。
-- Supabase SQL: enable RLS and let users see and change only their own row
alter table public.profiles enable row level security;
create policy "profiles: read own" on public.profiles
for select to authenticated
using ((select auth.uid()) = id);
create policy "profiles: update own" on public.profiles
for update to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
-- Firestore rules, same idea:
-- match /profiles/{uid} {
-- allow read, write: if request.auth != null && request.auth.uid == uid;
-- }对 Claude Code、Codex、Cursor、Gemini CLI、Antigravity、Kiro、Cline 和所有应用构建工具都同样适用。
运行生产构建,并搜索输出文件夹(扫描方法见下文)。源代码看起来可能很干净,而内联的环境变量却留在 bundle 中。
列出所有以 VITE_、NEXT_PUBLIC_ 或类似前缀开头的变量。每一个都必须是您敢印在广告牌上的东西。
Config.swift、Config.java、Info.plist、build.gradle、custom.js 和 Firebase 配置文件中只能包含公开 ID(OneSignal App ID、RevenueCat 公开密钥、AdMob ID、Firebase 配置)。
Supabase:每张表都启用了 RLS,没有对写入操作直接设为 true 的策略,Security Advisor 的警告都已解决。Firebase:没有 if true 规则。边缘函数从令牌中获取用户,绝不从请求体中的用户 ID 获取。
只要曾经被提交或部署过,密钥就已经泄露,即使最新的 commit 删除了它。先在服务提供商的控制台中轮换它,然后再清理。为代码仓库开启 secret scanning 和 push protection。
根据您的技术栈调整文件夹(Vite:dist/,Next.js:.next/static/,Create React App:build/)。对于 Firebase Web 配置,匹配到 AIza 是正常的;请确认该密钥已受限制,并且不是 Gemini 密钥。
# 1. Build the production bundle
npm run build
# 2. Search shipped files for secret-looking strings
grep -rEn "sk_live_|sk_test_|rk_live_|whsec_|sb_secret_|os_v2_app_|BEGIN (RSA )?PRIVATE KEY|AIza[0-9A-Za-z_-]{35}" dist/ .next/static/ build/ 2>/dev/null
# 3. Legacy Supabase keys are JWTs: decode each one and look at "role"
# "anon" is expected, "service_role" means a secret key shipped
grep -rhoE "eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+" dist/ 2>/dev/null | sort -u | while read -r t; do
echo "$t" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
done
# 4. Native projects and git history
grep -rEn "sk_live_|sb_secret_|os_v2_app_|service_role" ios/ android/ 2>/dev/null
git log -p --all | grep -En "sk_live_|sb_secret_|os_v2_app_|SERVICE_ROLE" | head -20它会先要求给出报告,并且从不打印完整的密钥,所以即使在共享会话中也可以运行。
在我们用 WebViewGold 把这个代码仓库发布为 iOS 和 Android 应用之前,请对它进行一次密钥和数据访问审查。请记住:这些应用加载的是我们已部署的 Web 应用,所以客户端 bundle 中的每个文件都是公开的,ios/ 和 android/ 中的每个值都会随应用安装包一起发布。 1. 列出所有地方引用的每一个 API 密钥、令牌、机密信息和凭据:源代码、env 文件、配置、CI 工作流、ios/、android/。对每一项写明:服务、使用位置、是否会进入客户端 bundle,以及它是可公开的(Supabase publishable 或 anon、Stripe pk_、Firebase Web 配置、OneSignal App ID、RevenueCat 公开 SDK 密钥)还是机密的(Supabase secret 或 service_role、Stripe sk_、rk_ 或 whsec_、OneSignal App API 密钥、服务账号、任何 LLM API 密钥)。 2. 对每个进入客户端的机密密钥:提议把调用迁移到一个服务器路由或函数中,先验证用户的会话,再从服务器端环境变量中读取机密密钥。在修改任何支付代码之前,先把计划给我看。 3. Supabase:列出每张表和每个存储桶,是否启用了 RLS,以及存在哪些策略。标记没有 RLS 的表和不检查 auth.uid() 的策略。Firebase:标记允许在没有 request.auth 的情况下访问或跨用户访问的规则。 4. 边缘函数和 API 路由:标记任何从请求体而不是从已验证的令牌中获取用户 ID 的函数或路由。 5. Git 历史:找出已提交的 .env 文件或密钥,并列出哪些密钥必须轮换。不要改写历史。 6. 在我们的代理上下文文件中添加一个简短的密钥章节,写明允许使用的可公开密钥,并禁止在客户端代码中出现其他任何密钥。 以表格形式报告。绝不打印完整的密钥值;只显示前 6 个字符。
if true。安全,它和较新的 publishable 密钥一样,本就是为公开客户端设计的,前提是每张表都启用了行级安全并配置了合理的策略。Supabase 计划在 2026 年底前弃用旧版 anon 和 service_role 密钥,所以新项目应使用 publishable 和 secret 密钥。
能。Web bundle 从您的 URL 下载,可以在任何浏览器中读取;IPA 和 APK 文件是归档文件,其中的配置文件和字符串很容易查看。只发布本就打算公开的密钥。
按设计不是;它用于标识您的项目,起保护作用的是 Security Rules 和 App Check。由于在项目中启用 Gemini API 后,未加限制的 Google API 密钥也能访问它,请把该密钥限制为只能访问所需的 API,并把 Gemini 调用放在服务器上。
只有公开的 App ID:iOS 上放在 Config.swift 中,Android 上放在 build.gradle 中。用于发送通知的 App API 密钥留在您的服务器上,服务器通过应用传递给 Web 应用的订阅 ID 来定位用户。
立即在服务提供商的控制台中轮换它,检查服务提供商的日志和账单是否有滥用迹象,然后从代码中删除它,并把调用迁移到服务器端。只删除而不轮换,旧密钥仍然有效。
商店审核不是安全审计,所以别指望它能发现泄露。真正的风险是滥用、意外的账单和用户数据暴露,而后者可能演变成在两个商店都会遇到的隐私问题。
结果相同,只是您需要投入的工作量不同。三种方式都附带审核通过保证和 7×24 小时人工支持。
$109 每个平台,一次性付费
WebViewGold 许可证:一个现成的 Xcode 或 Android Studio 项目。粘贴您的 URL,设置名称和图标,开启所需功能,构建并提交。永久免费更新。
在主页查看方案$349 每个许可证,一次性附加购买
无需 Mac、Xcode 或 Android Studio:在浏览器中完成配置,我们在云端构建两个应用,并将其交付到 App Store Connect 和 Play Console。
Cloud Builder 如何运作 试用免费演示$698 每个平台 $798
把您的 URL 和图标发给我们。我们的团队在 72 小时内完成配置、构建并交付您的应用,白标,附源码项目。两个平台共 $1,158。
Full Service 详情 服务提供方:冬季特惠价格有效期至 2026年12月31日。
App Store & Google Play 审核通过保证 – 否则 14 天内退款
想直接交给我们?Full Service:我们在 72 小时内完成配置、构建并交付,每个平台 $698 起。有疑问?通过 7×24 小时在线聊天联系我们。
退款依据我们 FAQ 中列出的条件:应用因非内容原因被 Apple 和/或 Google 拒审,并在购买后 14 天内提出申请。