微服务中的短信服务如何设计?

本文将带领你深入地学习如何设计和实现一个通用的基础短信服务,将采用 Spring Boot 开发短信服务,最终会注册到 Spring Cloud 微服务体系中,方便其他服务使用。

学完本文后你将掌握使用 Spring Boot 设计并开发一个微服务体系下的短信基础服务。

一、短信服务的需求

短信发送只是一个功能点,我们这篇文章重点聊的是短信服务。服务是什么?为什么要设计单独的服务?本部分会为你解答。

1. 什么是服务

我认为的服务其实就是一些独立的功能集合,将大大小小 N 个功能集合在一起,服务于外部调用者。要业务需求一致,都是统一处理某一块业务。要能达到代码复用,提高开发效率的作用。

2. 为什么要设计单独的短信服务

在很久之前,小张在一家软件公司上班,某天接到了一个发送短信验证码的需求,于是小张快速地选择了短信服务商,然后用短信的 SDK 集成到了项目 A 中,飞快地完成了这个需求。

此时的代码是这样子的,如下:

SmsClient.send("182xxxxxxxx",“您的短信验证码是1111”);

过了一段时间,系统 B 也有这个需求了,同样地小张也是采用之前的方式,快速地实现了需求。依次类推,就这样慢慢地有 10 个系统都接入了发送短信的功能。有一天由于某些原因,短信的认证信息发生了变化,带来的问题就是每个系统都得去修改配置,当然如果你用了分布式配置中心的话也是可以解决这个问题的,暂且不在我们的考虑范围内。

经过这次事件后,小张决定对短信这个功能进行一次升级,采用消息队列的方式来处理短信发送。系统发送消息到消息队列,然后单独写一个消费者去消费队列中的消息来发送短信。

后面如果要改短信配置,只需要修改短信消费者一个地方即可。同时也带来了另一个问题,那就是每个系统还是需要去关注消息队列的存在,当消息队列的地址发生变化时,还是会发生之前的问题。

还有一个问题是消息队列发送的短信是异步的,我们为了防止短信被恶意发送,肯定会做一些限制。比如单个号码一天只能发多少次等等这样的限制。用消息队列就无法得到反馈结果,只能每个系统都去维护发送记录,然后去做这些限制。

二、短信服务的好处

通过设计短信服务可以解决上面我们描述的几个问题。短信服务的好处包括:

  • 统一的代码,配置或者调用方式发生变化只需要修改一次;
  • 通过 Http 接口调用,可以实时反馈一些限制信息,比如该号码发送频繁等;
  • 方便做一些发送量的统计报表;
  • 统一的安全防护,防止恶意发送。

本文的短信服务会结合 Spring Cloud 来讲解,也就意味着其他的系统可以通过服务发现的方式来调用短信接口,如果你的公司中没有上 Spring Cloud,我建议可以用域名的方式将短信服务部署起来,这样短信服务节点发生变化对客户端也是没影响的。

三、短信服务要有哪些功能

  • 发送验证码短信,独立接口;
  • 通用的短信发送接口,只需要传递模板编码和参数即可;
  • 短信支持测试环境,测试环境不真正发送短信,发送钉钉消息,节约费用;
  • 支持验证码检测功能,这样接入方就不用自己缓存验证码;
  • 所有短信发送都要有记录,方便排查问题和对账;
  • 支持对每天的发送量和单 IP、单号码发送量的限制(最好集成配置中心动态调整)。

上面列出的几点功能是我认为一个短信服务必须要具备的基础功能,当然还会有其它的功能,毕竟每个公司的业务场景等都不一样。其实最不一样的是对于短信发送量的要求,大公司可能每天发送的量都在几百万、上千万的量,而小型公司每天可能就是几千、几万的量。

不同的量级对服务的性能要求都是不一样的,本文的设计只是针对小型公司的一个方案,因为我自己也是在小公司,如果量真的非常大,在设计层面会有一些特殊的处理。如何应对高并发的请求,如何平稳的处理每条短信等。短信服务是无状态的,并且集成到了 Spring Cloud 中,意味着可以动态扩容。

四、阿里云短信接入

短信服务商有很多选择,本文中基于阿里云短信来进行讲解。

接入流程:

阿里云短信服务主页地址:https://www.aliyun.com/product/sms

短信使用流程见下图:

大致的流程我们可以对照上面的流程图即可。首先注册阿里云账号,然后开通短信服务。

短信服务开通后需要获取 accessKey 和 accessKeySecret。

接着就是要创建签名,签名是需要审核的,签名指的是短信前面显示的名称,比如“【XX 网】您的验证码是 1111”中括号里面的就是签名,也就是你们公司的名称或者产品名称,一般是产品名称。

最需要注意的是模板的配置,同样也需要审核。我以前有用过其他短信服务商,短信发送只需要把短信内容传过去就可以发送了,这是最简单的一种方式。阿里云的就比较麻烦,它需要你先创建模板,审核通过之后才可以发送短信。

模板就是你短信的内容,比如你要发送验证码短信,那么你的模板内容就是:

您的验证码 ${code},该验证码 5 分钟内有效,请勿泄漏于他人!

我可以看到在模板内容中有占位符,这个就是我们程序发送时需要传递给阿里云的,也就是这个验证码是动态的,短信内容是固定的。

麻烦点在于每次发送其他类型的短信,你都得事先去申请模板,然后才能用这个模板去发送短信,不像我前面讲的,有的可以直接支持短信内容的发送那么方便。

短信发送示例:

首先我们需要创建一个 Maven 工程,在 pom.xml 中添加短信 SDK 的依赖,如下:

code 对应的解释文档地址:

https://help.aliyun.com/knowledge_detail/57717.html

短信服务的设计:

本部分主要是对短信服务接口的设计进行讲解,就是我们的短信服务需要支持哪些功能需求,比如发送验证码,发送带链接的短信等。

验证码短信接口:

验证码短信基本上覆盖了公司中 90% 的需求,像我们的登录、注册、找回密码等功能都需要验证码短信。

第三部分中我们对阿里云短信发送规则已经了解,现在就需要根据阿里云的参数信息来设计我们自己的短信发送接口了。

先对接口的参数进行设计,看需要哪些参数,哪些是必填的,我这边贴出我觉得需要的一些参数信息,仅供参考:

@Data@ApiModel(value = "SendCodeSmsParam", description = "发送验证码短信参数")public class SendCodeSmsParam {    @ApiModelProperty(value="手机", required=true)    private String mobile;    @ApiModelProperty(value="验证码", required=true)    private String code;    @ApiModelProperty(value="IP", required=true)    private String ip;    @ApiModelProperty(value="发送来源(一般是系统名称)", required=true)    private String source;    @ApiModelProperty(value="是否需要同步发送到钉钉消息群,测试时用")    private boolean syncSendDingDing = false;    @ApiModelProperty(value="钉钉消息群Token,默认有一个群")    private String dingDingToken;    @ApiModelProperty(value="是否调试模式,调试模式不会真正发送短信,测试时用")    private boolean debug = false;    @ApiModelProperty(value="是否使用缓存中的验证码,在短信发送成功后有可能用户没收到,这时候重发,可以发送之前一样的,如果用户收到了第一次时的短信,就不会出现失效的情况")    private boolean useCacheCode = false;

}

短信验证码实现的话无非就是将验证码缓存起来,然后调用阿里云提供的短信发送方法。关于是否使用缓存中的 code 我这边再详细地描述下,这个在实际使用过程中是经常会出现的一个问题,我在上面的参数类中也进行了描述,可能不太详细。

如果加上使用缓存中的 code 这个逻辑,第二次发送我发的还是第一次的 code,这样的话用户输入了第一次的 code 也是能正确使用的,因为我 2 次发送的 code 是一样的。

短信签名和验证码模板可以配置项目中也可以集成到配置中心,对应的字段是 this.signNamethis.codeTemplateCode

接下来这点非常有用,之前在测试过程中,我们的短信内容都是通过 MQ 的后台管理页面去看消息内容,不是很方便,因为测试环境不想直接发送短信,这样可以节约成本。还有就是在某些场景下,测试会模拟一些不存在的手机进行测试,就算真的发短信,我们也是收不到的,这个时候就需要有一个调试的开关来解决这个问题。

五、检查验证码是否正确接口

在某些场景下,接口使用方可能没有用到缓存,这个时候为了缓存短信验证码就得加上缓存的框架配置,当然也可以用数据库存储,基本上都是缓存来存储验证码,效率高,支持自动失效。

为了解决上面描述的问题,我们的短信服务可以支持验证码的缓存,同时也支持验证码的认证。当调用发送验证码短信接口后,前端用户输入了验证码,提交到后台,这个时候就可以直接调用验证码检查的接口,判断用户输入的是否正确。

验证参数信息:

@Data@ApiModel(value = "CheckCodeParam", description = "检查验证码是否正确参数")public class CheckCodeParam {    @ApiModelProperty(value="手机", required=true)    private String mobile;    @ApiModelProperty(value="用户输入验证码", required=true)    private String code;    @ApiModelProperty(value="发送来源(一般是系统名称)", required=true)    private String source;

}

接口定义:

@ApiOperation(value = "检查用户输入的验证码是否正确")@PostMapping("/checkCode")public ResponseData<Boolean> checkCode(@RequestBody CheckCodeParam param) {    if (param == null) {        return Response.failByParams("参数不能为空");
    }    // ....参数验证
    return Response.ok(smsService.checkCode(param));
}

验证逻辑很简单,通过来源 + 手机号获取缓存中的验证码,然后跟用户输入的比较,得出结果。

清除已验证过的验证码接口

当验证完成后,需要清空之前缓存的验证码信息,这边提供了一个清除的接口。

清除参数信息:

@Data@ApiModel(value = "RemoveCacheCodeParam", description = "删除使用了的验证码缓存参数")public class RemoveCacheCodeParam {    @ApiModelProperty(value="手机", required=true)    private String mobile;    @ApiModelProperty(value="发送来源(一般是系统名称)", required=true)    private String source;

}

接口定义:

@ApiOperation(value = "删除已使用的验证码缓存信息")@PostMapping("/removeCacheCode")public ResponseData<Boolean> removeCacheCode(@RequestBody RemoveCacheCodeParam param) {    if (param == null) {        return Response.failByParams("参数不能为空");
    }    // ...参数验证  
    return Response.ok(smsService.removeCacheCode(param.getMobile(), param.getSource()));
}

业务逻辑:

@Overridepublic boolean removeCacheCode(String mobile, String source) {
    cacheService.deleteCache(source + mobile);    return true;
}

通用短信发送接口

前面我设计的都是验证码短信,除了验证码短信,还有一大部分短信是其他类型的,比如提醒、活动促销类的。

我们知道,阿里云是通过先定义短信模板,然后用参数传递的方式去动态构造内容,那么我们可以设计一个通用的接口,来满足其它类型短信的发送。

参数定义:

@Data@ApiModel(value = "SendSmsParam", description = "短信参数")public class SendSmsParam {    @ApiModelProperty(value="手机", required=true)    private String mobile;    @ApiModelProperty(value="参数")    private Map<String, Object> params;    @ApiModelProperty(value="IP", required=true)    private String ip;    @ApiModelProperty(value="发送来源(一般是系统名称)", required=true)    private String source;    @ApiModelProperty(value="短信签名")    private String signName = "xx网";    @ApiModelProperty(value="短信验证码模板", required=true)    private String templateCode;    @ApiModelProperty(value="是否需要同步发送到钉钉消息群,测试时用")    private boolean syncSendDingDing = false;    @ApiModelProperty(value="钉钉消息群Token,默认有一个群")    private String dingDingToken;    @ApiModelProperty(value="是否调试模式,调试模式不会真正发送短信,测试时用")    private boolean debug = false;

}

接口定义:

@ApiOperation(value = "发送短信")@PostMapping("/sendSms")public ResponseData<NoticeResultDto> sendSms(@RequestBody SendSmsParam param) {    if (param == null) {        return Response.failByParams("参数不能为空");
    }    // ...参数验证  
    return Response.ok(smsService.sendSms(param));
}

业务逻辑那就是把这些参数传给阿里云即可:

public NoticeResultDto sendSms(SendSmsParam param) {    return this.sendSms(param.getMobile(), param.getSignName(), param.getTemplateCode(), 
            param.getParams(), param.isDebug(), param.getSource(), param.isSyncSendDingDing(),
            param.getDingDingToken(), param.getIp());
}

六、短信 API 的管理

API 的管理其实是非常重要的,这涉及到以后的维护,接入方的使用等问题。

通过前面的代码大家可以看的出,我们的接口都使用了 Swagger 来进行管理。如果你的项目中没有使用 Swagger 的话我建议你用 Word 文档的方式将接口描述清楚,方便使用者进行接入。

除了要有详细的接口文档,代码中一定要有详细的注释,没有那就是在坑以后的程序员同学。

由于完整内容篇幅太长,我只摘录了这一部分,感兴趣的同学可以查看猿天地在 GitChat 发表的严选 Chat 《微服务中短信服务如何设计》——

原文发布于微信公众号 - GitChat精品课(CSDN_Tech)

原文发表时间:2018-12-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券