iOS 11 短信过滤扩展简介

看 WWDC 2017 直播的时候,Keynote 上有一页讲 iOS 11 专门为中国用户新增了一些功能,那页只是提了一句很快就切走,但上面有一句让我眼前一亮:SMS fraud extension. 看起来是在 iOS 11 上开放短信过滤功能了?

坦率地讲,对我个人而言目前 iOS 的使用体验已经比较满意,但短信过滤的缺失是我最大的困扰,我经常被这几种短信烦到不行:

  • 淘宝、京东的店庆、618、双十一等各种营销短信。
  • 各家信用卡推荐你分期、预借现金、购买纪念品的短信。
  • 摩拜单车、ofo 喊你骑车的短信。

iOS 目前只能对特定号码开启「勿扰模式」,这样短信进来后就没有铃声和振动,只在短信图标上 +1 个未读。很显然这解决不了多大问题,一方面经常会有新的短信通道号码(以 106 开头的那些)过来,第一次肯定没法避免,另一方面像银行信用卡消费提醒、账单提醒这种有用的短信和营销短信多数是同一个号码发过来的,为了避免错过那些用得上的短信,你可能不会把此类号码设成勿扰。

这个问题在国内的安卓机上已经被解决得很好了,国内几个大厂的 ROM 都自带关键词过滤功能,甚至可以联网更新过滤规则。与此同时 iOS 开发者只能苦笑,Apple 从未提供过让第三方 App 读写短信的接口,这种接口涉及用户隐私也不可能直接提供,比如 iOS 10 新增的骚扰电话拦截,第三方 App 只能预先将需要标记或拦截的号码写入进去,由系统自己进行拦截,App 本身根本无法知道通话记录。此次 iOS 11 提供的短信过滤也是类似的思路,系统将收到的短信发送给过滤扩展,扩展根据内置的规则判断要不要过滤掉再回复给系统,扩展可以知道短信发送方的号码和短信的内容,但扩展运行在独立的沙盒中,无法向它的容器 App (containing app) 回写数据,这样的流程设计和权限控制就使得拦截短信和保护隐私两者兼得。

核心思路

关于这个新增的短信过滤扩展,有以下几个关键点:

  • 这个功能在新增的 IdentityLookup Framework 里。
  • 通讯录里的联系人发来的短信不会被拦截;回复会话达到 3 次也不会被拦截。
  • App 通过构建 Message Filter Extension 的方式进行过滤,系统会把短信发送方和短信内容发给 extension 询问结果,extension 给出三种可能的回复:允许、过滤、不处理(信息不足不知道怎么处理)。这一步的处理逻辑都是预先在本地 extension 里写好的。
  • 如果过滤结果是不处理,那么系统会将相关信息发送到你 App 相关联的服务器进行查询,并将服务端的结果再返给 extension 进行决策。
  • 出于隐私考虑,这个 extension 不能写回数据到它的容器 App,就是说你的短信内容 app 里面是肯定拿不到的,同时 extension 不能直接连接网络,只能通过系统帮你发送请求到服务端。
  • Extension 无法访问系统全局的剪切板,所以目前没有办法做到自动提取验证码。

理解了这几个关键点,那么开发和使用起来就很简单了,逻辑实在太清晰,都没什么可以讲的:容器 App 里编辑、保存一个过滤规则,extension 读取这个规则给出判断结果。

这个地方要注意的是,extension 虽然没有往外回写数据的能力,但是和容器 App 肯定是有办法读取同一份数据的,方式有多种,我自己写的代码里使用的是 App Group 的 NSUserDefaults。App 里编辑好规则,把规则对象序列化成一个字符串存到 NSUserDefaults 里,然后 extension 去读取字符串再反序列化出来。

过滤规则的设计,最常见的就是包含关键词了,这个足以覆盖多数场景。当然了,一个 App 里就只有添加关键词的功能多无聊,于是我强行捣鼓了一份稍微复杂点的规则:

  • 过滤规则分为白名单和黑名单两个部分,每个具体的名单由若干条件组构成,条件组是若干个单条判断逻辑的组合。
  • 规则的条件组之间为“或者”的逻辑关系,条件组的条件之间为“并且”的逻辑关系。
  • 白名单比黑名单有更高的优先级。
  • 每个条件判断逻辑都是 发送者/短信内容 与关键词的匹配。

对于关键词的匹配,也提供了”含有前缀”、”含有后缀”、”包含”、”不包含”、”匹配正则”五种匹配模式,我知道有些人不让他写个正则表达式他就不爽。

界面的设计没什么好说的,简单的工具类应用不需要花里胡哨,更何况这是一个 Demo 级别的 App,我直接用系统默认的效果,放了几个 icon 上去,最终长这样:

Interface

具体的代码实现以及安装方式,参阅我这个 GitHub repo:https://github.com/Bynil/MessageJudge

服务端

还有一个值得聊的点是,extension 可以把短信发送到服务端进行判断。我在文档中看到这一项时是有点惊讶的,因为允许把短信内容发到开发者的服务端就意味着极大的隐私泄露风险,iOS 系统处心积虑设计了一套又一套的规则就是不希望第三方 App 拿到敏感信息,为什么这里突然允许把短信内容往外发?我甚至以为我是不是理解错文档的意思了,看了好几遍才肯定的确就是这么设计的:当短信过滤扩展自己无法决定是否可以标记一条信息时,它还可以向开发者的服务器请求查询。

我个人认为这是一个比较鸡肋的功能,对短信的过滤本地做规则完全足够了,最多把规则做成可在线更新的,但是你说让系统把我的短信详情发给其他人的服务端,我完全不能接受。当然这个 feature 也不是肯定不安全:API 地址是写死在 plist 里的,请求由系统发起,extension 无法控制请求的来回,那么服务端只能知道短信发送者和内容,却无法知道这条短信是发给哪个用户的,这样的流程可以说已经把风险降到了最低。我写的 Demo 在一开始就没有任何涉及服务端的功能,只有本地的策略。当然,为了尽可能把 Apple 提供的功能摸透,肯定还是要试一试的。

前面说过,这个 extension 是无法直接发起网络请求的,必须通过系统来转发请求,因为系统层面有一套严格的机制来确保这个请求发出去是安全、可信的:

  • App 里必须加上 Associated Domains.
  • 服务端必须配置相应的 Shared Web Credentials.
  • Extension 的 plist 里提前写好 ILMessageFilterExtensionNetworkURL 来指定接口地址。

这一套东西和 iOS 9 的 Universal Link 是类似的,容易踩到坑,验证不通过但又找不到问题在哪的话可以参考我之前写的iOS 9 Universal Links 容易踩的坑

我的 apple-app-site-association 文件内容:

1
2
3
4
5
6
{
"messagefilter": {
"apps": ["49XXXX2T.me.gexiao.MessageJudge.MessageJudgeExt",
"49XXXX2T.me.gexiao.MessageJudge"]
}
}

我并没有专门去写服务端 API,只弄了一个 dummy server 来测试看看系统往服务端发送的到底是些什么玩意,dummy server 有现成的:https://gist.github.com/bradmontgomery/2219997 稍微改改把 POST 的 body print 出来就行了:

dummy-server

POST 过去是一串 JSON,里面有短信的发送方和内容:

1
2
3
4
5
6
7
8
9
10
11
12
{
"_version": 1,
"app": {
"version": "1"
},
"query": {
"sender": "+8615312345678",
"message": {
"text": "测试服务端数据"
}
}
}

这个请求返回的内容 extension 里都可以拿到。

你可以把项目切换到 server 分支查看配置了 associated server 的 App.

尴尬的问题

一个发送方的一条信息被标记为骚扰后,会导致这个发送方后续的消息永远被系统直接判定为骚扰。我一度以为这是 Beta 版的 bug,因为我的预设场景就是对每一条短信都进行过滤,不需要管发送方之前有什么行为。然而 Apple 回复我这是 feature,屏蔽对象是整个的 conversation 而不是单条信息。这就很尴尬了,部分场景比如银行发来的营销短信仍然无法处理。而且被屏蔽掉的短信和勿扰模式一样,虽然没有声音、振动了,但未读数仍会 +1。Apple 在设计这个功能时好像并没有了解清楚我国国情,实用性会打很大折扣。难道这个东西还能有其他的玩法?那就等 iOS 11 正式发布后看看腾讯和 360 是怎么做的好了。

另一件小问题是,Beta 版时 extension 可以回写数据给容器 App,这个是 bug 无疑。刚刚发布的 Beta 2 已经解决,就不再多说了。