一、概述
行云管家平台短信通知目前支持自定义配置短信网关,您可以使用行云管家平台自带的短信通知服务,同时也可以根据您的需要配置使用您自己的短信网关。
二、短信网关开发需求
对于自定义短信网关,您需要按照以下描述进行开发及配置:
1、开发一个Web应用,该Web应用需要提供一个http接口,供行云管家平台调用。Web应用接收到调用请求后,再去调用您自有的短信通知业务代码,发送短信
2、到行云管家管理控制台中,配置启用短信网关,并使用自有短信网关,同时指定网关URL(即Web应用接口调用URL)
三、Web应用开发详述
行云管家平台会以HTTP请求方式来对您的Web应用接口进行调用。
调用时的URL格式、请求头(header)、请求消息的消息体(body)、及接口内部处理过程描述如下。
3.1、Web应用被调用时的URL格式
行云管家平台所发出的每一次HTTP请求都会带上签名信息等相关参数,您的Web应用可以通过验证签名信息来确认请求是否由行云管家平台所发出。
行云管家平台在调用您的Web应用接口时,其调用请求为POST请求,签名信息等内容通过URL参数传递。格式如下:
http://{您的应用接口地址}?accessKeyId={行云管家平台OpenAPI AccessKey}&signature={Web调用请求签名}&version=1&nonce={调用请求唯一标识}×tamp={时间戳}
其中:
1、{您的应用接口地址}: 为您的Web应用接口的调用地址,举例:假设您的Web应用访问IP为192.168.1.100,应用名称为sendMessage,则您的应用接口地址为:192.168.1.100/sendMessage
2、{行云管家平台OpenAPI AccessKey}: 行云管家管理控制台 OpenAPI中的AccessKey信息,可以通过登录行云管家管理控制台查询获得
3、{Web调用请求签名}: 签名信息,行云管家平台会通过一个固定的签名工具对调用请求进行签名,您的Web应用接口在接收到请求后,可以对该签名信息进行校验,以确保调用者是行云管家平台,保障调用安全
4、{调用请求唯一标识}: 行云管家平台每次调用您的Web应用接口时都会带上一个用于标识本次调用的请求标识,每次调用的请求标识都是不同的,您的Web应用可以根据此请求标识来识别请求
5、{时间戳}: 行云管家平台每次调用您的Web应用接口时都会带上一个时间戳,供您的Web应用做请求识别、日志记录等操作
3.2、Web应用被调用时调用请求的请求头(header)
行云管家平台在调用您的Web应用接口时,其请求头(header)消息如下:
Content-type: application/json; charset=UTF-8 Accept: application/json
该请求头(header)消息类似于普通的http请求的请求头消息,不做赘述。
3.3、Web应用被调用时调用请求的消息体(body)
行云管家平台在调用您的Web应用接口时,其请求消息体(body)如下:
{ "phones": ["18000000000"], "template": "SystemUpgrade", "parameters": ["2018-10-01 19:30:00", "2018-10-01 22:30:00"] }
该请求体(body)参数说明如下:
1、phones: 本次发送短信请求对应的短信接收者的手机号码列表,您的Web应用接收到请求后,需要调用您的短信通知业务代码,将短信发送给这些手机号
2、template: 本次发送短信请求所要使用的短信模版名称,您的Web应用接收到请求后,需要获取到该短信模版名称对应的模版内容(是一段文字),并将模版内容中的参数以实际值替换,然后将替换后的内容作为短信内容,发送给手机号码列表中的手机号
3、parameters: 本次发送短信请求对应短信模版中的参数值列表,您的Web应用接收到请求后,需要获取到模版内容(是一段文字),并将模版内容中的参数以本参数值列表中的对应值替换,然后将替换后的内容作为短信内容,发送给手机号码列表中的手机号
3.4、Web应用被调用时内部处理过程
您的Web应用接口在接到调用请求时,其内部处理过程如下:
1、获取请求参数,包括:accessKeyId、请求版本version、请求签名signature、请求唯一标识nonce、请求时间戳timestamp
2、以相关参数配合accessKeySecret,计算得到请求对应的签名值
3、将计算得到的请求签名值与请求参数中所带签名值进行比较,判断请求合法性
4、如果请求合法
4.1、获取请求消息体(body)中各项参数值
4.2、根据请求参数值,结合消息模版,取得要发送的短信消息文本内容
4.3、调用您的短信发送业务代码,将短信消息文本内容发送予相应的手机号
4.4、根据您的实际需要,进行日志记录等其它操作
4.5、将操作结果返回予接口调用者(即行云管家)
5、如果请求不合法
5.1、根据您的实际需要,进行日志记录等操作
5.2、将操作结果返回予接口调用者(即行云管家)
四、获取行云管家平台AccessKey
前述Web应用调用请求URL中,有一个“Web调用请求签名”参数,该参数涉及的accessKeyId、accessKeySecret 是行云管家管理控制台OpenAPI中的AccessKey信息:
五、短信模版
行云管家目前用到了六种短信模版,分别是:验证码模版、会话双因子认证模版、升级通知模版、监控预警模版、后付费资源消费预警模版、告警恢复模版。
短信模版内容分别如下:
5.1、验证码模版
【行云管家】您的验证码是:%code%,该验证码将会在%minute%分钟后过期
5.2、会话双因子认证模版
【行云管家】您正在尝试进行的操作已开启双因子认证,请在%minute%分钟之内输入以下验证码:%code%,如非本人操作,请确认您的账户安全
5.3、升级通知模版
【行云管家】尊敬的用户,行云管家将于%startTime%进行版本升级,届时将暂停服务,请大家提前做好准备,升级工作预计于%endTime%完成,给您带来不便,敬请谅解!
5.4、监控预警模版
【行云管家】您的%resourceType%“%resourceName%”由于%content%而产生预警,请确认预警原因
5.5、后付费资源消费预警模版
【行云管家】您的云账户“%cloudAccount%”由于单日后付费资源消费大于%threshold%而产生预警,请确认预警原因
5.6、告警恢复模版
【行云管家】您的%resourceType%“%resourceName%”%monitorItem%告警恢复,恢复时间:%time%
5.7、会话邀请
【行云管家】您的好友“%user%”邀请您进入Ta的远程会话,请您访问:门户地址/sessionInvite.html,授权码:%authCode%
5.8、待办通知_无验证码
【行云管家】%applicant%提交了%type%:%subject%,请前往行云管家PC端%teamName%团队“我的待办”中对当前审批进行处理
5.9、待办通知
【行云管家】%applicant%提交了%type%:%subject%,如果您同意请将验证码 %code% 告知TA,不同意请直接忽略,详情请登录行云管家
5.10、指令审批
【行云管家】%user%申请%operation%:%command%,如果您同意请将验证码 %code% 告知TA,不同意请直接忽略,详情请登录行云管家
5.11、异地登录提醒
【行云管家】安全提醒:您的账户检测到来自 %ip% (%city%)的异地登录行为。如非本人操作,意味着您的账户可能存在安全隐患
提示:模版中两个%号隔开的是模版变动的内容,这些参数通过接口传递给第三方短信平台。如果您觉得行云管家短信网关配额不够,想用自有短信网关,可以参考我们的模版,关键参数通过接口传递您的自有短信网关上。
六、Web应用开发举例
这里以Java Servlet开发Web应用为例,说明开发您的Web应用的过程。完整代码下载参见 样例代码下载
6.1、创建Java Web工程并编写Servlet
1、首先,您需要创建一个Java Web工程,然后在工程中创建一个Java Servlet,Servlet的doPost()方法内容如下
protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException { response.setContentType("application/json; charset=UTF-8"); PrintWriter out = response.getWriter(); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 版本 String version = request.getParameter("version"); // 本次调用签名值 String signature = request.getParameter("signature"); // 行云管家管理控制台 OpenAPI中的AccessKey信息 String accessKeyId = request.getParameter("accessKeyId"); Map<String, Object> signMap = new HashMap<String, Object>(); signMap.put("timestamp", timestamp); signMap.put("version", version); signMap.put("nonce", nonce); signMap.put("accessKeyId", accessKeyId); // 通过工具类生成签名,与请求中携带的签名值进行比较,看是否相同 String signature0 = SignUtil.sign(null, signMap, "POST", accessKeySecret); Map<String, Object> result = new HashMap<String, Object>(); if (signature == null) { result.put("success", false); result.put("msg", "签名失败"); System.out.println("签名失败"); } else if (signature.equals(signature0)) { // 签名校验成功 // body为短信模版参数,拿到参数去自有短信平台发送短信 BufferedInputStream bis = new BufferedInputStream(request.getInputStream()); ByteArrayOutputStream buf = new ByteArrayOutputStream(); int r = bis.read(); while(r != -1) { buf.write((byte) r); r = bis.read(); } // StandardCharsets.UTF_8.name() > JDK 7 String jsonString = buf.toString("UTF-8"); //System.out.println("json is:"+jsonString); SmsRequest body = JSON.parseObject(jsonString, SmsRequest.class, null); result.put("success", true); result.put("msg", "签名成功"); // 短信内容 String smsText = body.getSMSText(); // 接收短信的手机号码列表 List<String> pns = body.getPhones(); // --------在这里调用您的业务代码,进行短信发送即可---------- start // for (String pn : pns) { String cmd = shellPath + "/sendmessage.sh \"" + smsText + "\" " + pn; ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c", cmd); builder.directory(new File(shellPath)); builder.start(); result.put("success", true); System.out.println("success"); } // --------在这里调用您的业务代码,进行短信发送即可---------- end // } else { // 签名校验失败,非行云管家平台发送请求 result.put("success", false); result.put("msg", "签名失败"); System.out.println("签名失败"); } //out.println(JSON.toJSON(JSON.toJSONString(result))); out.println(JSON.toJSONString(result)); out.flush(); out.close(); }
2、用到的短信模版枚举类“SmsTemplate”代码内容如下:
public enum SmsTemplate { /** * 短信验证码 */ VerifyCode(2),//验证码模版:验证码,超时(分) Session2FA(2),//会话双因子认证:超时(分)、验证码 SystemUpgrade(2),//升级通知:开始时间,结束时间 MonitorAlert(3),//监控预警 CostAlert(2),//后付费资源消费预警 AlertRecover(4),//告警恢复 LoginCmdAudit(4),//登录,指令审批 RemoteLogin(2),//异地登录提醒 PendingTask(4),//待办任务通知 SessionInvite(2),//会话邀请 PendingTaskWithoutCode(4),//代办通知_无验证码 ; private int paramLength;//模版需要的参数长度 SmsTemplate(int paramLength) { this.paramLength = paramLength; } public int getParamLength() { return paramLength; } }
3、用到的请求体消息类“SmsRequest”代码内容如下:
public class SmsRequest { //手机号列表 private List<String> phones = new ArrayList<String>(); //短信模版 private SmsTemplate template; //模版参数 private List<String> parameters = new ArrayList<String>(); public List<String> getPhones() { return phones; } public void setPhones(List<String> phones) { this.phones = phones; } public SmsTemplate getTemplate() { return template; } public void setTemplate(SmsTemplate template) { this.template = template; } public List<String> getParameters() { return parameters; } public void setParameters(List<String> parameters) { this.parameters = parameters; } public String getSMSText() { String sms = ""; switch (template) { case VerifyCode : sms = "【行云管家】您的验证码是:" + parameters.get(0) + ",该验证码将会在" + parameters.get(1) + "分钟后过期。"; break; case Session2FA : sms = "【行云管家】您正在尝试进行的操作已开启双因子认证,请在" + parameters.get(0) + "分钟之内输入以下验证码:" + parameters.get(1) + ",如非本人操作,请确认您的账户安全。"; break; case SystemUpgrade : sms = "【行云管家】尊敬的用户,行云管家将于" + parameters.get(0) + "进行版本升级,届时将暂停服务,请大家提前做好准备,升级工作预计于" + parameters.get(1) + "完成,给您带来不便,敬请谅解!"; break; case MonitorAlert : sms = "【行云管家】您的" + parameters.get(0) + parameters.get(1) + "由于" + parameters.get(2) + "而产生预警,请确认预警原因。"; break; case CostAlert : sms = "【行云管家】您的云账户" + parameters.get(0) + "由于单日后付费资源消费大于" + parameters.get(1) + "而产生预警,请确认预警原因。"; break; case AlertRecover : sms = "【行云管家】您的" + parameters.get(0) + "“" + parameters.get(1) + "”" + parameters.get(2) + "告警恢复,恢复时间:" + parameters.get(3) + "。"; break; case SessionInvite : sms = "【行云管家】您的好友“" + parameters.get(0) + "”邀请您进入Ta的远程会话,请您访问:门户地址/sessionInvite.html,授权码:" + parameters.get(1) ; break; case PendingTaskWithoutCode : sms = "【行云管家】" + parameters.get(0) + "提交了" + parameters.get(1) + ":" + parameters.get(2) + ",请前往行云管家PC端" + parameters.get(3) + "团队“我的待办”中对当前审批进行处理"; break; case PendingTask : sms = "【行云管家】" + parameters.get(0) + "提交了" + parameters.get(1) + ":" + parameters.get(2) + ",如果您同意请将验证码" + parameters.get(3) + "告知TA,不同意请直接忽略,详情请登录行云管家"; break; case LoginCmdAudit : sms = "【行云管家】" + parameters.get(0) + "申请" + parameters.get(1) + ":" + parameters.get(2) + ",如果您同意请将验证码" + parameters.get(3) + "告知TA,不同意请直接忽略,详情请登录行云管家"; break; case RemoteLogin : sms = "【行云管家】安全提醒:您的账户检测到来自" + parameters.get(0) + "(" + parameters.get(1) + ":)的异地登录行为。如非本人操作,意味着您的账户可能存在安全隐患"; break; default : sms = ""; } return sms; } }
4、用到的签名工具类“SignUtil”代码内容如下:
import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class SignUtil { private static final Object LOCK = new Object(); private static Mac macInstance; private static final String ALGORITHM = "HmacSHA1"; public static String sign(String reqURl, Map<String, Object> parameterMap, String method, String accessKeySecret) { if (reqURl == null) { reqURl = "/"; } if (!reqURl.startsWith("/")) { reqURl = "/" + reqURl; } String canonicalizedQueryString = canonicalize(parameterMap); String StringToSign = method + "&" + percentEncode(reqURl) + "&" + percentEncode(canonicalizedQueryString); return computeSignature(accessKeySecret, StringToSign); } public static String sign(Map<String, Object> parameterMap, String method, String accessKeySecret) { return sign("/", parameterMap, method, accessKeySecret); } private static String canonicalize(Map<String, Object> parameterMap) { StringBuilder sb = new StringBuilder(); String[] parameterNames = parameterMap.keySet().toArray(new String[parameterMap.size()]); Arrays.sort(parameterNames); int count = 0; for (String paramName : parameterNames) { count++; sb.append(percentEncode(paramName)); Object paramValue = parameterMap.get(paramName); if (paramValue != null) { sb.append("=").append(percentEncode(String.valueOf(paramValue))); } if (count < parameterNames.length) { sb.append("&"); } } return sb.toString(); } private static final String DEFAULT_ENCODING = "UTF-8"; private static String percentEncode(String value) { try { return value != null ? URLEncoder.encode(value, DEFAULT_ENCODING).replace("+", "%20").replace("*", "%2A").replace("%7E", "~") : null; } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String computeSignature(String key, String data) { try { byte[] signData = sign(key.getBytes(DEFAULT_ENCODING), data.getBytes(DEFAULT_ENCODING)); return ""; } catch (UnsupportedEncodingException ex) { throw new RuntimeException("Unsupported algorithm: " + DEFAULT_ENCODING, ex); } } private static byte[] sign(byte[] key, byte[] data) { try { if (macInstance == null) { synchronized (LOCK) { if (macInstance == null) { macInstance = Mac.getInstance(ALGORITHM); } } } Mac mac = null; try { mac = (Mac) macInstance.clone(); } catch (CloneNotSupportedException e) { mac = Mac.getInstance(ALGORITHM); } mac.init(new SecretKeySpec(key, ALGORITHM)); return mac.doFinal(data); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException("Unsupported algorithm: " + ALGORITHM, ex); } catch (InvalidKeyException ex) { throw new RuntimeException("Invalid key: " + key, ex); } } private static String buildCanonicalizedResource(String resourcePath, Map<String, String> parameters) { if (!resourcePath.startsWith("/")) { throw new RuntimeException("Resource path should start with '/'"); } StringBuilder builder = new StringBuilder(); builder.append(resourcePath); if (parameters != null) { String[] parameterNames = parameters.keySet().toArray( new String[parameters.size()]); Arrays.sort(parameterNames); char separater = '?'; for (String paramName : parameterNames) { builder.append(separater); builder.append(paramName); String paramValue = parameters.get(paramName); if (paramValue != null) { builder.append("=").append(paramValue); } separater = '&'; } } return builder.toString(); } }
6.2、打包Web工程并发布至Web容器
1、将上述Web工程打包为可发布到Web容器的应用,例如:打成war包
2、将打包输出物发布到Web容器中,例如:发布至Tomcat或Jetty中
3、测试并保证上述开发的Web应用接口可正常调用
6.3、配置行云管家短信网关
1、登录至行云管家管理控制台
2、配置短信网关为您的自有短信网关
通过上述方法进行开发及配置,即可使用您的自有短信网关,在行云管家中进行验证码等内容的短信发送。
6.4、样例代码下载
您可以在这里下载上述样例代码,下载后将其解压,会得到一个java web工程文件夹,将该工程导入到您的集成开发环境(IDE)中,并在Servlet类(SmsServlet)中编写您的发送短信代码,即可完成开发。【样例代码下载】