在 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 登录

相关推荐
BingoGo16 小时前
Laravel + Vue3 前后端分离开源后台管理框架 CatchAdmin v5.0 Beta 发布
后端·php
馬致远16 小时前
Vue TodoList 待办事项小案例(代码版)
前端·javascript·vue.js
程序员鱼皮16 小时前
什么是负载均衡?不就是加台服务器嘛!
java·后端·计算机·程序员·编程经验
闲人编程16 小时前
FastAPI性能优化技巧
后端·python·性能优化·fastapi·性能·codecapsule
岁月宁静16 小时前
FastAPI 入门指南
人工智能·后端·python
加洛斯16 小时前
Spring Task从入门到精通:定时任务开发完整教程
java·后端
用户20554059150516 小时前
嵌入式项目之温湿度闹钟
后端
用户2986985301417 小时前
C# 中如何从 URL 下载 Word 文档:基于 Spire.Doc 的高效解决方案
后端·c#·.net
小飞Coding17 小时前
你写的 equals() 和 hashCode(),正在悄悄吃掉你的数据!
java·后端
一字白首17 小时前
Vue 进阶,Vuex 核心概念 + 项目打包发布配置全解析
前端·javascript·vue.js