背景
在日常开发中,远程协作或处理支付回调等场景,常常需要远程访问我们本地的开发环境进行调试。这时候,一个可靠的内网穿透工具就至关重要。今天我们就来聊聊一个热门且强大的工具——FRP,并分享如何用它实现自定义域名访问本地服务,以及在此过程中可能遇到的“坑”。
FRP 官方资源
FRP 是一个专注于内网穿透的高性能反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。它能够帮助我们将内网服务安全、便捷地通过具有公网 IP 的服务器中转,暴露到公网。
应用架构
先简单介绍下我的项目背景。应用包含三个部分:
- 小程序:技术栈为 UniApp。
- 管理后台:技术栈为 Vue3 + Ant Design Vue。
- 后端API:技术栈为 Spring Boot + Spring Data JPA。
在生产环境的部署架构比较简单,通过 Nginx 代理管理后台和后端API:

用自定义域名远程调用本地服务
我们的目标是使用自己的域名(例如 dev.domain.com)来远程访问本地启动的服务。首先,你需要准备一台具有公网 IP 的服务器,并将你的域名通过 A 记录解析到该服务器的 IP 地址。
接着,根据你的操作系统下载对应版本的 FRP。压缩包内包含服务端(frps)和客户端(frpc)两个核心程序。我的演示环境是:服务器为 CentOS 7 (Linux),本地开发机为 macOS。

配置与启动 FRP 服务端
编辑服务端配置文件 frps.toml:
bindPort = 7000
auth.token = "666"
vhostHTTPPort = 80
webServer.addr = "0.0.0.0"
webServer.port = 7500
# dashboard 用户名密码,可选,默认为空
webServer.user = "admin"
webServer.password = "admin"
使用以下命令启动服务端:
./frps -c frps.toml
配置与启动 FRP 客户端
编辑客户端配置文件 frpc.toml,将 serverAddr 替换为你的服务器公网IP:
# 服务器外网IP
serverAddr = "x.x.x.x"
auth.token = "666"
serverPort = 7000
[[proxies]]
name = “web”
type = “http”
localPort = 3100
customDomains = [“dev.domain.com”]
locations = [“/”]
[[proxies]]
name = “api”
type = “http”
localPort = 8081
customDomains = [“dev.domain.com”]
locations = [“/api”]
使用以下命令启动客户端:
./frpc -c frpc.toml
下图清晰地展示了 FRP 代理本地项目的完整流程:

遇到的第一个“坑”:Vite HMR 导致页面无限刷新
一切配置就绪后,访问 http://dev.domain.com,页面却开始不停刷新。打开开发者工具,发现 Vite 在疯狂地进行 __vite_ping。

问题分析
这是因为本地启动的 Vite 项目默认开启了 HMR(热模块替换)。HMR 的核心是服务端(dev-server)与客户端(浏览器)之间建立一个 WebSocket 连接,用于实时推送代码更新。
WebSocket 的默认连接地址通常是 ws://本地或外网地址:本地端口。例如,本地端口是 3100,直接访问 http://localhost:3100 时,WebSocket 会连接 ws://localhost:3100,一切正常。
但当我们通过 FRP 代理访问 http://dev.domain.com(默认代理到服务器的 80 端口)时,浏览器试图建立的 WebSocket 连接地址变成了 ws://dev.domain.com:3100。然而,FRP 只代理了 80 端口的 HTTP 流量,并未代理 3100 端口的 WebSocket,导致连接失败,进而触发页面不断重试刷新。
解决方案
有两种思路可以解决:
- 保持端口一致:将 FRP 的 HTTP 代理端口也改为 3100。这样访问地址变为
http://dev.domain.com:3100,WebSocket 端口自然匹配。
- 修改 Vite 配置:在
vite.config.js 或 vite.config.ts 中,指定 HMR 的客户端端口为 FRP 代理的端口(如 80)。
server: {
hmr: {
clientPort: 80,
},
},
参考 Vite 官方文档:https://cn.vitejs.dev/config/server-options#server-hmr
第二个“坑”:小程序 HTTPS 与证书链问题
上面的配置基于 HTTP 协议。但某些场景(如微信小程序)要求必须使用 HTTPS。我们可以从云服务商申请免费的 SSL 证书,下载后会得到证书文件。

配置 FRP 支持 HTTPS
首先,修改服务端配置 frps.toml,启用 HTTPS 代理端口:
bindPort = 7000
auth.token = “666”
vhostHTTPPort = 80 # http 代理端口
vhostHTTPSPort = 443 # https 代理端口
然后,配置客户端 frpc_https.toml,使用 https2http 插件将本地的 HTTP 服务包装成 HTTPS:
serverAddr = “x.x.x.x”
serverPort = 7000
auth.token = “666”
[[proxies]]
name = “api”
type = “https”
localPort = 8081
customDomains = [“dev.domain.com”]
[proxies.plugin]
type = “https2http”
localAddr = “127.0.0.1:8081”
crtPath = “./dev.xxxx.com.public.crt”
keyPath = “./dev.xxxx.com.key”
hostHeaderRewrite = “127.0.0.1”
requestHeaders.set.x-from-where = “frp”
启动后,在浏览器访问 https://dev.domain.com,显示连接安全,证书信息也正常,似乎一切完美。

小程序真机报错:证书链不完整
然而,在小程序真机上进行测试时,却收到了错误:
{“errno”:600001,“errMsg”:“request:fail errcode:-202 cronet_error_code:-202 error_msg:net::ERR_CERT_AUTHORITY_INVALID”}
为什么浏览器显示正常,小程序却报错?查看微信小程序官方对HTTPS的要求,其中明确提到了“证书的信任链必需完整”。

我们立刻用专业的 SSL 检测网站(如 myssl.com)进行测试。结果“实锤”了问题根源:证书链不完整,这直接导致安全评级从 A 降到了 B。

修复证书链
在检测报告中,通常会提供“下载证书链”的选项,将完整的证书链文件下载下来。

我们需要将完整的证书链(通常是一个包含中级CA和根CA的 .crt 或 .pem 文件)与之前的服务器证书合并,或者在 FRP 服务端配置中正确指定证书链文件。重启 FRP 服务端后,再次检测,安全评级恢复到了 A。

此时,小程序真机访问也完全正常了。这涉及到 HTTPS 和 TLS 握手的底层机制,对于移动端特别是有着严格安全沙箱的小程序环境,完整的证书信任链是强制要求。
总结
- FRP 工具:包含服务端 (
frps) 和客户端 (frpc),支持 TCP、UDP、HTTP、HTTPS 等多种协议,是解决内网穿透需求的利器。
- Vite 代理注意:使用 FRP 代理 Vite 开发服务器时,务必注意 HMR 的 WebSocket 端口问题,确保本地端口与代理端口一致,或通过
clientPort 参数显式指定。
- 小程序 HTTPS 更严格:相较于浏览器,微信小程序等移动端环境对 HTTPS 证书的校验更为严格,必须保证证书链完整,否则会导致请求失败。这是开发中非常容易忽略的一个关键点。
希望这篇结合真实踩坑经历的 FRP 使用指南能对你有所帮助。如果在部署或网络配置中遇到其他问题,欢迎到 云栈社区 交流讨论。