Use Cases (更新: 2026/6/2)

Claude Code 安全实现 Geolocation API:权限、备用输入、隐私与测试

用 Claude Code 安全实现 Geolocation API:权限体验、HTTPS、备用输入、隐私与测试。

Claude Code 安全实现 Geolocation API:权限、备用输入、隐私与测试

位置功能很容易被低估。一个“查找附近门店”的按钮,看起来只是调用一次浏览器 API,但实际会牵涉权限弹窗、HTTPS、安全上下文、用户拒绝、超时、手动地址输入、地图服务边界、日志脱敏以及自动化测试。

如果只让 Claude Code“加一个当前位置按钮”,它很可能写出能跑的 getCurrentPosition 片段,却没有说明为什么要请求位置,也没有处理拒绝、没有备用输入,甚至把经纬度直接打进日志。公开产品不能这样交付。

本文以初学者也能照做的方式,讲清楚如何用 Claude Code 实现浏览器 Geolocation API。参考的一手资料包括 MDN Geolocation APIgetCurrentPositionwatchPositionPermissions APIW3C Geolocation 规范、Chrome 对安全来源的要求Chrome DevTools SensorsPlaywright emulationClaude Code permissions。站内可以继续阅读 地图集成安全审计响应式设计

先划清功能边界

Geolocation API 的职责只有一个:让浏览器尝试返回当前设备的位置。位置来源可能是 GPS、Wi-Fi、蜂窝网络、IP、系统定位服务或缓存结果。应用无法保证使用哪一种来源,也不能假设返回值一定是用户的真实精确位置。

因此,写代码前要先做四个决定。第一,只在用户主动点击时请求权限,不要在页面加载时弹窗。第二,默认不要启用高精度,除非业务真的需要。第三,必须准备邮编、城市、地址或门店名的手动输入。第四,先决定是否保存位置;很多场景只需要计算结果,不需要保存原始经纬度。

决策推荐做法常见错误
请求时机用户点击“使用当前位置”后打开页面立刻请求权限
精度初次使用 enableHighAccuracy: false所有场景都强制高精度
失败处理提供邮编或地址输入把拒绝当成异常并卡住流程
日志记录事件和粗粒度分组console.log(position) 直接泄露坐标

地图也要分层。Geolocation 返回坐标;地图渲染、地址转坐标、路线规划、附近地点搜索属于 Google Maps、Mapbox、OpenStreetMap 或后端服务。让 Claude Code 改代码时,要明确“浏览器位置模块只返回坐标,不碰地图 API key,不直接调用付费地图服务”。

具体产品场景

第一个场景是门店搜索。用户点击按钮后,应用用当前位置给门店排序。安全做法是在按钮旁边放“按邮编/城市搜索”,这样拒绝权限的用户仍然能完成任务。

第二个场景是配送范围与到达时间。生鲜、电商、上门维修、租赁服务都可能需要判断用户是否在服务区内。多数情况下,后台只需要保存“是否在服务区”“距离分组”或“可预约时段”,而不是保存精确坐标。

第三个场景是现场作业签到。清洁、运维、活动工作人员、销售拜访可以用位置确认是否到达指定地点。但定位失败时不能直接阻断工作,应提供照片、主管审批、手写备注等备用流程。

第四个场景是本地内容。天气、附近活动、库存门店、城市公告都可以和位置相关。不过如果只需要城市级别,用户手动选择城市往往比请求精确定位更合适,也更容易建立信任。

可直接运行的 getCurrentPosition 示例

下面保存为 geo-demo.html,通过 localhost 或 HTTPS 打开。它包含权限说明、超时、缓存位置、手动备用输入和简单的脱敏显示。

<!doctype html>
<html lang="zh">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Geolocation demo</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        line-height: 1.6;
        margin: 2rem;
      }
      button,
      input {
        font: inherit;
        padding: 0.7rem 0.9rem;
      }
      .panel {
        border: 1px solid #ddd;
        max-width: 36rem;
        padding: 1rem;
      }
    </style>
  </head>
  <body>
    <main class="panel">
      <h1>查找附近门店</h1>
      <p>
        我们只用当前位置排序附近门店,
        不保存精确地址或移动轨迹。
      </p>

      <button id="useLocation" type="button">使用当前位置</button>
      <p id="status" role="status" aria-live="polite"></p>
      <pre id="result"></pre>

      <form id="manualForm">
        <label for="postcode">邮编、城市或地址</label>
        <input id="postcode" name="postcode" autocomplete="postal-code" />
        <button type="submit">手动搜索</button>
      </form>
    </main>

    <script type="module">
      const status = document.querySelector("#status");
      const result = document.querySelector("#result");
      const button = document.querySelector("#useLocation");
      const form = document.querySelector("#manualForm");

      function showManual(reason) {
        status.textContent =
          `${reason}。也可以用邮编、城市或地址搜索。`;
      }

      function onSuccess(position) {
        const { latitude, longitude, accuracy } = position.coords;
        status.textContent = "已获取当前位置。";
        result.textContent = JSON.stringify(
          {
            lat: Number(latitude.toFixed(5)),
            lng: Number(longitude.toFixed(5)),
            accuracyMeters: Math.round(accuracy),
          },
          null,
          2,
        );
      }

      function onError(error) {
        const messages = {
          1: "位置权限被拒绝",
          2: "设备位置暂时不可用",
          3: "位置请求超时",
        };
        showManual(messages[error.code] ?? "无法获取位置");
      }

      button.addEventListener("click", () => {
        if (!("geolocation" in navigator)) {
          showManual("当前浏览器不支持 Geolocation");
          return;
        }

        status.textContent = "正在确认位置权限...";
        navigator.geolocation.getCurrentPosition(onSuccess, onError, {
          enableHighAccuracy: false,
          timeout: 8000,
          maximumAge: 60000,
        });
      });

      form.addEventListener("submit", (event) => {
        event.preventDefault();
        const data = new FormData(form);
        status.textContent =
          `将以“${data.get("postcode")}”为中心搜索。`;
      });
    </script>
  </body>
</html>

timeout 是等待上限,maximumAge 是可接受的缓存位置时长,enableHighAccuracy 是向浏览器请求更高精度。门店搜索通常不需要高精度;现场签到可以把缓存时间缩短,并在 UI 上告诉用户正在进行一次新的定位。

watchPosition 与停止追踪

watchPosition 用于持续接收位置变化。它适合配送员路线、运动记录、现场作业移动状态,不适合普通“查附近”按钮。调用后会返回一个 ID,必须用 clearWatch 停止。

import { useEffect, useRef, useState } from "react";

type LocationPoint = {
  lat: number;
  lng: number;
  accuracy: number;
  at: string;
};

export function TrackingPanel() {
  const watchId = useRef<number | null>(null);
  const [points, setPoints] = useState<LocationPoint[]>([]);
  const [error, setError] = useState<string | null>(null);

  function start() {
    if (!navigator.geolocation || watchId.current !== null) return;

    watchId.current = navigator.geolocation.watchPosition(
      (position) => {
        const { latitude, longitude, accuracy } = position.coords;
        setPoints((current) => [
          {
            lat: Number(latitude.toFixed(5)),
            lng: Number(longitude.toFixed(5)),
            accuracy: Math.round(accuracy),
            at: new Date(position.timestamp).toISOString(),
          },
          ...current.slice(0, 9),
        ]);
      },
      (err) => setError(`追踪失败:${err.code}`),
      {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 5000,
      },
    );
  }

  function stop() {
    if (watchId.current === null) return;
    navigator.geolocation.clearWatch(watchId.current);
    watchId.current = null;
  }

  useEffect(() => stop, []);

  return (
    <section>
      <button type="button" onClick={start}>开始追踪</button>
      <button type="button" onClick={stop}>停止</button>
      {error && <p role="alert">{error}</p>}
      <ol>
        {points.map((point) => (
          <li key={point.at}>
            {point.lat}, {point.lng}
            {" / "}
            {point.accuracy}m
          </li>
        ))}
      </ol>
    </section>
  );
}

持续追踪要有明确的开始、停止和保存策略。不要把“用户打开过页面”变成后台追踪。即使业务需要轨迹,也要先定义保存多久、谁能查看、如何删除。

隐私与日志设计

经纬度不应该作为普通调试信息处理。它可能进入浏览器控制台、错误监控、分析事件、服务器日志、客服截图或录屏工具。更稳妥的做法是只记录权限状态、精度分组和非常粗的位置分组。

type GeoLogInput = {
  lat: number;
  lng: number;
  accuracy: number;
  permission: "granted" | "prompt" | "denied" | "unknown";
};

export function toPrivacySafeGeoLog(input: GeoLogInput) {
  return {
    permission: input.permission,
    accuracyBucket:
      input.accuracy <= 50 ? "high" :
      input.accuracy <= 500 ? "medium" : "low",
    latBucket: Number(input.lat.toFixed(2)),
    lngBucket: Number(input.lng.toFixed(2)),
  };
}

如果只是优化转化,很多事件不需要坐标,例如 manual_search_usedpermission_deniedtimeoutstore_results_shown。让 Claude Code 改日志时,要明确“禁止记录原始 latitude 和 longitude”。

Permissions API 可以读取 grantedpromptdenied 等状态,但不要把它当成万能方案。真正的浏览器权限弹窗仍然发生在调用 getCurrentPositionwatchPosition 时。

export async function readGeoPermission() {
  if (!("permissions" in navigator)) return "unknown";

  try {
    const status = await navigator.permissions.query({
      name: "geolocation",
    });
    return status.state;
  } catch {
    return "unknown";
  }
}

失败模式与测试

必须测试 HTTPS。现代浏览器要求安全上下文,普通 HTTP 或被不安全页面嵌套的 iframe 可能直接失败。还要测试 Permissions-Policy,因为跨域 iframe 可能被顶层页面禁止使用 geolocation。

必须测试拒绝权限。拒绝是正常选择,不是异常用户。UI 应该自然切换到手动输入,而不是反复弹窗或显示技术错误。

必须测试超时和位置不可用。室内环境、桌面设备、系统定位关闭、VPN、企业浏览器策略都会触发失败路径。

Chrome DevTools 的 Sensors 面板适合手动检查;Playwright 可以做稳定的自动化测试。

import { expect, test } from "@playwright/test";

test.use({
  geolocation: {
    latitude: 31.230416,
    longitude: 121.473701,
    accuracy: 50,
  },
  permissions: ["geolocation"],
});

test("shows nearby stores from mocked location", async ({ page }) => {
  await page.goto("/stores");
  await page.getByRole("button", { name: "使用当前位置" }).click();
  await expect(page.getByText("已获取当前位置")).toBeVisible();
});

地图服务也要单独测试。Geolocation 成功不代表地图 SDK 成功,也不代表地理编码、路线规划或门店搜索接口成功。API key 限制、配额、账单和服务区域都属于地图层。

安全的 Claude Code 提示词

位置功能的提示词要写清范围、禁止事项和验收标准。不要只说“实现定位功能”。

claude <<'PROMPT'
Implement a beginner-friendly Geolocation feature.

Scope:
- Edit only src/features/location and related tests.
- Do not change billing, analytics, or map provider config.
- Preserve existing API keys and environment variable names.

Requirements:
- Request location only after the user clicks a button.
- Explain why location is needed before the browser prompt.
- Use getCurrentPosition with timeout and maximumAge.
- Add manual postcode/address fallback for denied or timeout cases.
- Do not log raw latitude or longitude.
- Add a Playwright test with mocked geolocation.
- Return a short verification checklist.
PROMPT

团队使用 Claude Code 时,可以用 permission rules 保护 .env、禁止自动 push,并让测试命令保持可执行。

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Bash(git push *)"
    ],
    "allow": [
      "Bash(npm test *)",
      "Bash(npm run lint)"
    ]
  }
}

如果团队希望把位置功能、地图服务、隐私说明和测试流程整理成可复用规范,可以查看 Claude Code 培训与咨询。带上现有地图供应商、隐私约束和一个真实用户流程,效果会比通用代码片段好得多。

实际验证记录

本文示例在 Chrome 的 localhost 环境下验证过,并使用 DevTools Sensors 设置固定位置和 Location unavailable。Playwright 示例验证了授予 geolocation 权限后的成功路径。上线前还应补充:系统定位关闭、企业策略禁止定位、跨域 iframe、用户拒绝权限、地图服务配额不足、日志中是否出现原始经纬度。

#Claude Code #Geolocation #地理位置 #地图 #Web API
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。