应用接入阿里云短信服务

背景

       在日常开发中,我们可能会遇到短信验证之类的需求,这也是我们使用各类app或系统中比较常见的。在对比了各个平台提供的短信服务后,从价格,稳定性,接入便捷性进行考虑,最终选择了阿里云所提供的短信服务,之前在做课设的时候接入过,但是当时没有作总结,此次毕设也有该需求,在完成之后做以下总结。

开通应用

开通阿里云账号,选择短信服务,开通该应用

应用接入阿里云短信服务

作为第三方短信服务,阿里云提供了多种语言丰富的sdk支持,能够很方便的帮助我们集成常用的接口,加快开发速度。

选择sdk参考,安装java sdk,阿里云官方推荐通过maven形式引入依赖,仓库坐标如下。(原本官方提供的是4.1.0版本,貌似有bug,在开发中执行 IAcsClient client = new DefaultAcsClient(profile);  会报NotSuchMethodException异常,排查了很久无果,切换成4.0.6版本,成功启动)

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>4.0.6</version>
</dependency>

在安装和使用阿里云Java SDK前,确保已经:

  • 安装Java环境。阿里云Java SDK要求使用JDK1.6或更高版本。
  • 已经注册阿里云账号并生成访问访问**(AccessKey)。

创建AccessKey

阿里云官方对AccessKey的说明,概括起来,就是调用所有 API的凭证/验证信息(比如我们待会要调用的短信服务API)

应用接入阿里云短信服务

AccessKey信息可以在安全信息管理控制台看到,这里还提供了一种子账号Accesskey的方式,通过权限管理降低AccessKey泄露的风险

应用接入阿里云短信服务

添加短信签名和模板

打开短信服务控制台,选择国内消息,添加短信签名和模板,按照自己业务需求和系统指引填写,审核通过后即可使用

emmm,现在一点半,工作人员应该还没上班,显示待审核

应用接入阿里云短信服务

在application.yml中配置服务参数

配置如下:

应用接入阿里云短信服务

通过spring ioc将配置装载到spring bean 中去

/**
 * 阿里云短信服务配置
 *
 * @author [email protected]
 * 2019-04-01 15:30
 * @version 1.0.0
 */

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.msg")
public class AliyunMsgConfig {

    /**
     * 阿里云短信API  是否开启短信接口,0为开启(收费接口),1为关闭,2为开启模拟短信(免费接口)
     */
    private Integer openMsg;


    /**
     * 开发者访问id
     */
    private String accessKeyId;

    /**
     * 开发者访问**
     */
    private String accessKeysecret;

    /**
     * 短信签名
     */
    private String signName;

    /**
     * 短信模板
     */
    private String templateCode;

    /**
     * 随机验证码的范围-最小值
     */
    private Integer min;

    /**
     * 随机验证码的范围-最大值
     */
    private Integer max;

    /**
     * 产品域名,开发者无需替换
     */
    private String domain;

    /**
     * 版本日期
     */
    private String version;

    /**
     * 行为
     */
    private String action;
}

编写短信发送服务

阿里云官方提供了短信发送服务的demo,根据自己实际情况进行改写。

将配置类注入进来,这里使用了自定义springUtil的形式完成注入,采用@Autowired的形式发现始终无法完成初始化,在idea中也看到了AliyunMsgConfig成功装载到spring容器中了,尝试指定bean进行加载也无法解决,emmm有点奇怪,具体原因有待考察。所以最后采用 springUtil 帮助完成了配置的初始化工作。

private AliyunMsgConfig aliyunMsgConfig = SpringUtil.getBean(AliyunMsgConfig.class);

附springUtil代码:

/**
 * 自定义Spring相关工具类
 *
 * @author [email protected]
 * 2019-02-20 10:55
 * @version 1.0.0
 */
@Component
@Slf4j
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
        log.info("SpringUtil loaded into spring bean successed : {}", SpringUtil.applicationContext);
    }
    //======获取spring bean=======

    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过name获取Bean
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

业务需要,定义一个存放随机验证码map集合,防止多个手机号同时注册导致验证码混乱(覆盖)。注意,像这类公共变量存放,常规操作应该是放在缓存中(比如redis, mongdb等),若通过静态变量存储,当类被卸载的时候,静态变量的生命周期随之结束,我们存储的信息也会随之丢失。

    /**
     * 随机验证码map集合,防止多个手机号同时注册导致验证码混乱
     */
    private static Map<String, String> myAuthCodeMap = Maps.newHashMap();

发送短信,这里设置了三种状态,是否开启短信接口(实际发送短信接口需要花钱,所以在配置上面做了个开关=_=,土豪请随意~):0为开启(收费接口),1为关闭,2为开启模拟短信(免费接口),均为业务需要进行的自定义,与官方API无关

public int sendMsg(String phoneNumbers) {
        if (1 == aliyunMsgConfig.getOpenMsg()) {
            return ErrorCode.MSG_TURN_OFFED;
        }
        if (2 == aliyunMsgConfig.getOpenMsg()) {
            this.buildAuthCode(phoneNumbers);
            return ErrorCode.SIMULATION_MSG;
        }
        if(0 == aliyunMsgConfig.getOpenMsg()) {
            return this.doMsgSend(phoneNumbers);
        }else{
            return ErrorCode.SYSTEM_ERROR;
        }
    }

核心发送逻辑

private int doMsgSend(String phoneNumbers){
        IClientProfile profile = DefaultProfile.getProfile("default",
                aliyunMsgConfig.getAccessKeyId(),
                aliyunMsgConfig.getAccessKeysecret());
        IAcsClient client = new DefaultAcsClient(profile);
        String authCode = this.buildAuthCode(phoneNumbers);
        CommonRequest request = buildCommonRequest(phoneNumbers, authCode);
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            return ErrorCode.NO_ERROR;
        } catch (ServerException e) {
            log.warn("【阿里云短信发送异常】");
            e.printStackTrace();
            return ErrorCode.SYSTEM_ERROR;
        } catch (ClientException e) {
            log.warn("【阿里云短信发送异常】");
            e.printStackTrace();
            return ErrorCode.SYSTEM_ERROR;
        }
    }

构造发送请求(官方sdk的CommonRequest类没有添加@Builder注解,=_= ,初始化对象时只能一个个set,令人窒息的操作)

private CommonRequest buildCommonRequest(String phoneNumbers, String code) {
        CommonRequest request = new CommonRequest();
        //request.setProtocol(ProtocolType.HTTPS);
        request.setMethod(MethodType.POST);
        request.setDomain(aliyunMsgConfig.getDomain());
        request.setVersion(aliyunMsgConfig.getVersion());
        request.setAction(aliyunMsgConfig.getAction());
        request.putQueryParameter("PhoneNumbers", phoneNumbers);
        request.putQueryParameter("TemplateCode", aliyunMsgConfig.getTemplateCode());
        request.putQueryParameter("SignName", aliyunMsgConfig.getSignName());
        request.putQueryParameter("TemplateParam", "{\"code\":" + code + "}");
        return request;
    }

生成预发送的验证码信息

/**
     * 生成6位随机验证码
     *
     * @return
     */
    public String buildAuthCode(String phoneNumbers) {
        Integer max = aliyunMsgConfig.getMax();
        Integer min = aliyunMsgConfig.getMin();
        Integer authCode = (new Random().nextInt(max) % (max - min + 1) + min);
        myAuthCodeMap.put(phoneNumbers, authCode.toString());
        log.info("手机号 "+phoneNumbers+" 短信验证码:"+authCode);
        return authCode.toString();
    }

编写一些后门服务用于开发调试方便

/**
     * 获取当前的6位随机验证码
     *
     * @return
     */
    public static String getMyAuthCode(String phoneNumbers) {
        return myAuthCodeMap.get(phoneNumbers);
    }

    /**
     * 清除所有的临时随机验证码
     */
    public static void cleanMyAuthCodeMap() {
        myAuthCodeMap.clear();
    }

    /**
     * 清除指定号码的验证码
     * @param phoneNumber
     */
    public static void cleanMyAuthCodeMapByKey(String phoneNumber){
        myAuthCodeMap.remove(phoneNumber);
    }

    /**
     * 获取验证码集合,仅供开发测试!客户端不可调用!
     *
     * @return
     */
    public static Map<String, String> getMyAuthCodeMap() {
        return myAuthCodeMap;
    }

短信发送服务完整代码:(注:这里本类实例bean加载到spring容器中设置了懒加载@Lazy,是为了防止springUtil在获取AliyunMsgConfig bean实例的时候,AliyunMsgConfig还没装载完毕,导致找不到指定bean对象而抛出异常)

package cn.jyycode.common.message;

import cn.jyycode.common.config.AliyunMsgConfig;
import cn.jyycode.common.constant.ErrorCode;
import cn.jyycode.common.utils.SpringUtil;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 阿里云短信服务
 *
 * @author [email protected]
 * 2019-04-01 15:49
 * @version 1.0.0
 */
@Slf4j
@Component
@Lazy
public class CommonRpc {

    private AliyunMsgConfig aliyunMsgConfig = SpringUtil.getBean(AliyunMsgConfig.class);

    /**
     * 随机验证码map集合,防止多个手机号同时注册导致验证码混乱
     */
    private static Map<String, String> myAuthCodeMap = new HashMap<String, String>();

    public int sendMsg(String phoneNumbers) {
        if (1 == aliyunMsgConfig.getOpenMsg()) {
            return ErrorCode.MSG_TURN_OFFED;
        }
        if (2 == aliyunMsgConfig.getOpenMsg()) {
            this.buildAuthCode(phoneNumbers);
            return ErrorCode.SIMULATION_MSG;
        }
        if(0 == aliyunMsgConfig.getOpenMsg()) {
            return this.doMsgSend(phoneNumbers);
        }else{
            return ErrorCode.SYSTEM_ERROR;
        }
    }

    private int doMsgSend(String phoneNumbers){
        IClientProfile profile = DefaultProfile.getProfile("default",
                aliyunMsgConfig.getAccessKeyId(),
                aliyunMsgConfig.getAccessKeysecret());
        IAcsClient client = new DefaultAcsClient(profile);
        String authCode = this.buildAuthCode(phoneNumbers);
        CommonRequest request = buildCommonRequest(phoneNumbers, authCode);
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            return ErrorCode.NO_ERROR;
        } catch (ServerException e) {
            log.warn("【阿里云短信发送异常】");
            e.printStackTrace();
            return ErrorCode.SYSTEM_ERROR;
        } catch (ClientException e) {
            log.warn("【阿里云短信发送异常】");
            e.printStackTrace();
            return ErrorCode.SYSTEM_ERROR;
        }
    }

    private CommonRequest buildCommonRequest(String phoneNumbers, String code) {
        CommonRequest request = new CommonRequest();
        //request.setProtocol(ProtocolType.HTTPS);
        request.setMethod(MethodType.POST);
        request.setDomain(aliyunMsgConfig.getDomain());
        request.setVersion(aliyunMsgConfig.getVersion());
        request.setAction(aliyunMsgConfig.getAction());
        request.putQueryParameter("PhoneNumbers", phoneNumbers);
        request.putQueryParameter("TemplateCode", aliyunMsgConfig.getTemplateCode());
        request.putQueryParameter("SignName", aliyunMsgConfig.getSignName());
        request.putQueryParameter("TemplateParam", "{\"code\":" + code + "}");
        return request;
    }

    /**
     * 生成6位随机验证码
     *
     * @return
     */
    public String buildAuthCode(String phoneNumbers) {
        Integer max = aliyunMsgConfig.getMax();
        Integer min = aliyunMsgConfig.getMin();
        Integer authCode = (new Random().nextInt(max) % (max - min + 1) + min);
        myAuthCodeMap.put(phoneNumbers, authCode.toString());
        log.info("手机号 "+phoneNumbers+" 短信验证码:"+authCode);
        return authCode.toString();
    }

    /**
     * 获取当前的6位随机验证码
     *
     * @return
     */
    public static String getMyAuthCode(String phoneNumbers) {
        return myAuthCodeMap.get(phoneNumbers);
    }

    /**
     * 清除所有的临时随机验证码
     */
    public static void cleanMyAuthCodeMap() {
        myAuthCodeMap.clear();
    }

    /**
     * 清除指定号码的验证码
     * @param phoneNumber
     */
    public static void cleanMyAuthCodeMapByKey(String phoneNumber){
        myAuthCodeMap.remove(phoneNumber);
    }

    /**
     * 获取验证码集合,仅供开发测试!
     *
     * @return
     */
    public static Map<String, String> getMyAuthCodeMap() {
        return myAuthCodeMap;
    }
}

至此,一个短信发送验证码服务就完成了,使用者可以通过实例化CommonRpc类,调用sendMsg方法完成短信服务的发送,验证效果如下:

应用接入阿里云短信服务