安全专家马伟老师日前发布了《#博客大赛#不就是个短信验证嘛,有这么复杂吗?》一文,引起广泛关注。
文章以“新增手机号和短信验证码登录”简单的一句话需求最终演变为
故事卡-274 作为用户,我可以通过手机号和短信验证码登录,以便于我更方便的登录。 安全验收标准:
- 短信验证码有效期2分钟
- 验证码为6位纯数字
- 每个手机号60秒内只能发送一次短信验证码,且这一规则的校验必须在服务器端执行
- 同一个手机号在同一时间内可以有多个有效的短信验证码
- 保存于服务器端的验证码,至多可被使用3次(无论和请求中的验证码是否匹配),随后立即作废,以防止暴力攻击
- 短信验证码不可直接记录到日志文件
- (可选)发送短信验证码之前,先验证图形验证码是否正确
- (可选)集成第三方API做登录保护
实际上,根据我的经验,还可以再加一些验收条件
一个小小的需求可以衍生出如此之多的验收条件,而且其中不少是非功能性的(不容易识别的、不容易实现的),以至于有同学感叹:
厉害,短信验证这个事,如果有人做成整套解决方案直接调用就好了,就像keycloak一样
实际上,我深有同感,今年某客户项目上,想开发一个手机号码验证的功能,我原以为都8102年了,阿里云应该有全套解决方案了,结果只能提供短信发送服务(其实单就发送短信来说,开发体验已经很不错了),但验证的工作还是要自己开发的。我也咨询过一些“老法师”:
这个验证其实也很简单,可能就没有开发可复用解决方案的价值了吧?
反正我确实没找到整套解决方案。。。
在微服务的大潮下,如果想要复用短信验证的能力,最先想到的是开发一个短信验证服务,开放API给Consumer验证手机号码或是短信登录,名字我都想好了,叫sms-otp
(OPT为one time password缩写)。
sms-otp 服务
如果我是甲方IT部门,可能就这么做了,找到一个软件集成商实现sms-otp
就行了。
作为数字化厂商,ThoughtWorks可能需要为很多客户交付短信验证服务,并且出于专业要求,我们并不会把为A客户定制的代码复制到B家使用,这时候一个开箱即用的微服务是最佳选择吗?让我们一起先来看看有哪些问题需要解决
微服务对于1,2,3都算解决的不错,但如果还有其他的“通用”需求呢?例如支付宝支付、微信登录呢,微服务的数量就开始膨胀了。在一些项目中,部分客户的IT基础设施比较滞后,尤其是一些跨国公司的中国分公司,甚至没有中国区的软件系统,合作初期的项目规模也不大,这类项目未必适合以微服务启动。
那有没有更灵活的方案,既可以在单体应用中开箱即用,又可以按需扩展为独立服务呢?
提到开箱即用,近几年在Java业界最火的就是Spring Boot
了,Auto Configuration
大大提高了新应用搭建的速度,在需要定制时又不失灵活性。我觉得这是把好锤子,来敲两下看看是不是找对了钉子?
由外滩一号孵化中的Custom Spring Boot Starter,大名。ThoughtWorks“外滩一号”是一个内部自发建立的组织,通过扣命小程序(详见#博客大赛# 数字化敏捷交付的尝试——以“扣命小程序”为例 ),积累了一定的孵化方法。
为项目添加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应用使用私钥对应的公钥即可验证该手机号码实现业务目标(如登录或保存验证过的手机号码)。
什么样的需求适合用这个方案解决
如果我们使用领域驱动设计来分析,可以发现我们需要解决的问题分为
核心子域:它是一个唯一的、定义明确的领域模型,你要在这里进行战略投资,并在一个明确的限界上下文中投入大量资源去精心打磨通用语言。它是组织中最重要的项目,因为这将是你与其他竞争者的区别所在。正是因为你的组织无法在所有领域都出类拔萃,所以你必须把核心域打造成组织的核心竞争力。做出这样的决定需要对核心域进行深入地学习与理解,而这需要承诺、协作与试验。这是组织最需要在软件中倾斜其投资的方向。
支撑子域:这类建模方式提倡的是“定制开发”,因为找不到现成的解决方案。你对它的投入无论如何也达不到与核心域相同的程度。你也许会考虑使用外包的方式实现此类限界上下文,以避免因错误的认为其具有战略意义而进行巨额的投资。这类软件模型仍旧非常重要,核心域的成功离不开它。
通用子域:通用子域的解决方案可以采购现成的,也可以采用外包的方式,亦或是由内部团队实现,但我们不用为其分配与核心域同样优质的研发资源,甚至都不如支撑子域。请注意不要把通用子域误认为是核心域。你并不希望对其投资过甚。当讨论一个正在实施DDD的项目时,我们最有可能讨论的是核心域。
——《DDD精粹:运用子域进行战略设计》Vaughn Vernon
可以发现核心子域是没有必要尝试代码层面的“复用”,这里应该使用定制的方式来实现差异化。通用子域是比较合适的,或者说尝试复用的投资收益最大的那一部分,另外,复用有可能将一部分支撑子域简化为通用子域。例如短信登录没有现成的方案(支撑子域),但如果我们找到了复用的方法,其实现难度和所需要的研发资源就简化为了通用子域。
如果是Starter的话,如何灵活定制呢?
接上一问的答案,既然这些Starter都是解决支撑/通用子域的问题,那么其领域规则、业务流程是比较固定、不易变化的。需要灵活定制的部分其实是技术实现,使用端口和适配器架构(详见《#博客大赛#端口和适配器架构——DDD好帮手》)可以将这两部分隔离开,使用适配器扩展/变更技术解决方案。举个例子:
大名的端口和适配器架构
各个出口端口(右侧橙色板块的Port)作为扩展点,定制的Driven Adapter通过Spring注入。
为什么要绑定技术栈?非Java技术栈怎么办?
可以使用该Starter快速搭建一个微服务。。。
为什么要开源?
因为TWer都是对质量很挑剔的人,开源的方式最容易让大家接受
开源最容易从机制上鼓励质量提升、驱动代码的演化
竞争对手不管用不用我们的开源成果,其报价也比我们低
这玩意靠谱不?有人用了没
大名还处于早期阶段,欢迎捉虫和Pull Request。目前还没有人用,欢迎试用。
有没有前端的开箱即用方案 ?
还没有,我也不是前端专家,我猜测前端的开箱即用方案可以做成类似于Ant Design或Element UI但更专用的组件?
大名是一个尝试,并不是一个答案(至少还不是)。把这个问题转个各位TWer吧: