前言
在移动端,我们除了使用账号密码、第三方社交平台账号(例如:微信、QQ、微博等)这几种登录方式以外,也会通过手机短信验证码的方式来做登录。
博主最近正在做移动端的手机短信验证登录。原本为了简单起见,选用的是某个不知名科技公司的短信服务,但是收费贵,服务也不太稳定等一系列问题的出现,导致博主开始另寻他路。
短信服务选择
博主挑选了几家大公司的短信服务,以下为各服务官网的产品价格:
在参考了各个服务的费用以及公司系统用户量等具体情况以后,博主最终选择了 阿里云短信服务 。
阿里云短信配置
在我们实现 阿里云短信发送API 之前,其实在阿里云平台上还有一系列的操作需要我们去申请、配置等(例如:开通短信服务、获取AccessKey、创建短信的签名和模板等等)。但是这些并不是本文的重点,所以博主这里不做详细的叙述。
大家可以自行参考阿里云提供的 短信服务文档使用指引,根据步骤操作。
API所需配置提取
假设上述配置操作大家已完成,博主这里用公司已注册好的模板举个例子。我们需要提取记录的配置有以下几种:
- AccessKeyID和AccessKeySecret: 关于这个内容大家自行参考官方文档 如何获取AccessKeyID和AccessKeySecret,步骤写的也比较详细,博主在这里就不做过多的叙述。
- 签名名称: 打开阿里短信服务管理控制台,按顺序点开:国内消息->签名管理,我这里选用阿里赠送的签名 阿里云短信测试专用 。
- 模板CODE和内容: 打开阿里短信服务管理控制台,按顺序点开:国内消息->模板管理->点击对应模板操作中的详情,我这里选用阿里赠送的 身份验证验证码 模板。
对以上的内容先做记录,因为后续在代码中我们相应会用到。
代码实现
基本的工作已经做完了,下面我们来看下具体代码的实现
1.添加依赖
<!-- 阿里云短信服务sdk -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
2.阿里云短信构造类
博主这里采用链式的写法,代码中的accessKeyId
和 accessKeySecret
需要替换成我们从云平台提取的对应值。
/**
* 阿里云短信构造类
* Created by Hilox on 2018/11/22 0022.
*/
public class AliSmsBuilder implements Serializable {
private static final long serialVersionUID = -8277819898310935813L;
/**
* 阿里云短信服务accessKeyId
*/
private String accessKeyId;
/**
* 阿里云短信服务accessKeySecret
*/
private String accessKeySecret;
/**
* 必填:待发送手机号
*/
private String phoneNum;
/**
* 必填:短信签名-可在短信控制台中找到
*/
private String signName;
/**
* 必填:短信模板-可在短信控制台中找到
*/
private String templateCode;
/**
* 可选:验证码
*/
private String verifyCode;
/**
* 可选:上行短信扩展码(无特殊需求用户请忽略此字段)
*/
private String smsUpExtendCode;
/**
* 可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
*/
private String outId;
private String defaultConnectTimeout;
private String defaultReadTimeout;
public AliSmsBuilder() {
// 默认初始化
accessKeyId = "***"; // 替换成自己的阿里云短信accessKeyId
accessKeySecret = "***"; // 替换成自己的阿里云短信accessKeySecret
this.defaultConnectTimeout = "10000";
this.defaultReadTimeout = "10000";
}
public AliSmsBuilder(String accessKeyId, String accessKeySecret) {
// 自定义初始化
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
}
public AliSmsBuilder setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
return this;
}
public String getAccessKeyId() {
return accessKeyId;
}
public AliSmsBuilder setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
return this;
}
public String getDefaultConnectTimeout() {
return defaultConnectTimeout;
}
public AliSmsBuilder setDefaultConnectTimeout(String defaultConnectTimeout) {
this.defaultConnectTimeout = defaultConnectTimeout;
return this;
}
public String getDefaultReadTimeout() {
return defaultReadTimeout;
}
public AliSmsBuilder setDefaultReadTimeout(String defaultReadTimeout) {
this.defaultReadTimeout = defaultReadTimeout;
return this;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public AliSmsBuilder setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
return this;
}
public String getPhoneNum() {
return phoneNum;
}
public AliSmsBuilder setSignName(String signName) {
this.signName = signName;
return this;
}
public String getSignName() {
return signName;
}
public AliSmsBuilder setTemplateCode(String templateCode) {
this.templateCode = templateCode;
return this;
}
public String getTemplateCode() {
return templateCode;
}
public AliSmsBuilder setVerifyCode(String verifyCode) {
this.verifyCode = verifyCode;
return this;
}
public String getVerifyCode() {
return verifyCode;
}
public AliSmsBuilder setSmsUpExtendCode(String smsUpExtendCode) {
this.smsUpExtendCode = smsUpExtendCode;
return this;
}
public String getSmsUpExtendCode() {
return smsUpExtendCode;
}
public AliSmsBuilder setOutId(String outId) {
this.outId = outId;
return this;
}
public String getOutId() {
return outId;
}
@Override
public String toString() {
return "AliSmsBuilder{" +
"accessKeyId='" + accessKeyId + '\'' +
", accessKeySecret='" + accessKeySecret + '\'' +
", phoneNum='" + phoneNum + '\'' +
", signName='" + signName + '\'' +
", templateCode='" + templateCode + '\'' +
", verifyCode='" + verifyCode + '\'' +
", smsUpExtendCode='" + smsUpExtendCode + '\'' +
", outId='" + outId + '\'' +
", defaultConnectTimeout='" + defaultConnectTimeout + '\'' +
", defaultReadTimeout='" + defaultReadTimeout + '\'' +
'}';
}
/**
* 发送短信消息
* @return
* @throws ClientException
*/
public String send() throws ClientException {
return AliSmsSDK.getInstance().sendSmsAli(this);
}
}
3.阿里云短信SDK
在这里有一点要强调下,我们从平台提取的短信模板内容为 验证码${code},您正在进行身份验证,打死不要告诉别人哦!
,所以在以下代码当中我们设置模板参数的JSON串的代码为 request.setTemplateParam("{\"code\":\"" + 变量替换值 + "\"}");
同理,如果你的模板内容是 亲爱的${name},您的验证码为${code}
那么在代码当中我们设置模板参数的JSON串的代码应该是 request.setTemplateParam("{\"name\":\"" + 变量替换值 + "\", \"code\":\"" + 变量替换值 + "\"}");
。阿里云对于变量替换值也是有要求的:变量替换值<=6位数字或字母
。
换句话说以上JSON串该如何写,主要还是看你的模板内容如何配置。当然 request.setTemplateParam("{\"code\":\"" + 变量替换值 + "\"}");
这段代码也可以忽略(只要对应的模板内容不配置占位符),因为这并不是必须的。
/**
* 阿里云短信SDK
* Created by Hilox on 2018/11/22 0022.
*/
@Slf4j
public class AliSmsSDK {
// 产品名称:云通信短信API产品,开发者无需替换
private static final String product = "Dysmsapi";
// 产品域名,开发者无需替换
private static final String domain = "dysmsapi.aliyuncs.com";
/**
* 私有化构造
*/
private AliSmsSDK() {}
/**
* 静态内部类
*/
private static class AliSmsSDKHolder {
private static final AliSmsSDK INSTANCE = new AliSmsSDK();
}
/**
* 选用静态内部类实现单例:
* 1.防止懒汉式单例多线程额外开销
* 2.防止饿汉式单例内存资源浪费
* @return
*/
public static final AliSmsSDK getInstance() {
return AliSmsSDKHolder.INSTANCE;
}
/**
* 发送短信验证码(阿里云短信平台)
* @param aliSmsBuilder
* @return
*/
public String sendSmsAli(AliSmsBuilder aliSmsBuilder) throws ClientException {
log.info("==== 开始发送短信验证码消息体: {} =====", aliSmsBuilder.toString());
// 设置超时时间-可自行调整
System.setProperty("sun.net.client.defaultConnectTimeout", aliSmsBuilder.getDefaultConnectTimeout());
System.setProperty("sun.net.client.defaultReadTimeout", aliSmsBuilder.getDefaultReadTimeout());
// 初始化acsClient, 暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", aliSmsBuilder.getAccessKeyId(), aliSmsBuilder.getAccessKeySecret());
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
// 组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
// 必填:待发送手机号
request.setPhoneNumbers(aliSmsBuilder.getPhoneNum());
// 必填:短信签名-可在短信控制台中找到
request.setSignName(aliSmsBuilder.getSignName());
// 必填:短信模板-可在短信控制台中找到
request.setTemplateCode(aliSmsBuilder.getTemplateCode());
String verifyCode = aliSmsBuilder.getVerifyCode();
// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为 request.setTemplateParam("{\"name\":\"Tom\", \"code\":\"123\"}");
if (!StringUtils.isEmpty(verifyCode)) {
request.setTemplateParam("{\"code\":\"" + verifyCode + "\"}");
}
String smsUpExtendCode = aliSmsBuilder.getSmsUpExtendCode();
// 选填-上行短信扩展码(无特殊需求用户请忽略此字段)
if (!StringUtils.isEmpty(smsUpExtendCode)) {
request.setSmsUpExtendCode(smsUpExtendCode);
}
String outId = aliSmsBuilder.getOutId();
// 可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
if (!StringUtils.isEmpty(outId)) {
request.setOutId(outId);
}
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
String code = sendSmsResponse.getCode();
log.info("==== 发送短信验证码成功, 返回code: {} =====", code);
return code;
}
}
4.阿里短信验证码返回code枚举
/**
* 阿里短信验证码返回code列表
* Created by Hilox on 2018/11/13 0013.
*/
public enum AliSmsCodeEnum {
OK("OK", "请求成功"),
RAM_PERMISSION_DENY("isp.RAM_PERMISSION_DENY", "RAM权限DENY"),
OUT_OF_SERVICE("isv.OUT_OF_SERVICE", "业务停机"),
PRODUCT_UN_SUBSCRIPT("isv.PRODUCT_UN_SUBSCRIPT", "未开通云通信产品的阿里云客户"),
PRODUCT_UNSUBSCRIBE("isv.PRODUCT_UNSUBSCRIBE", "产品未开通"),
ACCOUNT_NOT_EXISTS("isv.ACCOUNT_NOT_EXISTS", "账户不存在"),
ACCOUNT_ABNORMAL("isv.ACCOUNT_ABNORMAL", "账户异常"),
SMS_TEMPLATE_ILLEGAL("isv.SMS_TEMPLATE_ILLEGAL", "短信模板不合法"),
SMS_SIGNATURE_ILLEGAL("isv.SMS_SIGNATURE_ILLEGAL", "短信签名不合法"),
INVALID_PARAMETERS("isv.INVALID_PARAMETERS", "参数异常"),
SYSTEM_ERROR("isp.SYSTEM_ERROR", "系统错误"),
MOBILE_NUMBER_ILLEGAL("isv.MOBILE_NUMBER_ILLEGAL", "非法手机号"),
MOBILE_COUNT_OVER_LIMIT("isv.MOBILE_COUNT_OVER_LIMIT", "手机号码数量超过限制"),
TEMPLATE_MISSING_PARAMETERS("isv.TEMPLATE_MISSING_PARAMETERS", "模板缺少变量"),
BUSINESS_LIMIT_CONTROL("isv.BUSINESS_LIMIT_CONTROL", "业务限流"),
INVALID_JSON_PARAM("isv.INVALID_JSON_PARAM", "JSON参数不合法,只接受字符串值"),
BLACK_KEY_CONTROL_LIMIT("isv.BLACK_KEY_CONTROL_LIMIT", "黑名单管控"),
PARAM_LENGTH_LIMIT("isv.PARAM_LENGTH_LIMIT", "参数超出长度限制"),
PARAM_NOT_SUPPORT_URL("isv.PARAM_NOT_SUPPORT_URL", "不支持URL"),
AMOUNT_NOT_ENOUGH("isv.AMOUNT_NOT_ENOUGH", "账户余额不足"),
;
private String code;
private String msg;
AliSmsCodeEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
/**
* 根据code获取msg
* @param code
* @return
*/
public static String getMsgByCode(String code) {
if (StringUtils.isEmpty(code)) {
return null;
}
AliSmsCodeEnum[] values = AliSmsCodeEnum.values();
for (AliSmsCodeEnum aliSmsCodeEnum : values) {
if (aliSmsCodeEnum.getCode().equals(code)) {
return aliSmsCodeEnum.getMsg();
}
}
return null;
}
}
5.测试用例
博主在这里简单的写了个测试用例,后续调用方式大家可以从这里做参考。
对应的值需要替换成从云平台提取出来的相应值。
/**
* 测试用例模板
* Created by Hilox on 2018/11/22 0022.
*/
public class AliSmsSDKTest {
@Test
public void sendSmsAli() {
AliSmsBuilder aliSmsBuilder = new AliSmsBuilder();
String code;
try {
code = aliSmsBuilder.setPhoneNum("150****8879") // 替换成自己的手机号
.setSignName("阿里云短信测试专用") // 替换成自己的阿里云短信服务签名
.setTemplateCode("SMS_108565014") // 替换成自己的阿里云短信模板编号
.setVerifyCode("Hilox") // 替换成自己随机生成的验证码
.send();
} catch (ClientException e) {
e.printStackTrace();
// 短信发送异常提示
return;
}
// 短信异常码处理
if (code == null || !"OK".equals(code)) {
String errorMsg = AliSmsCodeEnum.getMsgByCode(code);
if (!StringUtils.isEmpty(errorMsg)) {
// 对应短信异常错误提示
return;
}
// 短信发送异常提示
}
}
}
源码传送门
【源码地址】:sms-ali