应用接入阿里云短信服务
背景
在日常开发中,我们可能会遇到短信验证之类的需求,这也是我们使用各类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方法完成短信服务的发送,验证效果如下: