支撑子域是为了项目成功必须要处理的问题,但由于没有现成、成熟的解决方案,必须要定制,费时费力。
如果能恰当地识别支撑子域的边界,形成"可复用"的"解决方案",就可以将其从支撑子域简化为通用子域,降低成本和风险 。
不就是个短信验证嘛,有这么复杂吗?
前几天安全专家马伟发布了《不就是个短信登录API嘛,有这么复杂吗?》,文章从“新增手机号和短信验证码登录”简单的一句话需求最终演变为故事卡-274。
作为用户,我可以通过手机号和短信验证码登录,以便于我更方便的登录。
安全验收标准:
- 短信验证码有效期2分钟
- 验证码为6位纯数字
- 每个手机号60秒内只能发送一次短信验证码,且这一规则的校验必须在服务器端执行
- 同一个手机号在同一时间内可以有多个有效的短信验证码
- 保存于服务器端的验证码,至多可被使用3次(无论和请求中的验证码是否匹配),随后立即作废,以防止暴力攻击
- 短信验证码不可直接记录到日志文件
- 发送短信验证码之前,先验证图形验证码是否正确(可选)
- 集成第三方API做登录保护(可选)
实际上,根据我的经验,还可以再加一些验收条件
- 应该可以通过配置白名单的方式,只向特定手机号码发送验证码,以免在非生产环境测试时发生打扰真实用户的事故
- 应该可以通过配置By Pass的方式,在特定环境禁用短信验证码发送,并总是验证通过,以便在非生产环境节约短信配额
一个小小的需求可以衍生出如此之多的验收条件,而且其中不少是非功能性的(不容易识别的、不容易实现的),以至于有同学感叹:
厉害,短信验证这个事,如果有人做成整套解决方案直接调用就好了,就像keycloak一样。 |
运用子域进行战略设计
那么短信验证是否能成为"整套解决方案"呢,我们可以使用领域驱动设计中子域分类的框架来分析:
——《DDD精粹:运用子域进行战略设计》Vaughn Vernon |
可以发现:
- 核心子域是没有必要或者说不应该尝试开发“可复用”的"整套解决方案"的,因为"可复用“、”整套解决方案“意味着高度的标准化,也就意味着可以以较低成本复制,就不可能成为核心竞争力。
- 通用子域应该采用,而且往往也能找到“可复用”的"整套解决方案",以便降低成本和风险。
- 支撑子域则显得很"鸡肋",由于没有现成、成熟的解决方案,必须定制,但它又不是项目的核心价值。因此,如果能恰当地识别支撑子域的边界,形成"可复用"的"解决方案",就可以将其从支撑子域简化为通用子域,进一步降低成本和风险。
我认为短信验证就是一个好例子,短信验证自身没有独立的价值,但没有它,某些重要的功能会缺乏保护。但目前只能找到发送短信的SDK,而缺乏对于"发送-验证"这个相对标准化的问题域的支持。
解决方案的形态是什么样的
在微服务的大潮下,如果想要复用短信验证的能力,最先想到的是开发一个短信验证服务,开放API给Consumer验证手机号码或是短信登录,名字我都想好了,叫sms-otp(OPT为one time password缩写)。
(sms-otp 服务)
如果我是甲方IT部门,可能就这么做了,找到一个软件集成商实现sms-otp就行了。
作为数字化转型服务厂商,ThoughtWorks的想法会再进一步,是否还有更通用的方法?
ThoughtWorks可能需要为很多客户交付短信验证服务,并且出于专业要求,我们并不会把为A客户定制的代码复制到B家使用,这时候一个开箱即用的微服务是最佳选择吗?
如果还有其他的“通用”需求呢?例如支付宝支付、微信登录呢,微服务的数量就开始膨胀了。在一些项目中,部分客户的IT基础设施比较滞后,这类项目未必适合以微服务启动。那有没有更灵活的方案,既可以在单体应用中开箱即用,又可以按需扩展为独立服务呢?
如果存在不确定性,不妨做个MVP
提到开箱即用,近几年在Java业界最火的就是Spring Boot了,Auto Configuration大大提高了新应用搭建的速度,在需要定制时又不失灵活性。我觉得这是把好锤子,来敲两下看看是不是找对了钉子?
我们针对短信验证推出了自定义的 Spring Boot Starter,大名。
通过starter,既可以将解决方案"嵌入"单体应用,也可以快速启动新的微服务。
以下是一个简单的接入示例,为项目添加Starter:
- compile group: "com.github.hippoom:sms-verification-starter:${latestVersion}"
- # 如果需要使用开箱即用的Redis验证码存储
- compile "org.springframework.data:spring-data-redis:2.1.2.RELEASE"
- # 如果需要使用开箱即用的Aliyun短信服务
- compile("com.aliyun:aliyun-java-sdk-core:4.0.6")
- compile("com.aliyun:aliyun-java-sdk-dysmsapi:1.1.0")
为应用注入配置项:
- # application-{profile}.properties
- # 如果使用开箱即用的Aliyun短信服务
- daming.sms.provider=aliyun
- daming.aliyun.accessKeyId={your key id}
- daming.aliyun.accessKeySecret={your key secret}
- daming.aliyun.sms.signature={your text} # 阿里云短信服务的签名,可以在控制台找到,如是中文,请转为Unicode
- daming.aliyun.sms.templateCode={your code} #阿里云短信服务的模板Code,可以在控制台找到
- # 设置私钥地址,此私钥会用来签名被验证过的手机号码
- daming.jwt.privateKeyFileLocation=/home/your-app/sms-verification-private.der
启动应用,并请求验证码:
- >curl -H 'Content-Type:
- application/json'
- -XPOST ${host}:${port}/api/sms/verification/code
- -d '{"mobile": "${your mobile}"}'
在收到验证短信后,尝试验证:
- >curl -H 'Content-Type: application/json' -XDELETE ${host}:${port}/api/sms/verification/code -d '{"mobile": "${your mobile}","code":"${the code}"}'
- {"token":"{a very long string}"}%
在Response中可以得到一个JWT,前端应用将该JWT提交给Consumer应用,Consumer应用使用私钥对应的公钥即可验证该手机号码实现业务目标(如登录或保存验证过的手机号码)。
一些自问自答
如果是Starter的话,如何灵活定制呢?
既然这些Starter都是解决支撑/通用子域的问题,那么其领域规则、业务流程是比较固定、不易变化的。需要灵活定制的部分其实是技术实现,使用端口和适配器架构可以将这两部分隔离开,使用适配器扩展/变更技术解决方案。举个例子:
大名的端口和适配器架构
各个出口端口(右侧橙色板块的Port)作为扩展点,定制的Driven Adapter通过Spring注入。
为什么要绑定技术栈?非Java技术栈怎么办?
可以使用该Starter快速搭建一个微服务。。。
有没有前端的开箱即用方案 ?
还没有,我不是前端专家,但我猜测前端的开箱即用方案可以做成类似于 Ant Design 或 Element UI 但更专用的组件?
总结
- 支撑子域是为了项目成功必须要处理的问题,但由于没有现成、成熟的解决方案,它必须定制,费时费力
- 如果能恰当地识别支撑子域的边界,形成"可复用"的"解决方案",就可以将其从支撑子域简化为通用子域,降低成本和风险
- 短信验证是从支撑子域简化为通用子域的好例子,Project Daming(中文为大名),是我们推出的短信验证的解决方案,它的目标是将短信验证从支撑域简化为通用域,它以自定义的 spring boot starter出现,可以帮助团队在单体应用中快速嵌入短信验证功能,也可以快速启动一个短信验证的微服务