
长年在服务器上折腾的人迟早会碰到一个需求某个任务跑完了、某个服务挂了、磁盘快满了你得第一时间知道。早些年我习惯挂着终端或者开着邮件提醒但邮件经常被丢进垃圾箱终端又总有不在跟前的时候。短信虽然朴实却是真能稳稳当当把一句话递到你手上的手段。当时我手上只有一个Linux服务器、一堆Shell脚本和curl花了一个下午把短信API接了起来从此定时任务、监控脚本全都多了一双会喊话的眼睛。这篇就完整梳理一遍我是怎么用Curl指令在Linux脚本里把短信通知跑通的。整个方案核心就三样东西Shell、Curl、短信API。Shell负责流程控制和任务触发Curl负责发HTTP请求云厂商的短信API负责把内容变成真正发到你手机上的短信。写完之后你也能在五分钟内手工发一条测试短信再花二十分钟封装成通用脚本直接塞进自己的监控体系里。全程不依赖Python、不走邮件网关、不需要额外安装守护进程一个bash就能搞定非常契合轻量运维的场景这篇东西适合刚接触Shell脚本、又恰好有短信发送需求的运维和开发同学。1. 为什么我最终选Shell加Curl而不是写个Python脚本1.1 我的应用场景和对通知系统的要求先交代一下当时的实际环境。我在维护几台跑业务的Linux机器上面既有Nginx和数据库也挂着几个每天定时执行的备份脚本和爬虫任务。原来出问题怎么办靠我隔几个小时登录看一眼或者翻日志。后来有一次一个备份脚本因为磁盘空间不足挂了三天我才意识到没有一个主动通知机制所谓监控就是个摆设。我评估下来通知方案的选择其实很有限。邮件容易进垃圾箱而且配置SMTP本身也很烦钉钉和企微机器人虽然方便但国内办公软件的通知声音我不一定注意得到短信呢只要手机有信号就一定能收到。至于为什么不用Python脚本——服务器上确实装了Python但很多生产机器环境比较旧Python的requests库不一定装了而curl是Linux里几乎铁定存在的命令。为了发一条通知去补一堆依赖这笔交易不划算。1.2 用Curl调用接口的天然优势Curl能成为Linux下调用HTTP接口的事实标准不是没道理的。它在绝大多数发行版里开箱即用哪怕是最小化安装的服务器一般也带着curl或者能用包管理器三秒装好。Curl支持GET、POST、PUT、DELETE这些主流方法也能带Header、带JSON体、处理超时、跟随重定向日志输出还能精确到每一字节的收发情况。最关键的还是可调试。用命令行发请求你看到的响应是明文报错时能直接看到HTTP状态码和返回体。配合-v参数还能看到握手过程、请求头等细节而写Python代码时遇到网络问题反而要多绕几个弯。运维场景里能被一行命令当场验证的东西就是最可靠的。1.3 和定时任务Crontab的天然亲和要知道Crontab执行的是命令Shell脚本无非是一个大号的命令集合而Curl又是一个命令。这三个东西它们本来就是同一生态的语言。我把发送短信的代码写成一个Shell函数在备份脚本最后一行调用一下定时任务就自动带上通知能力了不用额外起服务、不用配队列。这个顺手的感觉是其他语言很难给的你要写个Python脚本还得考虑cron环境里的PATH、PYTHONPATH甚至编码问题而Shell里写Curl就完全没这些负担。2. 接入短信API前必须摸清的底牌2.1 账号体系AccessKey和密钥的配合方式市面上的短信服务商常见的有阿里云、腾讯云、华为云以及一些老牌的短信平台。它们的API风格大同小异但鉴权方式几乎都是同一套思路用一组AccessKey和Secret来标识你的身份其中AccessKey是公开的标识Secret是你和服务器之间的共同秘密用来给请求签名。这里面有个让新手特别容易卡住的点Secret永远不会出现在请求参数里它是用来计算签名的不是直接传给服务器的。服务器也知道你的Secret服务器算出签名和你传过去的签名做对比一致就说明这个请求确实是持密钥的人发来的。不知道这个机制的人很容易把Secret明文塞进参数里结果不但报签名错误还把密钥暴露了。签名机制的完整过程我下一节会详细走一遍。2.2 短信签名和短信模板的含义短信API和普通业务接口有个很大的不同它不会让你随手传一段字符串当内容发出去。你得先申请一个短信签名比如【我的运维助手】还得申请一个短信模板比如您的服务器{1}发生异常请及时处理。审核通过后调用API时传的是签名名称和模板ID而不是直接传短信内容。为什么要这样做呢因为短信通道是运营商强监管的领域平台必须保证每一条短信都有明确的发送主体和内容类型才能避免垃圾短信和网络诈骗。我第一次接的时候因为模板里写了测试两个字被审核驳回了三次后来写成您的服务发生异常请检查处理才过。所以准备工作里一定要把模板内容想好留好占位符上线前先在控制台发一条测试短信确认签名和模板的状态是已开通。2.3 环境里必备的小工具侦察正式开始之前我习惯先确认环境里缺什么。用which curl date base64 openssl sed检查一下这几个命令分别负责网络请求、时间戳生成、加密运算、字符处理后面签名计算和请求发送全都用到。在绝大多数发行版里curl和sed几乎一定存在date也是但openssl不一定需要时用yum install openssl -y或apt install openssl -y补一下就行。版本上curl尽量别太老7.55以下的版本在处理某些TLS证书或者IPv6场景时会有一些已知毛病。你可以用curl --version看看版本号如果实在太老顺手升级一下。不过这里有个细节要注意云厂商的API域名走的都是HTTPS所以curl必须带openssl或nss的支持否则没办法验证服务器证书。想知道自己的curl是否支持TLS运行curl -V时看输出里有没有HTTPS字样。3. 核心实现之一签名计算的完整推导过程3.1 先理解签名公式再抄代码各厂商的签名算法里百度云的算是清晰友好阿里云的传统签名流程也有代表性。我把通用的计算原理拆给你看不针对某个具体厂商你理解了这套逻辑换任何一家API都能在三十分钟内完成适配。签名的输入是一组字符串通常由HTTP方法、请求参数、密钥这三部分组成。基本流程分四步把所有参数除了Signature本身和几个文件类参数按Key的字典序升序排列把参数名参数值用连接起来形成规范化查询字符串对参数值做严格URL编码不只是特殊字符连普通参数也得规范编码以密钥作为Key对规范化字符串做HMAC计算把结果再做一个Base64编码就得到签名。整个过程的精髓在于双方用同样的规则算一遍。你先算服务器也按同样规则算一遍俩值一样就通过。规则里任何一点不一致比如排序顺序不对、编码方式把空格转成了加号而不是%20结果就会报SignatureDoesNotMatch。3.2 带着具体实例手算一遍我们用一个小例子来验证。假设有两个参数PhoneNumbers13800138000和TemplateCodeSMS_123。第一步按ASCII码顺序排序P在T前面所以PhoneNumbers在前TemplateCode在后。第二步组成字符串PhoneNumbers13800138000TemplateCodeSMS_123。第三步对它做HMAC计算这里用的是HMAC-SHA1或HMAC-SHA256取决于API版本常见的阿里云旧版是HMAC-SHA1新版是HMAC-SHA256。密钥就是你的AccessKey Secret加上一个固定的字符。代码层面用Shell加openssl实现核心就这么几行# 假设ak_secret是密钥string_to_sign是上一步拼出来的规范化字符串 signature$(echo -n $string_to_sign | openssl dgst -sha1 -hmac $ak_secret -binary | base64)这里有两个容易出错的地方。一是echo -n不能写成echo多一个换行符整个签名就算不对二是base64编码时不能手动换行openssl输出binary再管道给base64就刚好但如果用base64 -w0可能会在某些系统上不识别-w参数建议用base64的默认行为但在Linux上默认会自动换行。despite这个坑实际用openssl base64更统一。3.3 时间戳和随机串那些规定动作短信API的签名里一般还要带两个防重放攻击的参数Timestamp和SignatureNonce。Timestamp要求是ISO8601格式的UTC时间不能带毫秒格式像2025-01-15T08:30:00Z。在Shell里生成这个时间最稳的写法是timestamp$(date -u %Y-%m-%dT%H:%M:%SZ)签名随机串SignatureNonce呢要的是每次请求都不一样防重放用的。Shell里生成随机串的办法不少我用过date %s%N配合$$进程号也用过cat /proc/sys/kernel/random/uuid。生产上更推荐uuid因为随机性更强但是在最小化系统里可能没有uuidgen命令。考虑到通用性我平时都用date %s%N$$拼一个够用反正防重放主要依赖时间戳窗口。3.4 为什么阿里云新版签名会更难拆我后来也接过新版签名算法的接口就是AccessKey ID以LTAI开头的那批老账号还在用的兼容模式以及部分新产品的ACS3-HMAC-SHA256。新版的区别在于多了个X-acs-signature-nonce之类的Header而且请求体的Hash也要参与签名。从Shell脚本的角度来说如果旧版能接入优先用旧版兼容入口因为旧版所有信息都在Query里拼一个URL就够了。新版要处理请求体的SHA256摘要还得额外维护Header顺序脚本复杂度马上就上来了。判断是哪个版本就看你的API描述文档里签名方法是HMAC-SHA1还是ACS3-HMAC-SHA256。对个人运维场景来说能用1.0的接口写信签就尽量别折腾3.0。4. 核心实现之二Curl指令的实战写法4.1 把请求拼出来GET方式发送短信等签名算出来消息已经成功了一大半。以发送短信为例手上有这些参数ActionSendSms、Version2017-05-25、RegionIdcn-hangzhou、PhoneNumbers手机号、SignName签名、TemplateCode模板ID、TemplateParamJSON串的UrlEncode、Signature刚才算出的签名。做GET请求时curl的写法很直白curl -sS --connect-timeout 10 -m 15 \ https://dysmsapi.aliyuncs.com/?ActionSendSmsVersion2017-05-25RegionIdcn-hangzhouPhoneNumbers13800138000SignName我的运维助手TemplateCodeSMS_123TemplateParam%7B%22keyword%22%3A%22disk%22%7DSignaturexxxx这里我反复强调几点。第一用-sS-s是静默模式不显示进度条但大写S会让Curl在出错时仍然打印错误信息不然脚本排查故障时完全没头绪。第二--connect-timeout和-m必须要有前者限制连接超时为10秒后者限制整个请求最多15秒没有超时的脚本一旦遇上网络抖动就会挂在那儿不动连定时任务都给卡死。第三所有中文和花括号必须URL编码这个可以直接在Shell里用__url_encode函数处理别手写编码。4.2 POST方式与数据体发送的差异有些API尤其是部分国内厂商较新的接入协议要求POST加JSON请求体。这时候curl要变一下curl -sS --connect-timeout 10 -m 15 \ -X POST \ -H Content-Type: application/json \ -H Authorization: acs ... \ -d {phone:13800138000,msg:disk full}注意一个细节如果-d和-X POST一起用数据是放在请求体里的但如果你本意是让参数走Query、数据走Body就得分清楚。很多老手会在shell脚本里图省事把所有参数都拼进URL结果是接口文档上的请求方法反而成了最常见的事情。还有一点-H加Authorization这个Header的时候值里的签名也要动态生成你调试时可以先拿curl -v看请求头输出长了什么样再对照文档报InvalidAuthorization就说明签名格式和道道不匹配。4.3 用-v和-i看响应细节对接任何接口第一步永远是别急着写完整脚本先手工拿一行curl打一遍看返回的是什么。我用得最多的组合是curl -sS -i https://dysmsapi.aliyuncs.com/...-i会把响应头一起打出来能看到HTTP状态码是200还是500有时候接口返回200但业务Code不是OK这说明请求到了服务端但业务校验失败比如签名错误、模板不存在。看完返回体里的Code字段和Message字段就能知道是签名问题、欠费问题还是模板审核中。亲手打一次拿第一手数据比看文档管用多了。5. 把通知能力封装成可以复用的通用Shell脚本5.1 完整脚本骨架下面这个脚本是我现在几台服务器上还在跑的精简版做成了独立的发送函数你拿去改一下密钥和参数就能用。别把密钥写死到脚本里我会用环境变量引用这样脚本即使误发到别人手里也不至于泄露。#!/bin/bash # send_sms.sh —— 通用短信通知函数 # 配置 SMS_AK_ID${SMS_AK_ID:-your_access_key_id} SMS_AK_SECRET${SMS_AK_SECRET:-your_access_key_secret} SMS_SIGN_NAME${SMS_SIGN_NAME:-我的运维助手} SMS_TEMPLATE_CODE${SMS_TEMPLATE_CODE:-SMS_123} SMS_REGION_IDcn-hangzhou SMS_ENDPOINThttps://dysmsapi.aliyuncs.com/ SMS_VERSION2017-05-25 SMS_ACTIONSendSms # URL编码函数 urlencode() { local data$1 printf %s $data | curl -Gso /dev/null -w %{url_effective} --data-urlencode - \ | sed s/.*?// }等一下这个urlencode函数里有一个我踩过的坑。curl -G --data-urlencode可以帮你做URL编码但是结果输出到stderr或者带上额外前缀很麻烦不如直接用纯Shell的编码函数。下面这个用od配合sed的实现相对可靠urlencode() { local data$1 local len${#data} local i0 local c for ((i0; ilen; i)); do c${data:i:1} case $c in [a-zA-Z0-9.~_-]) printf %s $c ;; *) printf %%%02X $c ;; esac done }5.2 签名函数和主发送函数签名函数做的是我前面铺垫的那套流程。参数的字典排序我是用printf配合sort做的这样不依赖Pythonbuild_signature() { local -a args($) local sorted local query local i # 排序 sorted$(printf %s\n ${args[]} | sort -k1) ... }不过把整个签名逻辑完整地塞进博客里篇幅会特别长而且不同厂商之间略有差异。我在实际项目里固定用自己惯用的一套模板用函数分离签名和发送这样换厂商只需改签名那一块。发送函数的核心是拼出最终URL交给curl执行再做响应判断。send_sms() { local phone$1 local template_param$2 # 生成时间戳、随机串、参数拼接、计算签名…… # 最终调用 response$(curl -sS --connect-timeout 10 -m 15 $url) echo $response }我把完整脚本放到GitHub Gist上维护这里把函数的关键逻辑展示出来主要是让你理解封装的层次——配置区、编码区、签名区、请求区四块互相解耦。后面加日志、加重试、加多手机号通知都只需在对应模块上做文章。5.3 和定时任务、监控脚本的组合套路脚本写完真正的价值是挂到监控里去。我常用的三种挂法第一种直接在crontab里配合逻辑。比如备份命令跑完不管成功失败都发通知0 2 * * * /usr/local/bin/backup.sh /usr/local/bin/send_sms.sh 13800138000 {job:backup,status:success} || /usr/local/bin/send_sms.sh 13800138000 {job:backup,status:fail}第二种在Shell脚本需要告警的地方调用函数。比如检查磁盘空间超过85%就发短信usage$(df / | awk NR2 {print $5} | tr -d %) if [ $usage -gt 85 ]; then send_sms 13800138000 {\alert\:\disk\,\usage\:\$usage%\} fi第三种检查某个进程是否活着挂了就发告警并尝试拉起。这个在线上最重要if ! pgrep -f nginx: master /dev/null; then systemctl start nginx send_sms 13800138000 {\alert\:\nginx down\,\action\:\restarted\} fi三种挂法覆盖了定时任务、资源告警、守护进程三类最常用的运维需求。你只要把自己的业务命令嵌套进去等于立刻给所有脚本加了一个通讯器官。5.4 脚本健壮性的几个增强点生产环境里发短信这种事宁可重复通知也别漏通知但又不能无限重试把API频率打爆。我建议加这两个能力一是控制最大重试次数。curl返回非0或响应里没出现Code:OK时重试最多三次中间sleep递增。二是做日志落盘。log_msg() { echo $(date %Y-%m-%d %H:%M:%S) $1 /var/log/sms_sender.log }日志看起来是小事但等哪天短信通道出故障你想复盘时就知道它有多救命了。另外发送结果里有一个BizId这是短信平台返回的业务流水号遇到短信发了但用户没收到这种线上投诉客服大概率会管你要这个BizId来查。所以日志里务必把整个响应保留下来别只存一个OK。6. 常见问题排查与高频踩坑记录6.1 签名错误永远排第一我在群里看到最多的问题就是签名错误实际上99%不是平台问题而是自己代码里某个字符没处理好。整理一张速查表给你照着排查会快很多。问题现象大概率原因排查命令或方法SignatureDoesNotMatch参数没按ASCII排序打印规范化字符串对比官方示例SignatureDoesNotMatchURL编码空格变成检查编码函数空格必须为%20SignatureDoesNotMatch密钥末尾少了新版签名经常要求Secret后面拼TimestampExpired本机时间不准运行date -u看UTC时间同步NTPInvalidTemplateCode模板ID输错或未审核控制台确认模板状态isv.SMS_SIGNATURE_ILLEGAL签名未通过审核检查签名内容与营业执照一致isv.MOBILE_NUMBER_ILLEGAL手机号格式不对或测试号码没白名单看文档要求测试时加测试号遇到签名错误我第一步永远是打开调试日志把待签名的字符串完整打出来拉一个官方签名工具生成的结果和自己脚本的结果对比。对比时重点看排序是否一致、编码是否一致、密钥拼法是否一致。三处都一样签名一定对。6.2 连接超时与RPC失败的处理请求阶段常见的问题还有curl返回56 Recv failure或者超时。这个66号错误码代表的是接收数据失败。网上有个常见场景说curl 56 recv failure: 连接超时定位下来多半是网络出口访问不到目标服务器的端口或者服务器防火墙把响应切断了。我的排查思路是先telnet或nc -vz测试目标域名和443端口能不能通通了再curl仍然失败就换curl -v看卡在哪一步。如果DNS解析出来不对检查/etc/resolv.conf如果IP能通但HTTPS握手失败把-k临时加上做对比生产上别用-k这里只是排查。另外有一种情况是短信平台对并发请求做了限制脚本在循环里发太多请求时会被平台的限流策略把连接断开。这种时候加上随机sleep或者串行发送比反复调超时参数有效得多。6.3 中文和特殊字符编码的连环坑发消息内容里带中文是常态而模板参数是一个JSON字符串比如{keyword:磁盘满}。这个字符串在发送时必须整体做URL编码。很多新手直接拼到URL里结果磁盘两个字在请求里变成了乱码服务端收到的JSON解析失败报InvalidParameter。我的建议很简单写一个urlencode函数先对TemplateParam整体编码再拼进URL。函数的细节可以回头看我5.1节提供的版本。另外有时模板参数里含有单引号和双引号Shell变量引用也要格外小心建议send_sms $phone $template_param时一定要用双引号包住变量否则Shell会把JSON里的空格或特殊字符拆开当多个参数传。6.4 一个关于脚本定时任务环境变量的大坑最后说一个特别隐蔽的问题crontab里运行脚本时环境变量和手工执行时不一样。你的PATH可能是/usr/local/bin:/usr/bin:/bin但在cron环境里可能只有/usr/bin:/bin。如果你的curl或openssl装在/usr/local/bin下手工执行没问题cron里却会报command not found。解决方法是在脚本开头显式设置环境变量或绝对路径export PATH/usr/local/bin:/usr/bin:/bin CURL_BIN/usr/local/bin/curl OPENSSL_BIN/usr/local/bin/openssl有些发行版cron还会用sh而不是bash来执行脚本脚本里如果你写了$((...))这类bash特性在sh环境下可能解析异常。保险起见脚本第一行写#!/bin/bash并且在crontab里用/bin/bash /path/script.sh来调用别直接写脚本路径这算是我被cron坑过好几次之后留下的习惯。7. 从短信推送延伸到更完整的通知体系短信接口跑通之后你会发现自己对通知的胃口变大了。一条短信最多几十个字适合传达发生了什么、要不要处理这种极简信息但具体报错堆栈和日志片段不适合发短信。我在实际使用中通常采用分级策略紧急故障用短信重要但不紧急的用钉钉或企业微信机器人日志级别的信息直接推到个人服务器上的一个Webhook。这个思路的落地方式不复杂把短信函数里那套签名和请求逻辑抽成一层再加上钉钉机器人的webhook发送函数两者都封装成发一条消息的公共函数上层脚本只关心级别。比如我的备份脚本里是这样用的notify info 备份完成 notify error 备份失败请关注notify函数内部按级别决定走短信还是webhook。这样短信通道因为欠费或者审核被卡住时其他通道还能兜底我在运维群里依然看得到告警。再往后你甚至可以把这个脚本包装成系统服务或者放进Docker容器里让其他机器通过HTTP调用。不过一步到位搞微服务反而复杂我建议大多数场景就停留在Shell函数这个粒度够用、好改、不会半夜被自己的架构绕晕。8. 拿这套能力还能做什么扩展短信通知这种能力一旦封装好扩展方向比想象中多。我给几个亲测可靠的方向一是结合inotifywait监控关键配置文件变化。比如nginx的配置文件被人改动了自动发条短信提醒你。这比每天手动比对配置高得多。二是结合进程退出码给压力测试做完成通知。以前跑一个压测要盯半天终端现在脚本用wait配合send_sms跑到结束把QPS和错误率发到你手机上。三是做业务报表的定时推送。每天凌晨统计昨天的订单数、错误数拼成消息发到手机早上一睁眼就能掌握状态。四是把多个脚本的告警聚合到一个公共函数里做频率控制和去重。比如同一个问题五分钟内最多发一条短信避免故障时短信轰炸把你的手机打到关机。每个扩展方向本质上都是把Shell脚本发出通知这个能力嫁接到已有的任务上。你不需要学新的语言也不用重新部署什么只需要在合适的时机调用几行函数整个自动化体系就有了反馈回路。我自己的感受是越是简单直白的工具越能长期保持生命力。Shell脚本加curl的方案可能不炫酷但它在任何一台Linux机器上都能跑不挑环境出了问题还能拿bash -x一行行看执行过程。短信通知这件事要的不是花哨的框架而是稳定送达。用最朴素的工具解决最核心的问题这大概就是运维脚本该有的样子。