30分钟用闲置安卓手机搭建私有短信网关

42 次阅读 0 点赞 0 评论 13 分钟原创技术教程

本教程演示如何用一台闲置安卓手机和 httpSMS 快速搭建零成本私有短信服务。通过 HTTP API 发送验证码、配置 Webhook 接收短信回调,完整覆盖从手机配置到 Spring Boot 集成的全流程。无需备案、无需第三方平台,适合个人项目、MVP 开发和物联网告警场景。

#短信网关 #Android #HTTP API #Spring Boot #开源工具 #后端开发
30分钟用闲置安卓手机搭建私有短信网关

30分钟用闲置安卓手机搭建私有短信网关

做后端开发的应该都遇到过这种场景:系统需要给用户发短信验证码、物联网设备异常需要短信告警、或者你想在服务器上跑个脚本给自己发个通知。找第三方 SMS 服务吧,国内要备案、国外要绑信用卡,按条收费还不便宜;自己搞短信猫硬件吧,成本高、维护麻烦。

有没有一种办法,用一台闲置的安卓手机就能搞定?

今天带你实操开源项目 httpsms,把 Android 手机变成 SMS 网关。只需调用 HTTP API 即可发短信,手机收到短信还能通过 Webhook 回调到服务器。整条链路跑通后,你就拥有了一套零成本的私有短信服务。

前置准备

  • 一台闲置或主力 Android 手机(Android 5.0+,能插 SIM 卡即可)
  • 一张有短信功能的 SIM 卡(预存话费,国内短信几分钱一条)
  • 一个后端项目(本教程用 Java/Spring Boot 演示)
  • 公网域名或内网穿透工具(如 ngrok,用于 Webhook 回调)

整篇教程以「使用官方托管服务」为主线,无需自部署服务端,10 分钟即可跑通。

第一步:注册并获取 API Key

打开 httpsms.com 注册账号,进入 Settings 页面找到 API Key。这个 Key 是调用所有 API 的身份凭证,等同于云服务的 AccessKey,务必妥善保管。

httpSMS 的后端 API 是无状态的,通过 API Key 识别用户并定位绑定的手机设备。一个 Key 对应一个用户下的所有关联手机。

第二步:安装并配置 Android 端

这是唯一需要手动操作的硬件环节。

  1. 官方发布页官网直达 下载 APK 并安装。
  2. 打开 App,用注册账号登录。
  3. 授权以下权限:
    • 短信权限:读取发送状态回执、拦截收到的短信进行转发
    • 通知权限:接收推送通知,触发发送行为
    • 电池优化白名单:确保后台不被系统杀死

踩坑提醒: 国产 ROM(MIUI、ColorOS、HarmonyOS 等)默认对后台应用做激进的进程管理。装完后务必到手机设置 → 电池 → 应用启动管理,将 httpSMS 设为「允许后台运行」和「自启动」,否则会出现消息发不出去或回调丢失。

App 登录成功后显示设备在线标识,此时手机已与账户绑定,API 端可以开始调度。

第三步:发送第一条短信

httpSMS 的 API 非常干净,核心就一个发送端点。用 curl 快速测试:

bash 复制代码
curl -X POST "https://api.httpsms.com/v1/messages/send" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "这是一条来自 httpSMS 的测试短信",
    "from": "+8613800138000",
    "to": "+8613900139000"
  }'

替换 YOUR_API_KEY 为你在 Settings 里看到的 Key,from 填你手机上的号码,to 填接收方号码。请求发出后会收到 202 Accepted 响应,说明请求已被接收,手机稍后会把短信实际发出。

为什么返回 202 而不是 200? httpSMS 采用异步架构:API 收到请求后先返回接受状态,再通过 Firebase Push 通知安卓手机,手机收到通知后从 API 拉取消息内容,最后通过 Android 原生 SMS API 发送。这种设计解耦了 API 层和设备层,手机暂时离线也不会阻塞服务端请求。

第四步:实战 —— 用 Spring Boot 搭建短信验证码服务

4.1 项目依赖

Spring Boot 项目引入 WebClient 即可,无需额外 SDK:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

4.2 短信发送服务

java 复制代码
@Service
public class HttpSmsService {

    private static final String API_BASE = "https://api.httpsms.com/v1";
    
    private final WebClient webClient;
    
    @Value("${httpsms.api-key}")
    private String apiKey;
    
    @Value("${httpsms.phone-number}")
    private String phoneNumber; // +8613800138000

    public HttpSmsService(WebClient.Builder builder) {
        this.webClient = builder
            .baseUrl(API_BASE)
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader("x-api-key", apiKey)
            .build();
    }

    /**
     * 发送短信验证码
     */
    public Mono<Void> sendVerificationCode(String targetPhone, String code) {
        String content = String.format("【你的应用】您的验证码是 %s,请在 5 分钟内完成验证。", code);
        
        Map<String, String> body = Map.of(
            "content", content,
            "from", phoneNumber,
            "to", targetPhone
        );

        return webClient.post()
            .uri("/messages/send")
            .bodyValue(body)
            .retrieve()
            .bodyToMono(JsonNode.class)
            .doOnSuccess(r -> log.info("短信已提交,目标: {}, 状态: {}", targetPhone, r))
            .then();
    }
}

4.3 Controller 调用

java 复制代码
@RestController
@RequestMapping("/api")
public class UserController {

    private final HttpSmsService smsService;
    private final StringRedisTemplate redisTemplate;

    // 省略构造器注入...

    @PostMapping("/register/send-code")
    public ResponseEntity<?> sendCode(@RequestParam String phone) {
        String code = String.valueOf(ThreadLocalRandom.current().nextInt(100000, 999999));
        // 验证码存入 Redis,5 分钟过期
        redisTemplate.opsForValue().set("sms:code:" + phone, code, 5, TimeUnit.MINUTES);

        smsService.sendVerificationCode(phone, code)
            .subscribe(); // 异步发送,不阻塞接口响应

        return ResponseEntity.ok(Map.of("message", "验证码已发送"));
    }
}

application.yml 配置 Key 和手机号:

yaml 复制代码
httpsms:
  api-key: "YOUR_API_KEY_HERE"
  phone-number: "+8613800138000"

调用 /api/register/send-code?phone=+86150xxxx1234,闲置手机就会把验证码短信发出。整个过程完全走自己的线路,不依赖第三方短信平台。

第五步:接收短信 —— Webhook 回调处理

手机收到的短信可通过 Webhook 转发到服务器,适用于「用户上行短信触发业务逻辑」场景,如回复 TD 退订、回复关键词触发查询等。

5.1 配置 Webhook

httpsms.com 的 Settings 页面找到 Webhook URL,填入服务端回调地址:

复制代码
https://your-domain.com/api/sms/callback

本地开发可用 ngrok 做临时公网隧道:ngrok http 8080,然后把生成的公网地址填进去。

5.2 回调处理代码

java 复制代码
@PostMapping("/sms/callback")
public ResponseEntity<?> handleSmsWebhook(@RequestBody Map<String, Object> payload) {
    log.info("收到短信回调: {}", payload);
    
    String from = (String) payload.get("from");
    String content = (String) payload.get("content");
    
    if ("TD".equalsIgnoreCase(content.trim())) {
        log.info("用户 {} 请求退订", from);
        // 执行业务逻辑...
    }
    
    return ResponseEntity.ok(Map.of("status", "received"));
}

手机收到新短信时,httpSMS 会第一时间 POST 到这个地址,payload 包含 fromtocontent 等字段。确保回调接口能正常返回 200 状态码即可。

重试机制: 回调返回非 2xx 状态码时,httpSMS 会重试。建议回调接口做好幂等性处理。

常见问题与踩坑提醒

  1. 消息发不出去/长时间 pending

    • 查 App 内日志,确认消息状态是 sentpending 还是 expired
    • 确认手机网络畅通(需能收到 Firebase Push 通知)
    • 检查电池优化/自启动权限是否已放开
  2. Webhook 没有触发

    • 确认 Webhook URL 公网可达(本地用 ngrok)
    • 确认 URL 返回 200,4xx/5xx 会触发重试,重试满后放弃
    • Settings 里可查看最近的 Webhook 投递记录
  3. 长短信处理

    • Android 原生 SMS 单条短信有字数限制(约 70 字符中文),超长会自动拆分。httpSMS 发送时自动处理拆分,建议控制单条内容在合理范围内以控制成本。
  4. 费用说明

    • httpSMS 官方托管服务有免费额度,超出后按用量付费,具体见官网定价。
    • 自部署方案参考 官方文档 的 Docker 部署部分。

总结

用一台安卓手机 + httpSMS,即可从零搭建完整的私有短信服务:

  1. 注册账号,获取 API Key
  2. 手机安装 App、登录、放开后台权限
  3. 调用 /v1/messages/send 发短信
  4. 配置 Webhook URL 接收上行短信

方案没有复杂中间件,无需备案,无需对接运营商 API,拿来就能用。适合个人项目、MVP 产品、内部通知、物联网告警等场景。

下一步可以尝试:

  • 使用官方提供的 Go SDKNode SDK
  • Webhook 回调结合自动化框架:收到短信 → 触发 CI/CD → 执行部署
  • 了解 httpSMS 的速率限制(Back Pressure)消息过期功能,在生产环境中更稳地控制发送节奏

有问题欢迎在评论区交流,或到仓库提 issue。

最后更新:2026-09-09T10:02:38

评论 (0)

发表评论

blog.comments.form.loading
0/500
加载评论中...