1. 短信验证码这件事为什么大家最后都选了云服务做过用户注册登录模块的人都知道验证码这道关卡绕不过去。早期很多团队自己搭短信猫、接第三方卡商通道折腾下来发现维护成本高得离谱——通道不稳定、到达率忽高忽低、被运营商拦截了还得自己排查。后来大家慢慢转向云厂商的短信服务原因很直接通道质量有保障、按条计费不用养硬件、API 接入也足够简单。阿里云短信服务算是国内用得比较多的一种方案尤其在 Java 技术栈的项目里接入流程已经非常成熟。但成熟不代表没有坑。我见过不少开发者第一次接入时卡在签名审核、模板变量格式、SDK 版本冲突这些地方明明照着文档写却跑不通。这篇内容就是把我自己接入过程中踩过的坑、验证过的步骤、以及一些文档里不会明说的细节整理出来给需要做这个功能的同行一个可以直接抄的参考。整篇内容会围绕 Java 环境下接入阿里云短信服务发送验证码这个核心任务展开从前期准备、依赖选型、代码实现、参数配置到线上排查每个环节都会说清楚为什么这么做而不只是怎么做。适合有一定 Java 基础、正在做用户体系相关功能的开发者阅读也适合已经接入过但想优化现有方案的人参考。2. 接入之前必须搞清楚的账号与权限准备2.1 短信服务开通与实名认证的前置条件在写第一行代码之前账号层面的准备工作必须做到位否则后面调接口全是报错。首先你需要在云平台上完成实名认证这是开通短信服务的前提。个人认证和企业认证都可以但企业认证在签名申请时通过率更高个人认证能申请的签名类型相对受限。开通短信服务本身不收费它是按实际发送条数计费的。开通之后你会进入短信服务控制台这里有几个关键概念需要先理解签名SignName显示在短信内容开头的标识比如【某某科技】。签名需要单独申请并审核审核周期通常几个小时到一两天不等。模板TemplateCode短信正文的格式定义验证码类模板一般长这样您的验证码是${code}5分钟内有效。模板同样需要审核。AccessKey调用 API 的身份凭证分 AccessKey ID 和 AccessKey Secret 两部分。这三个东西缺一不可而且签名和模板必须审核通过后才能正式使用。我建议在项目开发初期就先去提交签名和模板申请因为审核需要时间不要等到代码写完了才发现签名还没批下来。注意AccessKey Secret 只在创建时显示一次之后无法再次查看。如果丢失只能重新创建所以拿到之后立刻妥善保存到配置中心或密钥管理服务里千万别硬编码在代码中提交到代码仓库。2.2 签名与模板的申请技巧和常见驳回原因签名申请被驳回是新手最容易遇到的第一道坎。常见的驳回原因有这么几类签名内容与你的账号主体无关、签名涉及敏感词汇、签名过于宽泛比如只写通知两个字。正确的做法是签名用你的产品名、公司简称或已备案的域名主体保持与账号实名信息一致。模板申请也有讲究。验证码模板的变量通常用${code}表示变量名可以自定义但必须和代码里传的参数对应上。模板内容不能包含营销性质的话术验证码模板就老老实实写验证码相关的内容。我见过有人把验证码模板写成您的验证码是${code}回复TD退订结果被驳回因为验证码类短信本身就不该有退订选项。模板审核通过后你会拿到一个 TemplateCode类似SMS_123456789这样的字符串。这个 code 和签名一样都是调用接口时的必填参数。2.3 AccessKey 的安全管理策略AccessKey 的管理是个容易被忽视但极其重要的环节。很多教程为了演示方便直接把 AccessKey 写在代码里这在生产环境是绝对禁止的。一旦代码泄露或者被提交到公开仓库别人就能用你的账号发短信产生的费用全部算在你头上。推荐的做法有这么几种按安全等级从低到高排列管理方式安全等级适用场景配置文件明文低本地临时测试环境变量中小型项目、单机部署配置中心加密存储高中大型项目、多机部署RAM 子账号 最小权限最高生产环境推荐生产环境强烈建议使用 RAM 子账号只授予短信发送相关的最小权限而不是直接用主账号的 AccessKey。这样即使凭证泄露损失也能控制在短信服务范围内不会波及账号下的其他资源。3. 依赖选型官方 SDK 还是自己封装 HTTP 请求3.1 官方 SDK 的版本选择与依赖冲突处理阿里云短信服务的 Java SDK 经历过几个大版本迭代。早期是aliyun-java-sdk-dysmsapi这个独立包后来统一到了aliyun-java-sdk-core加各产品模块包的结构再后来又推出了新版dysmsapi20170525这种带日期的版本号包。选哪个版本我的建议是优先用较新的版本因为老版本有些已经停止维护了。但要注意新版 SDK 对 JDK 版本有要求一般需要 JDK 8 及以上。如果你维护的是老项目还在用 JDK 7那就只能用老版本 SDK。Maven 依赖大概长这样dependency groupIdcom.aliyun/groupId artifactIddysmsapi20170525/artifactId version3.0.0/version /dependency dependency groupIdcom.aliyun/groupId artifactIdtea-openapi/artifactId version0.3.0/version /dependency依赖冲突是接入过程中比较烦人的问题。新版 SDK 底层依赖了tea系列的基础库如果你的项目里已经有其他阿里云产品 SDK可能会出现版本不一致导致的NoSuchMethodError。排查方法是用mvn dependency:tree看依赖树找到冲突的包统一版本。3.2 自己封装 HTTP 请求的可行性分析有人会问能不能不用 SDK自己用 HttpClient 调 REST 接口技术上完全可以短信服务的 API 本质就是 HTTP 请求加签名计算。但我不推荐这么做原因有三第一签名计算逻辑比较复杂涉及 HMAC-SHA1 加密、参数排序、URL 编码等步骤自己实现容易出错且调试困难。第二官方 SDK 已经帮你处理了重试、超时、异常封装这些通用逻辑自己写等于重复造轮子。第三SDK 升级时接口签名算法如果有调整用 SDK 的项目改个版本号就行自己封装的得改代码。当然如果你有特殊需求比如要控制底层连接池、要做自定义的请求拦截那在 SDK 基础上做二次封装是合理的。但完全抛弃 SDK 从零实现投入产出比不高。3.3 SDK 初始化与客户端复用SDK 的客户端对象Client应该做成单例复用而不是每次发短信都 new 一个。因为 Client 内部维护了 HTTP 连接池频繁创建销毁会造成资源浪费。在 Spring 项目里把 Client 配置成一个 Bean 是最自然的做法。初始化 Client 需要传入认证信息新版 SDK 用的是com.aliyun.teaopenapi.models.Config对象Config config new Config() .setAccessKeyId(accessKeyId) .setAccessKeySecret(accessKeySecret) .setEndpoint(dysmsapi.aliyuncs.com); Client client new Client(config);Endpoint 一般固定是dysmsapi.aliyuncs.com不需要改。如果你用的是金融云等特殊环境Endpoint 会不一样这个要查对应文档确认。4. 发送验证码的核心代码实现与参数拆解4.1 请求对象的构建与各字段含义发送短信的核心是构建SendSmsRequest对象然后调用 Client 的sendSms方法。请求对象里几个关键字段SendSmsRequest request new SendSmsRequest() .setPhoneNumbers(13800138000) .setSignName(你的签名) .setTemplateCode(SMS_123456789) .setTemplateParam({\code\:\123456\});PhoneNumbers是接收号码支持多个号码用逗号分隔但验证码场景一般只发一个。号码格式必须是纯数字不带国家码比如13800138000不要写成8613800138000。SignName就是审核通过的签名内容注意不要带方括号直接写签名文字就行。TemplateCode是模板 ID这个必须和模板审核通过后拿到的一致。TemplateParam是模板变量格式是 JSON 字符串。这里有个容易踩的坑变量名必须和模板里定义的完全一致包括大小写。模板里写的是${code}你传的参数 key 就必须是code传成Code或者verifyCode都会导致变量替换失败用户收到的短信里会直接显示${code}这个原始占位符。4.2 验证码生成的安全随机策略验证码的生成看似简单但安全性上有讲究。用Math.random()生成是不推荐的因为它用的是伪随机算法在特定条件下可能被预测。正确做法是用SecureRandomSecureRandom random new SecureRandom(); int code 100000 random.nextInt(900000); String verifyCode String.valueOf(code);这样生成的是 6 位数字验证码范围从 100000 到 999999。为什么不用 4 位4 位验证码只有 1 万种组合暴力破解的成本太低。6 位有 90 万种组合配合发送频率限制和验证次数限制安全性足够。验证码生成后不能只存在内存里因为用户可能刷新页面或者服务重启。通常的做法是存到 Redis 里key 用手机号value 是验证码同时设置过期时间比如 5 分钟。验证的时候从 Redis 取出来比对比对成功后立即删除防止同一个验证码被重复使用。4.3 发送结果的判断与异常处理调用sendSms方法后返回的是SendSmsResponse对象里面包含Code、Message、BizId等字段。判断发送是否成功不能只看有没有抛异常还要看返回的 CodeSendSmsResponse response client.sendSms(request); if (OK.equals(response.getBody().getCode())) { // 发送成功 String bizId response.getBody().getBizId(); } else { // 发送失败记录错误码和错误信息 String errorCode response.getBody().getCode(); String errorMsg response.getBody().getMessage(); }常见的错误码有这些错误码含义处理建议isv.BUSINESS_LIMIT_CONTROL触发流控降低发送频率检查是否被恶意刷isv.SMS_SIGNATURE_ILLEGAL签名不合法检查签名是否审核通过、是否填写正确isv.SMS_TEMPLATE_ILLEGAL模板不合法检查模板 ID 是否正确、模板是否审核通过isv.MOBILE_NUMBER_ILLEGAL号码格式错误检查号码是否为纯数字、是否有效isv.AMOUNT_NOT_ENOUGH余额不足充值异常处理方面网络超时、连接被拒这类异常需要捕获并做重试。但重试要注意幂等性不能因为重试导致用户收到多条短信。我的做法是记录每次发送的请求 ID重试前先检查是否已经发送成功。5. 上线前必须验证的几件事和线上问题排查5.1 本地测试与线上环境的差异点本地开发时很多人会用真实的手机号去测试这没问题但要注意几个差异。本地网络环境可能和线上服务器不同线上服务器如果有出网限制需要把短信服务的域名加到白名单里。另外线上服务器的时区、字符编码设置也可能影响签名计算虽然概率低但确实遇到过。测试阶段建议专门准备一个测试手机号不要用真实用户的号码去试。如果项目还在开发期可以考虑用短信服务的测试功能或者 mock 掉发送逻辑避免产生不必要的费用。5.2 发送频率限制与防刷策略短信服务本身有流控限制同一个手机号一分钟内、一小时内、一天内能收到的短信条数都有限制。但光靠云厂商的限制不够你自己的业务层也要做防刷。常见的防刷策略是三层限制同一手机号 60 秒内只能发一次、同一 IP 一小时内最多发 10 次、同一手机号一天最多发 10 次。这些计数用 Redis 的过期键来实现最方便每次发送前先检查计数超过阈值直接拒绝。图形验证码是另一个有效的防刷手段。在发送短信验证码之前先让用户完成一个图形验证码校验这样能挡住大部分自动化脚本。虽然会稍微增加用户操作步骤但对防止短信接口被恶意刷取非常有效。5.3 线上发送失败的排查链路线上环境发送失败时排查要按顺序来不要东一榔头西一棒子。我的排查顺序是这样的第一步看错误码。错误码是最直接的线索对照上面的表格基本能定位到问题类型。第二步检查配置。AccessKey 是否过期或被禁用、签名和模板是否还在有效期内、Endpoint 是否正确。这些配置问题占了线上故障的一大半。第三步看网络。服务器能不能访问短信服务的域名可以用telnet dysmsapi.aliyuncs.com 443测试连通性。如果有防火墙或安全组限制需要放行。第四步看日志。把每次发送的请求参数和返回结果都打到日志里包括手机号脱敏后、模板 ID、返回码、BizId。出问题时翻日志比猜要快得多。第五步看用量。登录控制台看发送量统计如果发送量突然暴涨可能是被刷了需要立即启用更严格的限流。提示日志里记录手机号时一定要脱敏比如只保留前三位和后四位中间用星号代替。这既是合规要求也避免日志泄露用户隐私。6. 几个容易被忽略的工程化细节6.1 验证码的有效期与重发机制设计验证码的有效期设置是个平衡问题。太短用户来不及输入太长则安全风险增加。5 分钟是业界比较通用的选择。重发机制上用户点击重发时不应该重新生成验证码而是把原来的验证码再发一次或者延长原验证码的有效期。如果每次都生成新验证码用户可能收到多条短信旧的那条还没失效容易混淆。Redis 存储验证码时key 的设计建议用业务前缀加手机号比如sms:verify:13800138000这样不同业务的验证码不会互相覆盖。value 直接存验证码字符串就行不需要存 JSON。6.2 短信内容与模板变量的对应关系模板变量的替换是严格按照 key 来匹配的。如果你的模板里有多个变量比如${code}和${time}那 TemplateParam 里两个都要传String param {\code\:\ verifyCode \,\time\:\5\};JSON 字符串的拼接要注意转义用 JSON 库比如 Jackson 或 Fastjson 来生成更稳妥避免手动拼接时漏掉引号或转义字符。我见过因为手动拼 JSON 导致格式错误接口返回参数解析失败的案例。6.3 多环境配置的隔离方案开发、测试、生产环境的签名和模板可能不同AccessKey 也应该隔离。推荐用 Spring 的 Profile 机制或者配置中心来做环境隔离。每个环境一套独立的配置通过application-dev.yml、application-prod.yml这样的文件区分。如果公司有配置中心把短信相关的配置都放到配置中心里支持动态刷新。这样修改签名或模板不需要重启服务改完配置立即生效。对于需要频繁调整限流阈值的场景动态配置尤其有用。6.4 发送记录与对账每发送一条短信都应该落库记录字段包括手机号、验证码加密存储、发送时间、返回码、BizId。这些记录一方面用于问题排查另一方面用于和云厂商的账单对账。有时候云厂商统计的发送量和你自己记录的对不上有记录才能查清楚差异在哪。对账的周期建议每天一次比对发送条数和费用。如果发现异常增长及时排查是配置问题还是被恶意刷了。这个习惯在项目初期可能觉得多余但真出问题时能帮你快速定位。