不知道多少人被天地图的403折磨过。本地开发跑得好好的,一部署到服务器,地图图片全部加载不出来,控制台红彤彤一片403 Forbidden,看着就头大。我这次在Vue3项目里接天地图,从"本地联调没问题"到"服务器全挂",再到最后定位到Referer白名单校验,整个排查链路走下来花了不少时间。这篇就把天地图API返回403的成因、排查路径、解决方案,以及部署到服务器后的各种环境差异问题一次讲完,做前端、做全栈、或者负责运维部署的朋友都能直接用。
先说个结论:天地图返回403,绝大部分情况不是你的代码写错了,而是你的请求在天地图服务端那里"身份核查"没通过。理解这一点,排查方向就对了。
1. 403是谁发出来的:天地图服务端的鉴权机制
1.1 先把状态码含义搞对
很多人一看到403就开始怀疑是不是key过期了、是不是跨域了、是不是要被封了,其实这些猜测大多数方向都不对。HTTP状态码里,401是说"你没带凭证或者凭证根本不存在",而403是"凭证带了,但我核对之后决定不让你过"。这两个含义差得非常远,如果混着排查,你会一直去重新申请key、重新配白名单,但问题并不在那一层。
天地图返回403,至少说明一个关键事实:你的请求确实到达了天地图的服务器。它不是被自己的Nginx拦截了,也不是被本地防火墙吞了,而是天地图接收请求后主动拒绝的。这个信息非常值钱,它直接把排查范围缩小到了"请求参数"和"来源合法性"这两个方向。
1.2 密钥与应用类型的区别
天地图的API鉴权靠的是两层校验:一个是你的密钥tk,另一个是调用来源的合法性。
在天地图控制台申请应用后,你会拿到一个几十位的字符串,这就是tk。所有请求都要带上它。但光有tk还不够,天地图的密钥分两种应用类型:
- 浏览器端应用:面向网页场景,key绑定的是域名白名单。天地图服务端会读请求头里的
Referer字段,判断这个请求是不是从白名单内的域名发出的。 - 服务端应用:面向后端服务器调用,key绑定的是服务器出口IP白名单。这种模式不校验Referer,但会校验请求来源IP是否在白名单里。
结合Vue3项目最常见的错误形态来看,逻辑就非常清晰:你本地跑npm run dev,页面地址是http://localhost:5173,申请key时顺手把localhost:5173加进了白名单,于是联调一切正常。部署后页面地址变成https://map.company.com,Referer自然跟着变,如果控制台里没把新域名加进去,结果就是403 Forbidden。
1.3 鉴权链路:token + 来源校验
把天地图的鉴权流程理解成进小区就很好记了:
tk相当于你的门禁卡,门禁卡本身要有记录、要在有效期内。- Referer相当于门禁系统记录的访客来源楼栋,门禁系统得知道你这栋楼是允许进入的。
- 门禁卡刷了,但系统发现你来路不明,照样锁死。
所以天地图服务端收到请求后,实际上是连续做了两道检查:先看tk参数是否存在、是否有效,然后再看请求源头是否在白名单内。任何一道不过,返回的都是403。
这个机制也解释了为什么"本地能跑、线上403"和"线上正常、本地403"这两种情况同时存在——它们分别对应不同的白名单配置,域名对不上号就进不去。
2. 头号原因:referer白名单与域名绑定
2.1 为什么本地一跑就通、部署就403
这是我在开发中被问得最多的问题。如果你用的是天地图的浏览器端key,那九成就是Referer白名单的问题。
天地图控制台在创建"浏览器端"应用时,会让你填写域名白名单。服务端校验时,会拿请求头里的Referer字段去比对。本地开发时你填的大概率是http://localhost:5173或http://localhost,这个地址和本地实际发出的保持一致,所以通过。一旦部署,页面发起请求的Referer变成https://你的域名,白名单里没有这个域名,403就来了。
还有一个容易踩的坑:团队在测试环境配置过域名,上线时用同一套key,新域名没有新增进去,于是线上403。我个人的习惯是每个环境用独立的key,测试域名和生产域名分开配置,这样改来改去互不影响,也方便控制台看调用量统计时区分来源。
2.2 在天地图控制台正确配置白名单
登录天地图控制台后,大致路径是:进入应用开发列表,找到对应应用,点编辑,在允许访问的域名配置区域添加新的域名。
填写时注意下面几点:
- 域名尽量写完整。推荐直接填写完整的参考来源,比如
https://map.example.com,带上协议、域名、端口号。如果本地是http://localhost:5173,就完整写http://localhost:5173,不要偷懒只写localhost,否则校验可能不通过。 - 端口号不要漏。很多本地服务用的不是80端口,开发服务器一般是5173、8080之类,这些端口号都要写进去。
- 通配符酌情使用。天地图控制台支持
*.example.com这类泛域名写法,但如果你的域名是固定的,还是建议写具体域名,减少误伤面。 - 改完多久生效。正常是立即生效,但浏览器端在极端情况下可能有缓存残留。如果确认配了还是403,先强制刷新页面再验证,或换个隐身窗口测试。
2.3 验证Referer到底发没发出去
配置了白名单但还是403,就要确认浏览器到底有没有把Referer带过去。
最直接的办法是打开开发者工具,切到Network面板,刷新页面,找到天地图瓦片或API的请求,查看请求头里的Referer值。正常情况下,你应该能看到类似https://map.example.com/这样的字段。
如果你看到的Referer是空的,那问题就不在白名单,而在页面的Referrer Policy设置。有些工程会全局加<meta name="referrer" content="no-referrer">,这会导致所有请求都不带来源信息,天地图无法判断你的页面来自哪个域名,直接403。还有HTTPS页面请求HTTP资源时浏览器默认不发送Referer,但天地图本身是HTTPS,一般不会触发。
如果动态创建script加载天地图,可以给script设置referrerPolicy="origin",保证来源信息不会丢失:
const script = document.createElement('script') script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${tk}` script.referrerPolicy = 'origin' script.onload = () => { initMap() } document.head.appendChild(script)3. Vue3项目接入天地图的完整流程
3.1 动态加载JS API
Vue3项目里接入天地图,我一般不在index.html里直接写死<script>标签,而是在用到地图的组件里动态加载。好处是路由懒加载的页面不会一进应用就下载地图脚本,首屏压力小,代码层面就是个Promise封装:
// src/utils/tianditu.js let loadingPromise = null export function loadTianditu(tk) { if (window.T) { return Promise.resolve(window.T) } if (!loadingPromise) { loadingPromise = new Promise((resolve, reject) => { const script = document.createElement('script') script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${tk}` script.referrerPolicy = 'origin' script.onload = () => resolve(window.T) script.onerror = () => { loadingPromise = null reject(new Error('天地图API加载失败')) } document.head.appendChild(script) }) } return loadingPromise }这里把loadingPromise缓存起来,是为了防止多个组件同时调用时重复创建多个script标签。虽然是小事,但地图组件在Vue3里经常被复用,踩过一次重复加载的坑之后我就学乖了。
3.2 创建地图实例
加载完成后,用全局对象T创建地图:
<template> <div id="mapContainer" class="map-box"></div> </template> <script setup> import { onMounted, onBeforeUnmount } from 'vue' import { loadTianditu } from '@/utils/tianditu' const tk = import.meta.env.VITE_TIANDITU_TK let map = null onMounted(async () => { const T = await loadTianditu(tk) map = new T.Map('mapContainer', { projection: 'EPSG:4326', center: new T.LngLat(116.407, 39.904), zoom: 12 }) const layer = new T.TileLayer({ urlTemplate: 'https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}' }) map.addLayer(layer) }) onBeforeUnmount(() => { if (map) { map.clearAll() map = null } }) </script>注意,这段代码的瓦片urlTemplate里没有拼tk,原因是我在加载脚本时已经带上了key,天地图JS API会把密钥自动透传到瓦片请求上。如果你自己封装瓦片地址,也可以手动加&tk=你的key,但容易遇到key拼接错误、参数被覆盖之类的问题,优先复用官方机制就好。
3.3 开发环境的跨域代理
天地图的瓦片加载走的是图片或Canvas请求,不存在跨域限制。但如果你调用了天地图的纯数据接口,比如地理编码、逆地理编码,直接从浏览器fetch是会有跨域问题的。本地开发时用Vite代理解决:
// vite.config.js export default defineConfig({ server: { proxy: { '/tianditu': { target: 'https://api.tianditu.gov.cn', changeOrigin: true, rewrite: (path) => path.replace(/^\/tianditu/, '') } } } })请求写成fetch('/tianditu/geocoder?ds=xxx'),开发环境会自动转发到https://api.tianditu.gov.cn,跨域问题就绕过去了。
3.4 多环境key管理
我把天地图的key放在环境变量里,而不是硬编码在组件代码中:
# .env.development VITE_TIANDITU_TK=开发环境的key # .env.production VITE_TIANDITU_TK=生产环境的key这样同一套仓库代码,不同环境构建时取到的tk不同,配合不同环境配置的白名单,能省掉大量"环境切换导致403"的排查时间。这个实践推广到任何第三方服务的密钥管理都适用,不止天地图一个场景。
4. 部署到服务器后的Nginx与代理排查
4.1 先在浏览器里确认请求有没有发出去
部署之后遇到403,不要急着改Nginx,先在浏览器Network面板里确认两件事:
- 请求URL是否完整,
tk参数是否正常出现在请求里; - Referer字段是否是当前线上域名;
- 请求是否被重定向过,重定向后Referer有可能变化。
这一步的核心是把问题分类:如果请求根本没发出去,问题在你的域名解析、防火墙、代理层;如果请求已经发出去但返回403,问题在天地图侧的鉴权参数。很多人在这一层没做区分,就盲目去改服务配置,结果越改越乱。
4.2 Nginx反向代理时要透传Referer
如果你的页面通过Nginx反向代理出去,这里有一个容易忽略的细节:默认情况下Nginx会透传原始HTTP请求头,但如果你手动配置了proxy_set_header块,就很可能把Referer覆盖或抹掉。排查时确认配置里有这一行:
location / { proxy_set_header Host $http_host; proxy_set_header Referer $http_referer; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://你的后端地址; }$http_referer就是原始请求头里的Referer值。有这一行,天地图才能拿到页面的真实来源。如果没有,或者有人图省事写死了一个:proxy_set_header Referer http://localhost;`,线上不403才怪。
4.3 服务器访问公网受限怎么看
还有一种403跟天地图完全无关,出现在服务器访问不了公网的场景。部分内网部署环境的服务器出口受限,访问api.tianditu.gov.cn会被安全组或防火墙拦截。这种情况下,你本机浏览器访问是正常的,但服务器上的后端请求、或者某些需要服务端发起校验的环节,就会一直表现成403。
判断方法很简单,直接在服务器上执行:
curl -sS -o /dev/null -w "%{http_code}" "https://api.tianditu.gov.cn/api?v=4.0&tk=你的key"返回200说明网络通,返回403或超时说明不是被拦截就是白名单没配。这一条能快速区分网络层和鉴权层的问题,省得在两边反复横跳。
4.4 让服务器日志开口说话
排查的时候别只盯浏览器Network,还要看服务器日志。Nginx默认日志路径常见的是/var/log/nginx/access.log,可以实时观察:
tail -f /var/log/nginx/access.log | grep tianditu这样你能看到每个天地图相关请求的状态码、耗时、来源Referer。有时候浏览器显示403,但后端日志里显示500或502,这说明问题根本不在天地图,而在你自己的服务链路。前端的一个报错信息,不一定代表后端也是同一个错误类型。
5. 服务端中转:最稳妥的兜底方案
如果你的项目环境没法控制Referer,比如微信小程序、uni-app、桌面客户端、以及一些奇怪的WebView容器,浏览器端key的鉴权方式会变得不可控。这时候最稳的方案就是把天地图的调用挪到服务端,由你的后端统一向天地图请求,前端只与自己的后端交互。
5.1 申请服务端key并绑定IP白名单
在天地图控制台创建应用时,应用类型选择"服务端",填写服务器的公网出口IP。这里有一个容易错的点:必须填准确的出口IP,不是内网IP,也不是负载均衡的虚拟IP。可以让服务器自己查一下:
curl ifconfig.me如果服务器有多个出口IP,或者部署在多地域,把所有出口IP都加进去。服务端key的鉴权就看请求来源IP在不在白名单内,漏一个就403一个。
5.2 Node.js中间层转发示例
这里给一个Express的简化实现,实际项目中可以封装到你的网关服务里:
const express = require('express') const axios = require('axios') const router = express.Router() const TIANDITU_BASE = 'https://api.tianditu.gov.cn' const SERVER_TK = process.env.TIANDITU_SERVER_TK router.get('/proxy/tianditu/:path(*)', async (req, res) => { try { const targetUrl = `${TIANDITU_BASE}/${req.params.path}?${new URLSearchParams({ ...req.query, tk: SERVER_TK }).toString()}` const response = await axios.get(targetUrl, { responseType: 'arraybuffer', headers: { 'User-Agent': 'YourAppName/1.0' } }) res.set('Content-Type', response.headers['content-type']) res.set('Cache-Control', 'public, max-age=86400') res.send(Buffer.from(response.data)) } catch (err) { if (err.response) { res.status(err.response.status).send(err.response.statusText) } else { res.status(502).send('Bad Gateway') } } }) module.exports = router这里有个细节:服务端请求不需要Referer,但最好带上一个定制User-Agent,方便在天地图控制台排查错误日志时定位到自己的应用。我还加了Cache-Control,让瓦片能被浏览器HTTP缓存命中,地图拖拽体验会好很多。生产环境还可以加一层Redis缓存,把高频瓦片缓存到本地,减少对天地图的直接调用量。
5.3 前端调用方式调整
Vue3前端就不需要再暴露天地图的key了,请求指向自己的服务:
const layer = new T.TileLayer({ urlTemplate: '/proxy/tianditu/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}' })如果你的后端不是Node.js,用Java、Go、Python写都没问题,核心思路一致:key放服务端,请求从服务端发出,IP白名单负责鉴权。
6. 高频翻车点清单与快速定位技巧
最后把实际项目中踩过、见过的翻车点整理成速查表,方便遇到403时对照排查。很多问题表现形式都是403,但成因完全不同。
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 本地正常,线上403 | Referer白名单没加线上域名 | 控制台添加线上完整域名 |
| 本地也403 | tk不正确或应用未生效 | 核对tk,确认应用状态 |
| Referer显示为空 | 页面设置了no-referrer策略 | 调整referrer policy |
| file://直接打开403 | 无Referer或来源为null | 用本地服务启动页面 |
| 服务端调用403 | 出口IP不在白名单 | 申请服务端key并添加IP |
| 环境切换后403 | 同一key访问多个域名 | 每个环境独立key |
| 隐私模式/插件拦截 | 扩展程序改写了Referer | 换普通窗口验证 |
| 偶发403,刷新后正常 | 浏览器缓存了旧请求 | 加版本号参数,强制刷新 |
6.1 用curl快速复现403
遇到403先不要反复刷新页面,用curl发一个请求是剥离浏览器环境最快的方式:
curl -sS -o /dev/null -w "%{http_code}" \ -H "Referer: https://你的域名/" \ "https://api.tianditu.gov.cn/api?v=4.0&tk=你的key"返回200说明天地图侧校验没问题,问题在浏览器行为或页面网络环境。返回403说明key、Referer白名单、IP三者中至少有一项不对,换成白名单里的域名再试,逐步二分定位。
6.2 容易被缓存干扰的坑
配置了白名单并确认无误后还是偶发403,可以怀疑浏览器缓存了旧的天地图脚本或瓦片。强制刷新一次,或者给请求加版本号参数,例如JS API地址加&_t=时间戳,能减少这类缓存干扰。实际排查中这个坑不容易想到,但确实会让人误判问题仍然存在。
6.3 天地图服务自身的可用性
还有一种情况:你的配置全对,但天地图侧因为自身维护或调用量配额用尽返回异常。可以去天地图开放平台官网看看服务公告,或者到控制台的调用量统计里看当天请求曲线。如果控制台显示的调用量已经到顶,那就是配额问题,需要申请调整配额或临时换一个key过渡。
经过这么一轮排查,绝大多数天地图403都能定位到根因:要么是Referer白名单没配全,要么是服务端IP白名单不对,要么是请求在代理层把来源信息弄丢了。我自己现在的习惯是,不管多熟悉的流程,每次新项目接入天地图,都先从第一步就区分清楚应用类型——前端页面就用浏览器端key搞域名白名单,服务端调用就用服务端key配IP白名单。这个决定会贯穿整个项目后续的每次部署。
最后再分享一个调试小技巧:排查天地图403时,把要验证的域名逐个用curl加上Referer跑一遍,返回200的直接放行,返回403的立刻回控制台对照白名单逐字比对。这个方法具体、快速,比在浏览器里一次次刷新乱猜高效得多,建议收藏备用。