一、概述

行云管家平台短信通知目前支持自定义配置短信网关,您可以使用行云管家平台自带的短信通知服务,同时也可以根据您的需要配置使用您自己的短信网关。

二、短信网关开发需求

对于自定义短信网关,您需要按照以下描述进行开发及配置:

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={调用请求唯一标识}&timestamp={时间戳}

其中:

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)中编写您的发送短信代码,即可完成开发。【样例代码下载