手机号与验证码登录
OpenNI 已实现中国大陆手机号登录:获取验证码、六位输入、重发冷却、过期处理、会话与账号注销。真实短信通过腾讯云发送,默认关闭;本地测试模式不会发送短信。完整接口和错误字段见 认证接口合同。
先在本机走通#
在仓库根目录启动隔离测试服务:
API_MODE=demo SMS_PROVIDER=test DATA_DIR=:memory: node services/api/src/server.mjs
把 App 或小程序连接到 http://localhost:8787/api/v1,进入手机号登录,输入测试手机号,再填接口返回的本地验证码。客户端应标明“本地验证码,未发送短信”。验证码每次随机生成,没有固定万能码。测试模式只允许 API_MODE=demo,不能用于生产;内存数据库随进程结束清空。
GET /api/v1/auth/capabilities 会返回可用登录方式。短信未配置时入口会显示不可用,不会创建假身份或假装发送短信。微信登录与短信登录可以分别启用。
测试验证码、手机号账号、重发状态和发送额度会记录验证来源。即使复用数据库和同一个 SMS_HASH_SECRET,测试账号与正式账号也保持独立:测试验证码不能换取正式会话,演示成员角色不会带入正式账号,也不会占用正式成员邀请。建议继续使用上面的独立内存数据库做本地测试。
准备腾讯云资料#
在自己的腾讯云账号中创建短信应用,完成国内短信签名和验证码模板审核,准备应用的 SDK AppID、签名正文、模板 ID、地域与有发送权限的 SecretId / SecretKey。国内短信签名应填写审核通过的文字,不是签名 ID;模板参数个数要与已审核模板一致。查看腾讯云 SendSms 官方文档。
本项目模板支持两种形式:一个变量放验证码;或第一个变量放验证码、第二个变量放有效分钟数 5。不要使用需要其他动态变量的模板。
从品牌配置导出服务器环境#
打开 品牌配置,展开可选的腾讯云短信配置,选择正式环境并填写资料。下载包中的 server.env 仅放服务器,不能放进公开的 club.json、网页、小程序或 Flutter 配置。
新部署可以生成一次 SMS_HASH_SECRET。已有数据库必须沿用原来的值。 它用于计算手机号身份索引和验证码摘要;更换它会让已有手机号无法对应原账号。把它与数据库备份一起安全保管,重新导出、迁移或重建服务器时不要生成新值。当前没有身份密钥轮换迁移工具。
也可手动复制根目录 .env.example 为 .env 并填写:
| 变量 | 填写内容 |
|---|---|
API_MODE |
production |
SMS_PROVIDER |
tencent;默认 disabled |
SMS_HASH_SECRET |
一次生成、长期保管的至少 43 位 base64url 随机值 |
TENCENT_SMS_SECRET_ID / TENCENT_SMS_SECRET_KEY |
服务器专用云 API 凭证 |
TENCENT_SMS_SDK_APP_ID |
短信应用 SDK AppID |
TENCENT_SMS_SIGN_NAME |
审核通过的签名正文 |
TENCENT_SMS_TEMPLATE_ID |
审核通过的验证码模板 ID |
TENCENT_SMS_REGION |
短信服务地域,例如 ap-guangzhou |
TENCENT_SMS_TEMPLATE_HAS_MINUTES |
一个变量填 false;验证码、分钟数两个变量填 true |
SMS_TRUSTED_PROXY_IPS |
可选,可信反向代理的精确 IP,逗号分隔 |
首次生成身份密钥的命令:
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
API 还要求品牌的 demoData=false,填写自己的经营主体、客服、隐私说明与服务协议。然后执行配置检查,再重启服务:
node scripts/doctor.mjs --env .env --mode production
node --env-file=.env services/api/src/server.mjs
配置检查不发送短信,也不验证腾讯云账号余额、模板权限或手机收取结果。自己的受控测试号码收到验证码后,还需验收错误码、重发、过期、重复使用和注销。此次实现只运行模拟供应商测试,没有发送真实短信。
验证码规则#
验证码有效 5 分钟,发送冷却 60 秒,同一挑战最多输错 5 次。按手机号最多每小时 5 次、每天 10 次;按 IP 最多每小时 20 次、每天 100 次。校验接口每个 IP 每 10 分钟最多 60 次。所有额度持久化,重启不会清空。
成功重发会替换之前未使用的验证码;供应商失败保留之前仍有效的码。超时不自动重发,因为供应商可能已经接受请求。真实接口的 delivery=accepted 只表示腾讯云接受请求,不能证明手机已收到。返回 retryAfterSeconds 时按服务器时长等待,不要循环重试。
默认不相信 X-Forwarded-For。若使用反向代理,SMS_TRUSTED_PROXY_IPS 只填写实际受控代理的精确 IP;代理须正确整理来源链。服务器从连接端向前跳过明确可信的节点,取第一个不可信来源。不要填通配符;配置缺失时代理后用户可能共享额度,应在上线前验证。
数据库只保存带服务端密钥的手机号、IP 和验证码摘要,不保存明文验证码或手机号登录索引。手机号仍会传给短信供应商完成发送,经营者应在自己的隐私说明中解释用途与供应商。发送额度记录保留滚动 24 小时,验证码记录过期 24 小时后在后续认证请求中清理;注销立即清除身份关联和该手机号挑战,额度记录仍按期限保留。备份也应遵守经营者的保留规则。
升级已有手机号数据库#
如果旧版本数据库没有记录短信验证来源,升级无法判断其中的手机号是否只用过本地测试码。系统会使这些账号的旧会话和未使用验证码失效,保留原用户、角色、微信关联与订单,不把未标记的手机号自动认作正式身份。
此后用真实验证码验证到这类旧手机号时,会提示“此手机号需要联系客服核实后登录”(SMS_IDENTITY_REVIEW_REQUIRED),不会静默创建新账号而隐藏旧订单。运营方需核实账号归属及成员邀请记录后处理恢复;当前没有自动恢复或合并工具。原有微信身份仍可通过正常微信登录访问原账号。升级前请先备份数据库与原 SMS_HASH_SECRET,不要靠更换密钥或删除账号来绕过核实。
手机账号如何微信付款#
短信登录只证明手机号,不生成虚假 OpenID。用户在订单页明确选择“关联微信并继续支付”后,客户端获取一次新的微信 code,用当前会话请求小程序 /auth/wechat/link 或原生 /auth/wechat-app/link。成功会保留同一个用户、角色和订单,仅增加相应微信身份并换发会话,再继续支付原订单。
微信身份已属于另一个账号时会拒绝关联,不会静默合并订单。一个账号对同一微信应用只关联一个身份;小程序和原生应用分别关联。仅有手机号登录的俱乐部可先验收预约,微信付款仍需要 对应应用和商户配置。新手机号账号只能是客户;成员角色仍由管理员邀请码授予。
常见问题#
| 提示 | 处理 |
|---|---|
| 短信登录尚未配置 | 检查 SMS_PROVIDER 与完整参数,重启 API |
| 俱乐部尚未完成资料 | 应用真实品牌与政策配置,确认 demoData=false |
| 请求未获确认 | 检查腾讯云签名、模板和额度;等待冷却,不连续点击 |
| 验证码过期或已被替换 | 使用最近一次获取的码;必要时等待后重新获取 |
| 尝试次数已用完 | 当前挑战已锁定,按提示等待后重取 |
| 此微信已关联其他账户 | 联系俱乐部客服,或明确切换账号;系统不会自动合并 |
| 此手机号需要联系客服核实后登录 | 联系俱乐部核实旧账号归属;不要反复获取验证码或改用新账号覆盖订单 |
供应商请求使用固定 HTTPS 地址与 TC3 签名方法,不自动重试;密钥和供应商原始错误不会返回客户端。手机验证码不能消除转售号码或 SIM 卡被接管的风险,高权限运营入口仍使用独立管理员凭证。