做iOS开发的老哥十有八九都接过这种需求App注册页面要加手机号验证码登录或者找回密码必须走短信验证再不然就是运营那边提了个需求要给用户发通知短信。短信接口这东西技术门槛不算高但坑是真的多签名审核、模板报错、真机收不到、审核被拒哪一个都能让你折腾一整天。这篇文章我把自己这些年用Objective-C对接短信接口、在iOS项目里做短信验证码和高性能集成的经验完整梳理一遍覆盖方案选型、SDK配置、代码实现、线程优化、防刷、抓包调试一直到上架审核目标是让正在做objective-c短信接口开发对接的同行们拿到文章就能直接照做少踩几个我当年踩过的雷。不管你是刚入行的iOS开发还是接手了老项目需要补短信功能的同学文章里我会尽量把每一步的为什么也讲清楚。不是说照着抄就行而是你抄完之后心里对这套链路是通透的。1. 动手前先想清楚iOS短信功能到底要集成到什么程度很多朋友一上来就急着找SDK、写代码结果做到一半发现需求理解错了或者选了不合适的方案只好推倒重来。我在项目里吃了这个亏所以现在每次接到短信相关的需求都会先花半小时把需求和边界理清楚。1.1 iOS项目里最常见的三类短信需求第一类是验证码短信。注册、登录、改密码、绑定手机号都需要向用户手机发一条验证码用户填对之后才允许继续操作。这类需求对时效性要求最高验证码一般5到10分钟过期而且必须防刷。第二类是通知短信。比如订单状态变化、物流提醒、会议通知属于事务性消息用户操作触发或者后台任务触发。这类短信对实时性要求略低一点但对稳定性和内容准确性要求高模板固定不能随意拼接内容。第三类是营销短信也就是运营推送的优惠活动、上新通知。这类短信在iOS上最容易翻车因为平台对UGC内容、营销内容管控很严短信服务商对营销模板的审核也更严格而且用户投诉一多签名和模板可能直接被封。看清楚需求区别很重要因为三类短信在接口选择、服务商配置、代码结构上有不小的差异。尤其是营销短信如果你在App里没有做用户主动订阅的确认流程审核阶段被拒的概率非常大这块我后面会专门讲。1.2 方案选型第三方SDK还是服务器直连短信网关这是第一个关键决策点。市面上的做法大致分两条路第二条路App只负责发起请求真正的短信发送由你自己的业务服务器完成。App调用你自己后端的发送接口后端拿用户的手机号去调用短信服务商的OpenAPI比如阿里云短信、腾讯云短信的HTTP接口服务商再向用户手机下发短信。App端不需要集成短信服务商的SDK只需要在自己的工程里发一个HTTP请求就行。第一条路在App里直接集成第三方短信SDK。比如MobTech的SMSSDK这类SDK把发送验证码、校验验证码的流程封装好了你只需要初始化调用一个方法SDK自己去请求服务商下发短信。这条路对客户端开发者最省事因为不需要自己写HTTP层也不需要维护后端的短信发送逻辑。这时候肯定有人问到底选哪条我个人的建议是只要你的App有后端就优先走服务器直连。原因很简单短信服务商的AccessKey和AppSecret属于核心凭据一旦放在App客户端里反编译就能被拿到。而第三方SDK虽然把包封装了一层但核心密钥还是存在于SDK包体内的这属于一种便利换安全的取舍。如果你的App只是个Demo或者确实没有自己后端那用第三方SDK快速验证流程没问题。如果是商业级产品还是把短信逻辑收敛到服务端更稳妥。1.3 我为什么最终放弃了纯客户端SDK方案我在一个电商类项目里做过一次完整的技术选型。最开始图省事直接用第三方短信SDKApp拿到验证码后直接SDK校验流程是通了结果上线没两周就遇到两件头疼的事。第一件事是审核风险。部分短信SDK为了提升验证码到达率会申请一些特殊权限或者内部集成了热更新组件这在App Store审核时属于高危信号。虽然不一定必拒但审核的不确定性大增我这个项目就因为这个被驳回了一次理由还比较模糊。第二件事是统计和审计困难。短信业务是强合规业务每次发送的通道、模板、原因都需要记录。SDK在客户端直接发送日志全在用户的手机上后端拿不到完整日志出了问题根本没法定责。后来我按后端直连的方式重构一遍把每次发送都记录到服务端的日志表连短信服务商返回的messageId都存下来排查问题效率翻倍。第三件事是版本迭代成本。SDK一旦升级可能带来接口变动和二进制体积增加而且iOS每出一个版本SDK适配的节奏不可控。自己封装一层HTTP接口后端接口永远是稳定的客户端只需要关心自己服务器的协议完全不受服务商SDK版本影响。当然这不是说第三方SDK一无是处。对于没有后端的小工具型App、个人开发者做的DemoSDK集成确实是上线最快的路径。但凡是正经商业项目我都建议走服务端直连客户端只做一件最纯粹的事帮我后端接口。这个思路贯穿后面所有章节。2. 集成前的关键配置这步没做好后面全是坑很多开发者以为短信集成就是从写代码开始的其实大坑在写代码之前就已经埋好了。签名没申请、模板不匹配、Bundle ID对不上每一项都能让联调卡壳。2.1 开发者账号、Bundle ID和App ID的对应关系无论走哪条方案只要你的App要上线Bundle ID都是绕不开的基础。短信服务商的SDK注册时通常需要你填写App的Bundle ID这个值必须和Xcode工程里的Bundle Identifier保持一致否则SDK的初始化可能直接失败或者在真机调试时调用接口返回异常。用Xcode打开工程选中TargetGeneral标签页里的Bundle Identifier就是它。常规格式是com.company.appname建议一开始就规划好因为上架之后Bundle ID理论上不能改改了就是新App。如果你还涉及推送通知类的短信业务需要去开发者后台开通Push Notifications能力生成推送证书。但注意短信验证码本身走的是蜂窝网络的消息通道和APNs推送完全是两码事。不要被短信推送这种叫法搞混验证码短信不需要配置APNs除非你还要做站内信推送。2.2 短信服务商后台的签名、模板和AccessKey一个都不能少短信服务商都有一套审核机制核心是两个东西短信签名和短信模板。短信签名是用户手机上显示的发送方标识比如【某某科技】这种。签名代表你的企业身份申请的时候需要提供企业资质或应用的注册信息。个人开发者申请签名比较麻烦但也不是完全不行一般用应用名称作为签名也能过关键是名称不能太泛。短信模板是短信内容的格式。模板里像验证码这种变化的部分用占位符表示比如您的验证码为{code}5分钟内有效提交后服务商审核通过你才能用这个模板发送。这里有个常见的坑模板内容里的标点、空格必须和审核通过的完全一致。有些服务商在模板里用全角括号你调用API时用了半角提交直接报错。我踩过一次排查了半天最后发现是括号全半角的问题实在无语。AccessKey和AccessSecret也一样在服务商控制台创建注意它们是两个不同的概念。调用服务商OpenAPI时AccessKey写在请求参数里而AccessSecret是用来签名的绝对不能出现在请求参数里更不能出现在客户端代码里。要区分清楚才能理解为什么密钥不能放客户端。2.3 ATS配置与网络权限为什么HTTP请求发不出去iOS 9之后强制推行ATSApp Transport Security默认只允许HTTPS请求。如果你后端的短信接口是HTTP很多内网测试环境都是直接请求会被系统拦截报错信息通常长这样App Transport Security has blocked a cleartext HTTP request。解决方式是在Info.plist里配置ATS例外。但你得想清楚如果只是开发调试阶段连HTTP测试环境可以用NSAllowsLocalNetworking临时放开本地网络如果你的正式环境是HTTP不建议全局关闭ATS而是用NSExceptionDomains精确指定某个域名为例外。不过现在的短信服务商OpenAPI基本全是HTTPS你自己的后端接口也应当走HTTPS。这里提醒一句ATS的配置是全局性的如果因为短信接口把整个App的HTTP请求都放开了审核的时候被质疑风险会增高不如老老实实让后端把HTTPS配上。还有个小细节iOS 14之后本地网络权限Local Network Privacy会在应用访问局域网时弹出授权提示。如果你在真机上调试后端的测试服务跑在局域网里第一次发起请求会有弹窗没点允许的话耗时很长这也是排查为什么真机连不上后端的一个参考点。3. 核心实现Objective-C代码层面的对接细节方案定好、配置做完就进入写代码环节。我按最常见的服务端直连方案来拆解因为这套代码结构可以完整应用到商业项目里如果你选的是第三方SDK代码体例也类似只是请求对象换成SDK的接口而已。3.1 网络层封装一个简单的HTTP管理类客户端不需要关心短信服务商的OpenAPI细节只需要关心自己后端的接口。我习惯写一个轻量级的网络管理类基于NSURLSession封装专门负责验证码相关的HTTP请求。// SMSNetworkManager.h #import Foundation/Foundation.h typedef void(^SMSCompletionBlock)(BOOL success, NSError *error); interface SMSNetworkManager : NSObject (instancetype)sharedManager; /// 请求发送验证码 - (void)sendVerificationCodeWithPhone:(NSString *)phone scene:(NSString *)scene completion:(SMSCompletionBlock)completion; /// 校验验证码 - (void)verifyCode:(NSString *)code withPhone:(NSString *)phone completion:(SMSCompletionBlock)completion; end// SMSNetworkManager.m #import SMSNetworkManager.h static NSString * const kBaseURL https://api.example.com/v1/sms; implementation SMSNetworkManager (instancetype)sharedManager { static SMSNetworkManager *instance nil; static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ instance [[SMSNetworkManager alloc] init]; }); return instance; } - (void)sendVerificationCodeWithPhone:(NSString *)phone scene:(NSString *)scene completion:(SMSCompletionBlock)completion { NSURL *url [NSURL URLWithString:[kBaseURL stringByAppendingString:/send]]; NSMutableURLRequest *request [NSMutableURLRequest requestWithURL:url]; request.HTTPMethod POST; [request setValue:application/json forHTTPHeaderField:Content-Type]; NSDictionary *bodyDict { phone: phone, scene: scene }; NSError *serialError nil; NSData *bodyData [NSJSONSerialization dataWithJSONObject:bodyDict options:0 error:serialError]; if (serialError) { completion(NO, serialError); return; } request.HTTPBody bodyData; NSURLSessionDataTask *task [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { dispatch_async(dispatch_get_main_queue(), ^{ completion(NO, error); }); return; } // 这里解析后端返回的JSON判断业务成功与否 NSDictionary *json [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; BOOL success [json[code] integerValue] 0; NSError *bizError nil; if (!success) { NSString *msg json[message] ?: 请求失败; bizError [NSError errorWithDomain:SMSBusinessError code:[json[code] integerValue] userInfo:{NSLocalizedDescriptionKey: msg}]; } dispatch_async(dispatch_get_main_queue(), ^{ completion(success, bizError); }); }]; [task resume]; } end这里有几个细节想说。所有回调统一在主线程回调因为后续要操作UI比如倒计时按钮、弹窗提示避免在子线程直接刷新UI导致的崩溃。后端返回的code字段要与开发前约定的业务码一致不要在客户端写死判断逻辑宁可多一个字段也不要让客户端去猜。NSURLSession每次请求都会有自己的delegate queue如果你要统一控制超时、公共参数建议自己用NSURLSessionConfiguration创建一个session保存下来代码会更可控。3.2 发送验证码的完整调用链与倒计时逻辑UI层的逻辑其实不复杂但有一个地方容易写乱倒计时状态管理和用户重复点击。- (IBAction)sendCodeButtonTapped:(UIButton *)sender { NSString *phone self.phoneTextField.text; if (![self isValidPhoneNumber:phone]) { [self showToast:请输入正确的手机号]; return; } sender.enabled NO; [[SMSNetworkManager sharedManager] sendVerificationCodeWithPhone:phone scene:register completion:^(BOOL success, NSError *error) { if (success) { [self startCountdownWithButton:sender]; [self showToast:验证码已发送请查收]; } else { sender.enabled YES; NSString *msg error.localizedDescription ?: 发送失败请稍后重试; [self showToast:msg]; } }]; } - (void)startCountdownWithButton:(UIButton *)button { __block NSInteger remaining 60; self.countdownTimer dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(self.countdownTimer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0); dispatch_source_set_event_handler(self.countdownTimer, ^{ if (remaining 0) { dispatch_source_cancel(self.countdownTimer); self.countdownTimer nil; button.enabled YES; [button setTitle:重新发送 forState:UIControlStateNormal]; return; } [button setTitle:[NSString stringWithFormat:%lds后重试, (long)remaining] forState:UIControlStateNormal]; remaining--; }); dispatch_resume(self.countdownTimer); }手机号校验这个函数虽然简单但值得单独写。中国手机号第一位是1第二位是3到9后面9位是数字。正则表达式简单写一下- (BOOL)isValidPhoneNumber:(NSString *)phone { NSString *regex ^1[3-9]\\d{9}$; NSPredicate *pred [NSPredicate predicateWithFormat:SELF MATCHES %, regex]; return [pred evaluateWithObject:phone]; }注意这个校验只是客户端UI层的快速提示真正的有效性校验必须在后端做因为客户端的校验可以被绕过后端还需要配合风控去判断这个手机号是否满足发送条件。倒计时这里容易出问题的是界面销毁时的定时器清理。如果你用了dispatch_source在viewWillDisappear或者dealloc里一定要dispatch_source_cancel否则页面退了定时器还在跑会有内存隐患。还有如果用户在前台进入后台再回来dispatch_source的计时是连续的不会因为你切后台就暂停这点比NSTimer更符合倒计时预期。3.3 验证码校验必须放在服务端做的事验证码校验是整个流程里最核心的安全环节。客户端拿到用户输入的验证码传给后端接口后端比对存储的验证码是否一致以及是否过期、是否超过最大尝试次数。- (IBAction)verifyCodeButtonTapped:(UIButton *)sender { NSString *phone self.phoneTextField.text; NSString *code self.codeTextField.text; if (code.length ! 6) { [self showToast:请输入6位验证码]; return; } __weak typeof(self) weakSelf self; [[SMSNetworkManager sharedManager] verifyCode:code withPhone:phone completion:^(BOOL success, NSError *error) { typeof(self) strongSelf weakSelf; if (!strongSelf) return; if (success) { [strongSelf enterMainPage]; } else { [strongSelf showToast:error.localizedDescription ?: 验证失败]; } }]; }为什么校验必须在后端做因为如果客户端本地有个节点保存了验证码反编译你的App就能看到校验逻辑用户甚至不需要真收短信直接用暴力枚举的方式去试。后端的校验逻辑可以做如下几层防护验证码与手机号绑定一个验证码只能用于一个手机号每个手机号每天的发送次数限制验证失败次数限制同一IP维度做频率限制。这些写在后端客户端完全不需要感知但短信业务流程的可靠性会大幅上升。3.4 自动填充验证码iOS 12的OneTimeCode技巧短信发出去了用户切到短信App看验证码再切回你的App手动输入这个体验确实繁琐。iOS 12开始支持验证码自动填充系统会识别短信中的验证码文本在键盘上方推荐填充你的输入框需要做一个小小的适配。// 在viewDidLoad里设置 self.codeTextField.textContentType UITextContentTypeOneTimeCode; self.codeTextField.keyboardType UIKeyboardTypeNumberPad;就这么简单。系统的识别逻辑要求短信文本里有明显的验证码占位信息和有效期描述比如您的验证码是1234565分钟内有效系统才能准确识别。你只管保证模板内容结构清晰。还有如果你的App部署了iOS 17以上的系统验证码自动填充的隐私策略更严格了系统只会在短信来自同一应用域名或关联域名时自动推荐所以如果你自己后端在短信模板里使用了Apple的Universal Link关联域名一定要确认关联配置无误否则自动填充会失效。这一块在实测时常常被忽略建议联调时专门测一下不同系统版本下的填充行为。4. 高性能集成的几个实战策略不要把短信接口做成卡顿源短信功能虽然小但一旦集成方式粗糙很容易拖累整体App体验。尤其在大促场景下瞬时发送量很大接口响应如果有延迟用户那边转圈几秒就会骂娘。从客户端角度高性能集成主要看三个方面异步非阻塞、缓存与批量思想、频率控制策略。4.1 异步非阻塞是底线我见过一些公司里的老代码直接在调用短信接口的地方加了个同步等待等接口返回才让用户做下一步操作。这在iOS上是大忌因为网络请求一旦发起等待时间受网络环境影响极大可能几秒甚至超时。用户看到的就是界面卡死。NSURLSession天生是异步的但前提是你不要在回调里执行耗时操作。如果你的短信功能需要把手机号、场景、设备信息、经纬度等参数打包发送这个打包过程一般很快但如果涉及加密、签名、压缩就要注意把计算量大的部分放到后台队列处理。dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 这里做参数加密、拼装请求体 NSData *encryptedData [self encryptParam:params]; dispatch_async(dispatch_get_main_queue(), ^{ // 回到主线程发起UI相关的请求 [self sendRequestWithEncryptedData:encryptedData]; }); });这里的核心原则是UI永远不要因为短信接口的处理逻辑而被阻塞。发送失败、网络超时都应当以优雅的方式提示用户而不是让用户干瞪眼。4.2 缓存与批量思想不是所有短信都要实时发这里我说的缓存是指客户端层面的结果缓存。比如营销短信里的活动通知不是一个用户单独触发而是后台运营批量下发。客户端能做什么在用户点击活动页时先把活动信息请求下来缓存然后异步去触发推送服务商的短信通知。这不是一条链路的事而是多条链路的编排。但如果你接的是验证码类接口不要做缓存。验证码的安全性在于一次性、时效性缓存了验证码、客户端只追求秒开反而会导致验证码状态不同步后端判定已过期前端还让用户填。这种场景需要的是高频监控、快速反馈而不是缓存优化。那批量思想体现在哪体现在你的后端接口设计上。如果同一个用户在短时间内由于误操作连续点了多次发送按钮后端应该合并请求或者直接按频率限制拦截。客户端层面按钮的倒计时就是一种防止重复发送的天然批量限制每次发送前先检查距离上次发送是否超过60秒这个策略很朴素但很有效。4.3 防刷策略客户端能做的和不能做的短信验证码最大的敌人是刷子。他们用脚本肉鸡手机号批量撞库、批量发送骚扰短信消耗你的短信配额骚扰用户体验。客户端能做的防刷有限因为客户端所有逻辑都能被逆向所以更合理的分层是客户端做初级限制后端做核心限制。客户端层面可以做的按钮倒计时禁用最简单防不住有经验的刷子简单滑块验证一个页面发送验证码之前先要求用户滑动确认埋点记录用户行为如果监测到非常规频率上报到后端。后端层面必须做的同一手机号每分钟最多1次、每天最多5次这是底线同一IP每分钟最多N次N由你们的压测数据决定针对异常行为如短时间内同一设备变换多个手机号做风控联动。我在项目里用过一个很有效的策略发送验证码前先要求客户端调用后端的预检接口后端根据设备指纹、IP、用户行为给出是否允许发送的裁决。对于判定为异常的风险请求后端直接返回请求过于频繁请稍后再试而不是真的去调用短信服务商下发短信。这样即使有刷子把你的短信配额刷爆也只能刷到预检接口真正的短信通道风险小很多。4.4 弱网环境下的体验优化移动网络环境参差不齐地铁、电梯、地下车库里的用户也照样要注册。弱网下发送短信验证码最常见的现象是请求超时用户等了几秒没反应再点一次又超时。客户端这块有几个可以优化的点。超时时间要合理。默认超时时间通常60秒太长了用户等不了。我一般设置8秒到12秒超时之后明确提示网络不给力请检查网络设置。短超时能快速失败让用户尽早进入重试流程。请求幂等性。手机号加场景加本次会话ID后端收到重复请求时只返回第一次的处理结果不重复下发。这个策略放在后端客户端只需要保证同一个按钮点击事件生成的会话ID不变即可。错误分类型展示。网络错误、服务端错误、频控拦截这三种情况用户看到的文案应该不一样。统一文案发送失败会让用户一头雾水也不利于客服排查。NSError *error ...; if (error.code NSURLErrorTimedOut) { [self showToast:网络超时请检查网络后重试]; } else if (error.code NSURLErrorNotConnectedToInternet) { [self showToast:当前无网络连接]; } else if (error.code 429) { // 服务端定义的频控码 [self showToast:发送太频繁请稍后再试]; } else { [self showToast:error.localizedDescription]; }5. 常见问题与排查技巧实录从收不到短信到审核被拒这部分是实战里最容易让人抓狂的环节。短信功能集成完联调时经常遇到各种诡异问题我把真机调试、抓包、审核这几个场景里的典型坑集中写出来。5.1 收不到短信先按链路一层层排查收不到短信是最常见的故障现象。但收不到的原因千奇百怪千万不要一上来就怀疑SDK或者服务商。我按排查顺序写下来手机号是否输错。这个看似废话但真很多用户输错自己手机号验证码发到别人那里去了。所以界面要明确展示发送至尾号XXXX的提示。短信是否被拦截。现在的手机都有骚扰拦截功能尤其iOS 11之后原生支持SMS过滤扩展有些拦截App会把验证码短信也吞掉。建议用户查一下设置-信息-未知与过滤信息名单。短信服务商状态是否正常。去服务商控制台查发送记录看返回状态如果发送失败常见原因是签名或模板审核不通过、当日限额耗尽、服务商通道异常。后端是否真的调用了短信服务商接口。联调时经常出现后端代码没部署或者配置环境错误导致实际请求没发出去。这一点可以通过后端日志确认。如果以上都没问题但用户还是说收不到那就要考虑短信延迟的问题。验证码短信偶尔会有分钟级延迟这通常不是你的问题而是服务商通道调度问题。如果体验要求高可以考虑加一条如果1分钟未收到可点击语音验证码的兜底。5.2 用Charles抓包调试短信接口联调排查问题抓包是逃不掉的手段。iOS上最常用的抓包工具是Charles配合手机代理就能看到App发出的HTTP请求。前提条件是手机和电脑连同一个局域网手机Wi-Fi代理设为电脑IP加8080端口Charles上开启SSL Proxying并安装证书到手机。这里有个坑iOS 10.3之后安装Charles证书后还需要到设置-通用-关于本机-证书信任设置里手动开启完全信任否则HTTPS请求还是解密不了。抓包主要看什么看短信发送接口的请求参数、响应体、状态码。比如你发现前端发了phone字段但后端报签名错误那说明签名的生成逻辑有问题如果你看到请求根本没有发出去那就是ATS或网络权限配置的问题Charles里连一条记录都不会有。现在的App普遍做了HTTPS证书校验也就是防中间人攻击的证书锁定SSL Pinning。如果你在代码里做这层加固Charles抓包会显示证书错误需要临时在DEBUG模式下关闭证书锁定或者把Charles的证书加到信任列表。这个属于开发期操作注意不要把这个关闭逻辑带到线上包。5.3 真机调试中的开发者模式、证书与签名问题短信功能必须在真机上才能完整测试模拟器上收不到短信。真机调试会碰到几个高发问题。设备未进入开发者模式。iOS 16之后你需要到设置-隐私与安全性-开发者模式里手动开启。如果没开启Xcode运行时会报Could not launch网上很多人都卡在第一步就是这里。签名和UDID配对问题。免费开发者账号创建的证书只有7天有效期过期之后再调试就要重新签名。除非你买了99美元一年的开发者账号否则要做好每次过期都重签的准备。推送证书过期。如果你的短信功能同时关联了推送推送证书过期后推送发不出去但短信本身不受影响。排查时注意区分。遇到真机连接报错最快的定位方法是看Xcode的Device窗口和Console日志。Console日志里能看到App崩溃堆栈很多时候短信SDK初始化失败的异常信息都在这里。如果你用的是自签证书要注意证书信任设置里有没有添加对应的信任否则接口请求同样会失败。5.4 上架审核中最容易踩的坑短信功能最容易在上架环节被App Review盯上主要集中在几个方面。第一是短信权限的描述。如果你的App申请了读取短信权限这几乎用不到但有些SDK会悄悄申请审核员会问你要用途说明。实际上验证码自动填充不需要读取短信权限那是系统级能力。如果你集成第三方SDK时发现权限列表里多了读取短信的权限建议直接换一个SDK这种权限在审核中极其危险。第二是隐私政策。只要App有短信验证码就必须在隐私政策里明确说明手机号用于什么目的、如何存储、如何保护。审核被拒时最常见的原因就是隐私政策链接打不开或者内容里没提短信验证码相关的数据使用条款。第三是过度营销。如果你的App有营销短信功能审核员会重点看用户是否明确同意接收。也就是说在用户注册或下单时营销短信请求必须是独立的、可选的勾选项不能捆绑在我已阅读协议里面。我那次审核被拒就是因为默认勾选了同意接收活动通知后面把这个勾选框改成默认不勾选同时增加独立的订阅页面再审就过了。还有两个细节容易被忽略短信验证码的发送按钮文案。Apple不太喜欢诱导性文案比如立即免费领取验证码这种带营销倾向的表述尽量用中性的获取验证码就行。应用图标与短信签名的一致性。签名通常放在短信开头格式是【某某】这个某某必须和你的应用名称、品牌名一致否则容易被审核认定为虚假标识。6. 最后分享几个短信用久了才体会到的经验上面那条关于审核的分享是我被拒一次之后老老实实总结出来的这里就不再重复说一定要看审核指南这种空话了。说三个更细的经验可能对你有用。第一个经验是会话ID的设计。我在项目里会让后端生成一个sendId返回给客户端客户端校验验证码时把sendId一起传过去。这样即使同一个手机号短时间内发了多次验证码后端也能精确定位用户到底在校验哪一次发送的验证码不会因为验证码覆盖导致用户填了正确的码却报错。第二个经验是短信回执状态的回传。短信服务商在下发之后会异步回报送达结果例如成功、失败、已读等状态。我建议后端接一个回调接口把这些状态记录下来。用户说没收到客服可以快速查到短信已到达还是运营商网关失败省去大量扯皮。第三个经验是关于多渠道降级的。大促期间短信通道非常容易拥堵这时候如果你能支持语音验证码作为降级方案用户体验会好很多。很多短信服务商同时提供语音验证码接口逻辑几乎一样只是从文字短信变成电话播报六位数字。客户端只需要在后端返回短信发送失败建议走语音验证码时弹窗让用户选择听语音验证码。这个功能不复杂却能在关键时刻救回一批用户。短信接口集成看着是个小功能实际上涉及客户端、服务端、服务商、运营商、审核一共五层。把它打通不难但真正做到高性能、不踩坑、稳定上线需要把每一层的细节都照顾到。希望这篇文章能帮你把链路理清楚少走点弯路。