在 Expo 中实现 Azure SMS-OTP 登录

手机号 + 一次性验证码(OTP)是移动端最友好的登录方式。本文示范如何仅用 Expo + Better Auth + Azure Communication Services (ACS) 在 30 分钟内完成:

  1. 购买并绑定支持 SMS 的 Azure 电话号码
  2. 在 Clinic Dashboard 暴露一个 /api/sms 端点,负责发送 OTP
  3. 在 Expo 客户端通过 Better Auth 的 Phone Number 插件完成登录
  4. 登录成功后把患者信息持久化到 PostgreSQL

整体架构

markdown 复制代码
┌────────────┐     HTTPS      ┌─────────────────┐
│ Expo App   │◄──────────────►│ Clinic Dashboard│
│            │                │  Node/Next.js   │
└────▲───────┘                └──────┬──────────┘
     │ SMS OTP                       │
     │                               │ ACS SDK
     │                               ▼
     │                    ┌──────────────────────┐
     │                    │ Azure Comm. Services │
     │                    │   SMS Gateway        │
     └────────────────────┴──────────────────────┘

步骤 1:购买 Azure SMS 电话号码

  1. 登录 Azure Portal
  2. Communication Services → 电话号码 → 获取
  3. 选择:
    • 国家/地区(与账单地址匹配)
    • 类型:Local 或 Toll-Free
    • 功能:✅ 发送短信
  4. 搜索 → 添加到购物车 → 立即购买
  5. 几分钟后状态为 已预配 ,记下号码 +1xxxxxxxxxx

步骤 2:Clinic Dashboard 暴露 /api/sms

安装依赖

bash 复制代码
npm i @azure/communication-sms dotenv

.env

ini 复制代码
AZURE_CONNECTION_STRING=endpoint=https://xxx.communication.azure.com/;accesskey=xxx
AZURE_SMS_FROM=+15551234567   # 刚买的号码

/pages/api/sms.ts(Next.js 13 App Router 同理)

ts 复制代码
import { SmsClient } from "@azure/communication-sms";
import "dotenv/config";

const sms = new SmsClient(process.env.AZURE_CONNECTION_STRING);

export default async function handler(req, res) {
  if (req.method !== "POST") return res.status(405).end();

  const { phone, code } = req.body;
  if (!phone || !code) return res.status(400).json({ error: "missing fields" });

  await sms.send({
    from: process.env.AZURE_SMS_FROM,
    to: [`+${phone}`],
    message: `Your verification code is ${code}`,
  });

  res.json({ success: true });
}

部署后得到 HTTPS 端点 https://api.clinic.com/api/sms


步骤 3:Expo 端集成 Better Auth

安装依赖

bash 复制代码
npm i better-auth @better-auth/expo
npx expo install expo-secure-store expo-web-browser

auth.ts

ts 复制代码
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo";

export const authClient = createAuthClient({
  baseURL: "https://api.clinic.com",
  plugins: [expoClient()],
});

步骤 4:Better Auth 服务端配置

安装

bash 复制代码
npm i better-auth drizzle-orm pg

better-auth.ts

ts 复制代码
import { betterAuth } from "better-auth";
import { phoneNumber } from "better-auth/plugins";

export const auth = betterAuth({
  database: { provider: "pg", url: process.env.DATABASE_URL! },
  plugins: [
    phoneNumber({
      sendOTP: async ({ phone, code }) => {
        await fetch("https://api.clinic.com/api/sms", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ phone, code }),
        });
      },
    }),
  ],
});

步骤 5:Expo 登录界面示例

ts 复制代码
import { useState } from "react";
import { View, TextInput, Button, Alert } from "react-native";
import { authClient } from "./auth";

export default function LoginScreen() {
  const [phone, setPhone] = useState("");
  const [otp, setOtp] = useState("");

  const send = async () => {
    const res = await authClient.phoneNumber.sendOtp({ phone });
    res.error && Alert.alert("Error", res.error.message);
  };

  const verify = async () => {
    const res = await authClient.phoneNumber.verifyOtp({ phone, code: otp });
    if (!res.error) {
      // 成功:跳转到主界面
    } else {
      Alert.alert("Error", res.error.message);
    }
  };

  return (
    <View>
      <TextInput
        placeholder="+1234567890"
        value={phone}
        onChangeText={setPhone}
      />
      <Button title="Send OTP" onPress={send} />
      <TextInput
        placeholder="6-digit code"
        value={otp}
        onChangeText={setOtp}
        keyboardType="numeric"
      />
      <Button title="Verify" onPress={verify} />
    </View>
  );
}

八、持久化患者信息

verifyOtp 时可携带附加字段:

ts 复制代码
await authClient.phoneNumber.verifyOtp({
  phone,
  code: otp,
  additionalFields: {
    name: "Alice Smith",
    dob: "1990-01-01",
    role: "patient",
  },
});

Better Auth 会自动写入 users.meta 或自定义 patients 表。


九、本地开发小技巧

• 用 npx ngrok http 3000 暴露后端,替换 baseURL

• 开发构建:
npx expo run:iosnpx expo run:android(Expo Go 不支持自定义 scheme)


十、安全与生产建议

  1. OTP 存入 Redis,TTL 5 分钟,验证后删除
  2. /api/sms 做速率限制:同一手机号 60 秒 1 次
  3. 切勿把 Azure Connection String 暴露到客户端
  4. 可选:启用 ACS Number Lookup API 预先验证号码是否可达

十一、一分钟速查清单

任务 状态
Azure 订阅已付费
已购买支持 SMS 的号码
/api/sms 部署并 HTTPS
Better Auth server 已配置
Expo 客户端集成完成

十二、参考链接

Better Auth Phone Number Docs

Azure Communication Services SMS Quickstart

ACS Number Lookup (Preview)


至此,一个可上线、可扩展的 Azure SMS-OTP 登录体系便搭建完毕。祝开发顺利!# 在 Expo 中实现 Azure SMS-OTP 登录

相关推荐
想用offer打牌5 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX6 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法7 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端