ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Shell+Curl调用短信API:给Linux服务器告警加上短信通知

Shell+Curl调用短信API:给Linux服务器告警加上短信通知 服务器凌晨三点挂了没人知道第二天早上业务方找上门来这种画面相信搞运维和搞独立开发的朋友都不陌生。我自己手里也有好几台Linux机器跑着一堆定时脚本、数据任务、备份任务出问题不可怕可怕的是出问题的时候你根本不知情。后来我把所有关键任务都加了一个“消息出口”——脚本执行出错就立刻发短信到我手机而这个出口的实现方式就是今天要聊的东西在Linux脚本里用Shell配合Curl命令快速调用短信API。这篇内容不是简单丢一段代码给你而是会从“为什么要这么干”“签名是怎么算出来的”“curl参数为什么这么选”“线上发不出去怎么排查”这几个角度完整拆一遍。写完你不仅能直接在项目里用还能在面试或者跟同事扯方案的时候把底层原理讲得明明白白。适合三类人看一类是做运维的需要给监控脚本加告警通道一类是后端开发临时需要一个“脚本通知能力”但不想引入一堆依赖还有一类是个人站长手里几台服务器想用最低成本搞定通知。无论哪一类这个方案的核心优势都很直接Linux自带的curl加一个简单的Shell脚本零第三方依赖改起来也快。1. 先说结论这玩意儿到底解决什么问题1.1 短信通知在系统里的位置很多人一听“发短信”就觉得要接服务商SDK、要写Java/Python接口、要搞异步队列其实那是营销短信平台的玩法。对于“服务器告警”“任务跑完通知我”“定时脚本异常了告诉我”这种低频、高重要度的场景你根本不需要那么重的东西只需要一个能打的HTTP请求就够了。短信API本质上就是一个公开的HTTP接口你传手机号、模板编号、模板参数、签名它返回一个JSON告诉你成功还是失败。这种接口用curl就能调压根不需要SDK。1.2 为什么是Shell加Curl而不是Python或者写个独立程序我知道肯定有人会说“我用Python写个脚本调用requests库不也很快吗”。但你想过没有你要通知的那个场景本身就在Shell世界里crontab里跑的备份脚本、你手动执行的一条部署命令、一台IoT设备上的初始化脚本它们本来就在bash环境下。如果为了发个短信去引入Python环境、装库、再做异常处理成本反而上去了。curl是Linux系统自带的老牌工具几乎所有发行版都预装了不需要额外安装任何东西写法也直白——就是发一个POST请求。更重要的是Shell脚本里能直接把前后逻辑串起来磁盘检查发现超过阈值当场调用发送函数三行代码搞定通知。这种“顺手粘上去”的爽感是重型SDK给不了的。1.3 这个方案适合什么场景不适合什么场景适合单条告警通知、定时任务结果通知、脚本报错通知、设备离线通知、低频业务提醒。这些场景一天顶多几十条对吞吐量没有要求对发送延迟也能接受几秒。不适合验证码发送、营销群发、需要高并发批量的业务短信。那种场景请老老实实用服务商SDK走正式的业务流程不要用Shell脚本去硬扛。明确了这个边界之后我们再看具体实现你会发现短信API调用的核心难点根本不在curl而在“签名”。2. 动手前的三个关键认知2.1 短信API的本质就是一个HTTP请求你现在不妨把短信API想象成一个窗口你递进去一张单子上面写着“把这个内容发给这个手机号”窗口工作人员核验你的身份之后帮你把短信发出去。这张单子就是HTTP请求窗口地址就是API域名和接口路径工作人员核验身份的过程就是鉴权。国内主流的云厂商短信API比如阿里云、腾讯云、华为云基本都是这个模式只是请求格式和签名算法略有差异思路完全一致。以阿里云短信为例接口地址是dysmsapi.aliyuncs.com调用方式是向这个地址POST一个表单里面带上Action参数、你的AccessKeyId、短信签名、模板编号、模板参数值、时间戳、随机数等一堆“公共参数”服务端校验通过后执行发送然后返回JSON。整个过程里唯一需要动脑子的是下面这个鉴权三件套。2.2 鉴权三件套AccessKeyId、AccessKeySecret与签名任何云厂商的API都绕不开两个东西AccessKeyId和AccessKeySecret。你可以简单理解成AccessKeyId是你的工号别人看见工号知道你是谁AccessKeySecret是你的印章千万不要给别人看到谁拿到印章谁就能以你的名义发短信。那么签名是什么呢你可以这样类比签名是“拿印章盖出来的一张条子”。具体到技术上就是你把这次请求的所有参数按规则拼成一个字符串然后用AccessKeySecret作为密钥对这段字符串做HMAC-SHA1哈希得到的结果再做Base64编码最终得到一段看起来像乱码的字符串。把这个字符串作为Signature参数一并传给服务端服务端用同样的算法自己算一遍如果结果一致就说明请求确实来自持有AccessKeySecret的人。这里有一个非常重要的点同一个请求参数只要你改了任何一个值签名就完全变了。所以签名本质上起到了“防篡改”的作用这也是我为什么建议你在脚本里保留完整的签名计算过程而不是图省事写死在请求里。2.3 签名和模板要提前过审这个最容易踩坑我第一次用短信API的时候犯了一个特别蠢的错误——文档都没细看直接用服务商控制台给的测试签名和测试模板去调接口结果返回isv.SMS_SIGNATURE_ILLEGAL。后来才明白国内短信服务商要求你要发短信必须先申请一个“签名”比如你的App名称或公司名称再创建一个“模板”短信正文支持用变量占位两项审核通过之后才能调用接口。审核通常需要几分钟到几小时不等取决于服务商和你提交的内容。签名不能乱写一般是品牌名、App名或者网站名模板正文里需要变量的地方用${code}这种占位符。这个环节虽然不涉及代码但如果你没提前申请后面代码写得再漂亮也白搭。所以动手写脚本之前请先检查你的账号下有没有审核通过的签名和模板。3. Shell脚本调用短信API的完整实现3.1 一个能直接跑的通用函数我用阿里云短信API作为例子因为它的签名算法比较典型其他厂商的API你只要把域名和参数名替换一下核心逻辑可以直接复用。这个函数你复制回去改掉AccessKeyId和AccessKeySecret就能跑。#!/bin/bash # send_sms.sh - 阿里云短信通知脚本 SMS_ACCESS_KEY_IDLTAI5tXXXXXXXXXXXXXXXX SMS_ACCESS_KEY_SECRETyour_secret_here SMS_SIGN_NAME你的签名 SMS_TEMPLATE_CODESMS_123456789 SMS_REGION_IDcn-hangzhou # URL编码函数按RFC3986规则统一用xxd做字节级转换 urlencode() { local input$1 printf %s $input | xxd -p | tr -d \n | sed s/\(..\)/%\1/g | sed s/%2d/-/g; s/%5f/_/g; s/%2e/./g; s/%7e/~/g } # 发送短信函数 # 用法: send_sms 13800138000 {code:123456} send_sms() { local phone$1 local template_param$2 local timestamp local nonce timestamp$(date -u %Y-%m-%dT%H:%M:00Z) nonce$(date %s%N) # 准备参数行keyvalue用printf按行输出通过sort按字典序排序 local params params$(printf %s\n \ AccessKeyId$(urlencode $SMS_ACCESS_KEY_ID) \ ActionSendSms \ FormatJSON \ PhoneNumbers$(urlencode $phone) \ RegionId$SMS_REGION_ID \ SignName$(urlencode $SMS_SIGN_NAME) \ SignatureMethodHMAC-SHA1 \ SignatureNonce$nonce \ SignatureVersion1.0 \ TemplateCode$SMS_TEMPLATE_CODE \ TemplateParam$(urlencode $template_param) \ Timestamp$(urlencode $timestamp) \ Version2017-05-25 \ | sort) # 拼成key1value1key2value2...形式的规范化查询字符串 local canonical_query canonical_query$(printf %s $params | paste -sd -) # 构造待签名串: POST%2F再次URL编码后的规范化查询字符串 local string_to_sign string_to_signPOST$(urlencode /)$(urlencode $canonical_query) # 计算签名: HMAC-SHA1, 密钥是 AccessKeySecret, 结果Base64 local signature signature$(printf %s $string_to_sign | openssl dgst -sha1 -hmac $SMS_ACCESS_KEY_SECRET -binary | base64) # 发送请求 local resp resp$(curl -s -X POST https://dysmsapi.aliyuncs.com/ \ --data-urlencode Signature$signature \ --data $canonical_query \ --connect-timeout 10 \ --max-time 30) echo $resp }调用方式很简单你把它保存成send_sms.sh然后source进来或者直接在脚本里执行source ./send_sms.sh result$(send_sms 13800138000 {code:123456}) echo $result返回的JSON长这样{Message:OK,RequestId:12345-6789,Code:OK,BizId:123456}看到Code:OK就说明发送成功了。3.2 签名算法逐项拆解这一步看懂就全通了上面那段代码里最不好理解的就是签名计算我来把它拆成几块讲清楚。第一块是规范化查询字符串。云厂商的要求是所有公共参数按参数名ASCII码升序排序参数名和参数值都要做URL编码然后用连接。比如排序前参数列表是乱的排序后变成AccessKeyId...、Action...、FormatJSON这样依次排列。为什么要排序因为签名是一个完整的字符串如果服务端和你客户端拼接的字符串内容不一致签名就永远对不上。排序就是为了让双方的拼接顺序完全一致。第二块是URL编码这也是最容易出错的地方。阿里云要求的是RFC3986编码规则除了字母、数字以及-、_、.、~这4个保留字符之外其他字符一律编码成%XX形式空格编码成%20而不是。很多人踩坑就踩在这里——用curl --data-urlencode时它内部会正确编码但自己拼签名串时用了普通的encodeURIComponent逻辑导致空格变、中文没处理好签名永远验证失败。我上面给的urlencode函数用的是字节级转换先把字符串用xxd转成十六进制再统一加%前缀最后把RFC3986允许的4个字符还原。这种方式对中文、英文、数字、特殊字符都一视同仁也不会因为Shell的环境locale不同产生差异。实测在常见Linux发行版上都很稳。第三块是待签名串的结构。阿里云的签名串格式是固定的HTTPMethod 编码后的路径 编码后的规范化查询字符串具体到代码里就是POST%2F再次URL编码后的canonical_query。注意这个“再次编码”的细节整个查询字符串要先拼好然后对整个字符串再做一次URL编码。也就是说前面已经编码过的%在第二次编码时又变成了%25。我第一次实现的时候把这个细节漏了结果每次签名都对不上后来仔细看文档才发现是“对规范化后的查询字符串再进行一次编码”不是直接拿拼好的串去签。第四块是HMAC-SHA1计算。Linux自带的openssl命令就能做不需要装额外组件。密钥由AccessKeySecret加上一个组成算法是HMAC-SHA1输出转Base64。注意这里有个隐含的坑如果你的AccessKeySecret里本身含有特殊字符一定要看清楚密钥拼接时有没有被错误处理好在一般密钥都是字母数字组合不太会遇到。3.3 curl参数为什么这样选而不是图省事直接发我看到过很多简化版的示例代码就一行curl -X POST https://dysmsapi.aliyuncs.com/?ActionSendSmsPhoneNumbersxxx把AccessKeyId明文拼接在URL里连签名都没有这种只能做技术验证不能上生产。我上面的代码里有几个参数选择是经过考虑的。-s是静默模式不让curl把进度条和额外信息打到标准输出方便直接捕获请求结果。-X POST指定请求方法阿里云短信接口推荐POST方式参数放请求体里避免超长URL。--data-urlencode Signature$signature这一段是为了让curl帮我对Signature的值做一次URL编码——因为Base64产生的、/、这些字符在URL里会被解释成特殊含义如果不编码服务端收到的值就变了。--connect-timeout 10和--max-time 30这两个超时参数必须加。短信接口如果不可用你的告警脚本可不能一直卡在那等响应该放弃就放弃日志里记一笔就行不然告警系统本身也会变成故障源。还有一个细节参数名本身我建议用--data-urlencode但参数值如果已经是编码过的比如canonical_query里的所有值都已经是%XX形式就要用--data直接传否则--data-urlencode会对%再做一次编码把数据变成双重重编码服务端解析时就乱了。4. 把通知函数塞进现有脚本定时任务、重试与防抖4.1 接入crontab让脚本出错了自动喊人写好了发送函数最自然的用法就是配合crontab做定时巡检。比如你有一个磁盘空间检测脚本/usr/local/bin/check_disk.sh每隔5分钟跑一次发现某个分区超过90%就发短信。你只需要在脚本里source发送函数然后在检测逻辑触发的地方调用即可#!/bin/bash source /usr/local/bin/send_sms.sh threshold90 use_percent$(df -h / | awk NR2 {print $5} | sed s/%//) if [ $use_percent -gt $threshold ]; then send_sms 13800138000 {alert:disk high, please check} fi然后把脚本挂到crontab*/5 * * * * /usr/local/bin/check_disk.sh /var/log/check_disk.log 21这里有个小技巧短信内容不要写太细因为短信模板审核要求正文里不能放太宽泛的内容一般写成“您的服务器磁盘占用已超过阈值请登录查看”这种固定话术再把具体数据放到模板参数里。4.2 网络抖动是常态发送失败要有重试机制短信API本质是HTTP请求网络不可能永远稳定。我自己就遇到过深夜告警触发时刚好赶上API网关微微抖动第一次请求超时丢包结果这条重要告警就没了。所以真正上生产前务必加一层简单重试。send_sms_with_retry() { local phone$1 local param$2 local attempt local resp for attempt in 1 2 3; do resp$(send_sms $phone $param) if echo $resp | grep -q Code:OK; then return 0 fi echo [$(date)] send_sms failed, attempt$attempt, resp$resp /var/log/sms_notify.log sleep $((attempt * 2)) done return 1 }sleep $((attempt * 2))是一种朴素的指数退避第一次失败等2秒第二次失败等4秒第三次失败等6秒。重试三次基本能扛过大部分瞬时抖动。注意判断成功的时候不要只看HTTP状态码而是看返回JSON里的Code字段因为业务失败时HTTP状态码也是200但Code不是OK。4.3 防止一分钟内收到几十条轰炸短信锁文件与去重磁盘告警这种场景有个麻烦如果你没处理磁盘持续超阈值crontab每5分钟触发一次你就每5分钟收到一条短信晚上能被活活吵醒。所以一定要做去重和防抖。最简单的方式是锁文件。比如你想让同一个告警在30分钟内只发一次可以这么写alert_lock/tmp/disk_alert.lock if [ -f $alert_lock ]; then exit 0 fi touch $alert_lock # 发送短信 send_sms_with_retry 13800138000 {alert:disk high} # 30分钟后自动释放 nohup sh -c sleep 1800 rm -f $alert_lock 思路很简单锁文件存在就直接退出不存在则创建并发送同时起一个后台任务过30分钟删除锁文件。这里要注意如果你把脚本放在快速循环里nohup后台任务的清理方式理论上可行但更稳的做法是手动把告警恢复后主动删除锁文件这样收到“恢复通知”后锁文件也被清掉避免30分钟内出现新告警时被屏蔽。4.4 日志与调试把每次请求写清楚别等出事了再猜我在生产脚本里一定会加一行日志把每次短信通知的时间、接收号码、返回结果记到一个固定文件里。这不是为了好看是真到了排查问题时没有日志全靠猜效率低到想哭。echo [$(date %F\ %T)] phone$phone resp$resp /var/log/sms_notify.log顺便提醒一句不要日志里打印AccessKeySecret哪怕你自认为是内网服务器也不行。密钥泄露是短信API最大的风险攻击者拿到你的密钥就能拿你的账号发短信烧的是你的钱。正确做法是把AccessKeyId和AccessKeySecret放在一个只有root可读的配置文件里脚本运行时source进来这样脚本本体可以放进版本管理密钥不会泄露。5. 常见问题排查速查表5.1 调用短信API返回的错误码我在实际使用中遇到的API报错按出现频率排序主要就是下面这几种。我把常见问题整理成了一份速查表建议复制到你的运维笔记里。返回错误含义排查方向SignatureDoesNotMatch签名不一致服务端计算的签名和请求里的签名对不上检查签名串拼接顺序、URL编码规则、密钥是否错误重点检查是否忘了对规范化查询字符串做第二次编码InvalidTimeStamp.Expired时间戳无效或过期服务器时间是否不准执行date -u看UTC时间必要时配置NTP同步时间偏差超过15分钟就会报这个错isv.SMS_SIGNATURE_ILLEGAL短信签名不合法或未审核通过去控制台确认签名审核状态确认签名名称拼写无误isv.MOBILE_NUMBER_ILLEGAL手机号格式非法检查号码是否11位、是否包含空格或86前缀部分服务商不支持国际号码isv.BUSINESS_LIMIT_CONTROL触发业务流控短信接口有QPS限制降低发送频率或者拆分请求isv.AMOUNT_NOT_ENOUGH余额不足去控制台充值TemplateParam相关报错模板参数与模板内容不匹配检查JSON格式是否正确键名是否和模板里的${变量名}一致这里特别说一下SignatureDoesNotMatch这是大家第一次接短信API时遇到最多的坑。我建议你在调试阶段临时加一段输出把string_to_sign打印出来然后去服务商的签名调试工具里粘贴进去对比多试几次就能找到差异。常见的差异集中在参数排序不对、编码规则不对、拼串时多了或少了、没有对整个查询串做二次编码。5.2 curl本身的报错怎么判断除了API业务层的报错curl在网络层也会报一堆问题。这些报错有些是服务器环境问题有些是外部网络问题。curl: (28) Connection timed out表示TCP连接建立超时常见原因服务器出方向网络受限、API域名解析出的IP无法访问、本地防火墙拦截。你可以先用curl -v看详细过程看卡在DNS解析、TCP建连还是TLS握手然后用telnet或者nc单独测一下目标端口通不通逐步定位。curl: (56) Recv failure: Connection reset by peer这类错误通常是服务端主动断开连接常见原因是TLS版本不兼容或者请求体格式不对被网关直接丢弃可以尝试加--tlsv1.2或者检查Content-Type头。如果是复杂的网络环境问题先停掉所有复杂的链路配置直接用最简单的HTTP请求不带任何证书验证做连通性测试确认基础网络没问题后再还原配置。绝大多数“curl报错发不出去”其实都是网络层问题而不是API参数问题。# 快速连通性测试 curl -v --connect-timeout 5 https://dysmsapi.aliyuncs.com/ -o /dev/null如果这条命令能走完TLS握手并返回HTTP响应说明网络通如果卡住先解决网络再回来讨论参数。5.3 Shell脚本里的中文、引号和转义坑中文内容在Shell短信脚本里是个大坑。首先是脚本文件本身编码如果你在Windows上编辑了脚本再传到Linux上文件可能是GBK编码或者带CRLF换行符执行时中文模板参数就会乱掉最后服务端收到乱码短信内容要么乱码要么报错。建议统一用UTF-8编码并在脚本开头加一行export LANGen_US.UTF-8避免locale问题。其次是单引号和双引号的陷阱。模板参数是一个JSON字符串里面既有单引号又有双引号。在Shell里传JSON字符串外层用单引号包住是安全的因为JSON里只有双引号没有单引号比如{code:123456}。一旦模板参数里自己也包含单引号就要考虑转义或者改用数组方式拼接。我的经验是模板参数越简单越好尽量只传数字、字母和短字符串不要传复杂嵌套结构。还有一个很隐蔽的坑短信平台对模板变量的长度有限制不同厂商不一样但一般单个变量不超过20个字符。别把一长段日志塞进参数里那样不仅可能被截断还可能触发模板不匹配。6. 一些场景扩展让这个脚本更有价值6.1 多手机号通知与收件人配置实际运维中往往不只通知一个人。你可以把接收人写成一个以空格分隔的列表在循环里逐条调用这样新同事入职加个手机号就行不用改脚本逻辑。NOTIFY_PHONES13800138000 13900139000 for phone in $NOTIFY_PHONES; do send_sms_with_retry $phone {alert:deploy failed} done但注意你每发一条都是要钱的告警群发阈值要克制一般两三个人足够别把整个团队都拉进去。6.2 其他云厂商短信API怎么适配如果你用的是腾讯云短信核心思路一样只是签名算法从HMAC-SHA1换成了TC3-HMAC-SHA256请求头里要加X-TC-Action等参数URL也变成cvc.tencentcloudapi.com。用华为云则是OpenStack风格的签名。刚接触不同服务商时建议先看官方文档里的“签名方法”段落把“参数排序编码拼接哈希”这个模式吃透你会发现各家差别其实就是算法名和拼串格式的不同没有本质区别。6.3 配合企业微信或钉钉机器人做双通道短信虽然可靠但成本比IM通知高不少。我现在的做法是严重告警走短信加企业微信机器人双通道普通告警只发企业微信。这样做既保证了“重要事情一定能找到人”又把短信费用控制在合理范围。企业微信机器人比短信API更简单一个webhook地址加curl就能发这个大家有兴趣可以自己去接。7. 写到最后的一点个人体会聊了这么多技术细节最后说点实际操作层面的心得。短信通知这件事最怕的不是代码写不出来而是把它当成一个“补丁”随意粘贴。我踩过几次坑之后现在给自己定了几个原则密钥永远放配置文件且进不了版本库发送函数必须支持日志输出和重试告警一定要分级同一类告警必须做防抖短信模板先想清楚再申请不要拿“测试内容”糊弄审核。另外还有一个实用技巧在开发阶段别真的给自己发短信可以在发送函数里加一个环境变量开关比如SMS_DRY_RUN1时只打印请求内容而不实际发送这样调试脚本逻辑时既不会花钱也不会打扰别人。等到确认万事俱备再把开关关掉。最后想说的是Shell脚本调用短信API看着简单但它把“系统自动化”和“人为干预”这两件事恰到好处地连在了一起。服务器出问题不可怕怕的是出了问题没人知道。你手里的每一台Linux服务器其实都值得拥有这样一个小小的喊话能力。希望这篇文章能帮你少走几步弯路把这个能力稳稳当当地加到自己的工具箱里。
返回列表