对读取短信验证码封装库的思考

在我的目前阶段,许多开源项目的产生都是直接来源于项目本身的需求,比如这次要讲的读取短信验证码的封装库。

项目目前挂在 github 上的公司组织下,地址为:https://github.com/parkingwang/sms-captcha。使用方法很简单,在界面创建时注册:

mCaptchaObserver = SmsCaptcha.with(this)
        .captchaLength(4)
        .addressLike("10657%")
        .fillTo(mSmsCode)
        .createAndRegister();

界面销毁时移除注册:

mCaptchaObserver.unregister();

业务的实现

这里读取短信验证码的实现说来也很简单,核心逻辑就是动态注册一个短信的内容观察者,然后继承 ContentObserver,在收到回调之后查询指定号码的未读短信,通过正则过滤出验证码出来。最核心的代码如下:

do {
    // 获取短信内容
    final String body = cursor.getString(cursor.getColumnIndex(BODY));
    // 使用正则表达式匹配出验证码
    final Matcher matcher = mCaptchaPattern.matcher(body);
    if (!matcher.find() || matcher.groupCount() != 1) {
        // 匹配不出来则找下一条
        continue;
    }
    String code = matcher.group(1);
    if (TextUtils.isDigitsOnly(code)) {
        if (mCaptchaListener != null) {
            // 找到后回调并退出
            mCaptchaListener.onCaptchaReceived(code);
        }
        break;
    }
} while (cursor.moveToNext());

完整的代码可参考项目:https://github.com/parkingwang/sms-captcha。全部代码也很简单,目前就两个类,加一起也就两百多行。

封装的思考

在做好了之后,我就想到如何把它造成一个通用的*,以便可以在其他项目中使用。

验证码的正则表达式

首先,从内容中匹配出验证码,最简单有效的就是使用正则表达式了,所以,接下来的思考就是如何设计拼接出尽可能通用的正则表达式。
应用的验证码的文本格式通常按顺序包含如下信息:
包含应用名称的信息,“验证码”这三个字,连续的多位数字,其他内容。
需要注意的是,在这个“其他内容”中,也可能会包含多位数字,比如像这样的:如有疑问,请拨打客服电话xxx-xxxxxxxx。像这样的信息,如果你的正则表达式是如下这样的:

.*应用名称.*验证码.*\d{4}.*

由于正则表达式默认是贪婪模式,那么它就会匹配到后面的电话号码中的数字。所以需要使验证码这三个字与数字之间的搜索为懒惰模式,即加上问号,改为如下:

.*应用名称.*验证码.*?\d{4}.*

这种情况是确实存在的。当你是为一个项目写代码时,可以不考虑。但是当你是想做一个 SDK 给多个项目使用时,却应当尽可能地考虑存在的情况。尽可能的考虑严谨、周全。

另外,如果验证码的位数不固定,可能是 4-6 位之间,则匹配则为 \d{4,6},则如果都是4位,也可以写为 \d{4,4}

短信的查询条件

注册了短信的内容观察者之后,会在短信内容会变化时回调,然后在收到回调之后,我们就需要按照条件去查询符合结果的短信,并检索出内容里包含的验证码。这里的查询条件也简单说一下。
首先,要查询的短信验证码必须是未读的,因为已读的短信可能是过去已经使用的,对我们没有任何意义。
其次,有些应用的验证验证码的发送号码可能不是固定的,比如更换了短信服务端,所以应允许发送号码的模糊查询,可以查询以什么开头的号码。所以查询条件应该可以指定为:

address=xxxxxx AND read=0

address LIKE %xxxxx AND read=0

这两种模式。

剩下的封装工作就都比较简单,就不足一谈了。

对产品的思考

当你把代码实现之后,跑到手上的华为测试机,可能会发现,什么效果都没有。是代码出问题了吗?
再细查,会发现查询短信的时候,都没有查询出手机接收到的那条验证码短信,然后再试图扩大查询范围,发现手机里凡是涉及到验证码的短信都查询不出来。
各种尝试无果之后,你点开了短信里的“设置”,发现有个“高级”,点进去之后看到了一个“验证码安全保护”的功能,底下有一行小字“禁止第三方应用读取和使用验证码信息”的描述,这个功能是打开的状态。你恍然大悟,把它给关掉,再一试,果然验证码出来了。

但是这个功能是手机厂商默认打开的,而功能这么隐蔽,用户一般是不会去关闭的,那么怎么解决这个问题呢?
获取短信验证码的方法必然存在多种。之前负责对应项目的前同事,在获取不到验证码之后并没有细究原因,而是转向了另一种获取验证码的方式——通过截取通知栏的内容,从中获取验证码。

然而这种方式,它首先需要让用户授权应用读取通知栏内容的权限。这是一个很隐蔽也很敏感的权限,一旦你同意了,就意味着这个应用可以读取到所有应用发送到通知栏上的内容。而且打开了这个权限之后,用户自己很难再找到这个授权的地方去关闭它,所以它会是一个引起一些敏感用户不适的一个坑。

是不是要采取这种方式,我们回归到读取验证码本身这条产品需求上。
关于程序员对产品的思考,在去年与一个厦门的朋友聊天时,很同意他的一些观点。程序员需要有产品思维,需要有自己对产品的思考,去思考它的出发点。没有一点产品思维的话,以后的提升空间是有限的。
回到原来的话题上。这条需求是为了自动填充验证码,减少用户的输入,从而提升用户体验。
那么,为了实现这一点,而去要求用户去打开一个可能有些用户觉得特别敏感的权限,并且事后用户自己很难去关闭它,是提升了用户体验还是存在着损害用户体验或引起敏感用户对应用安全的不适感的风险呢?要知道,即便是支付宝或微信,用户都会怀疑其是否窃听、偷调摄像头、分析聊天内容,更不用说我们这种目前用户级别很小的应用了。

另一方面,自动填充验证码,这其实是一个可选的需求。换句话说,即便不能读取到验证码帮用户输入,用户也不会因此而觉得应用存在问题,可能许多用户甚至都不会想到这个问题。所以应避免把它给做得太重,从而舍本逐末。

上一篇:洛谷 P2261 [CQOI2007]余数求和 解题报告


下一篇:一个动态权限库的设计