0%

0x00 前言

最近刷 leetcode,又回去回顾了很多数据结构的知识。刷题时经常会碰到使用 HashMap 来实现 O(1) 时间内的元素快速查找,以空间换时间。Hash 表的基本原理就是对 key 进行哈希运算得出 HasCode,然后通过对 HashCode 进行变换得到数组的 index。最后将 value 插入到该 index 位置上。但实际上,并不存在完美的散列算法能使得对于每个不同的输入值都能得到独一无二的 HashCode。这也就意味着对于不同的输入值可能会产生相同的 index,这也就是哈希碰撞。对于哈希碰撞大体上有两种解决方案:按照一定规则寻找数组中其他空余的位置,将 value 插入该位置;在 index 位置上使用链表来保存相同 HashCode 的 key 对应的 value。Java 选择了后者的实现方式。那么问题就来了,在对 HashMap 进行 get 操作的时候,势必要进行 key 值的比较。如果 Key 的长度很大的话,HashMapget 操作耗时应该会显著增加,那么是不是这样呢? 今天研究了一下这个问题。

0x01 Java 的 HashMap 是如何 get 到目标值的

如上面所说的,为了解决哈希碰撞问题,在对 HashMap 进行 put 操作的时候,有几率会在同一个 index 的位置上挂载多个 value,那么 get 操作想要获得正确的值就必须可以将查询时输入的 keyput 时保存在 HashMap 中的 key 值进行比较,相同时才返回 value,否则就继续向后遍历链表。流程如下面流程图所示:

get 的流程

0x02 耗时点分析

由上面的流程可以知道, get 操作涉及了一步 value.key.equals(key) 的比较操作,如果 keyString 类型的话,那我们看下 String 是如何实现 equals 方法的:

可以看到,在判断是否相同的时候会先比较字符串的 coder 是否相同,从coder方法中可以得知这个标志位是用来区分字符串是否是 Compact String 的,如果两个字符串的 coder 标志位不同也就说明两个字符串的类型有区别,那么就一定不同。如果相同,我们看到又继续执行了 StringLatin 或者 StringUTF16equals 方法。这两个方法的实现很类似,我们以 StringLatin.equals 的实现为例来看:

看到这里是使用了一个 for 循环来遍历两个字符串,逐字符比较两个字符串是否相同,那么这里的时间复杂度是 O(n)

0x03 验证

通过以上的分析可知,HashMap.get 的真正时间复杂度其实是和 key 的判等速度有关的,并不是严格意义上的 O(1)。可见,在字符串作为 Key 的例子下如果使用很长字符串作为 Key,那么 HashMap.get 会耗费大量的时间来进行字符串的判等。下面做一个实验来检验一下这个结论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Solotion {
// 生成 len 长度的字符串
String lsGen(int len) {
String s = "";
for (int i = 0; i < len; i++) {
char c = (char) (0x4e00 + (int) (Math.random() * (0x9fa5 - 0x4e00 + 1)));
s += c;
}
return s;
}

// 测试函数
void test (String s) {
Map<String, Integer> m = new HashMap<>();
for (int i = 0; i < 10000; i++) {
String str = s + i;
m.put(str, i);
}

Long bf = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
m.get(s + i);
}
Long af = System.currentTimeMillis();
System.out.println("Length of String: " + s.length() + " Time : " + (af - bf));
}

public static void main(String[] args) {
Solotion m = new Solotion();
String lstr = m.lsGen(100000); // 长字符串作为 key
String sstr = m.lsGen(100); // 短字符串作为 key
lstr.equals(sstr);
m.test(lstr);
m.test(sstr);
}
}

运行得到以下输出:

1
2
3
4
Connected to the target VM, address: '127.0.0.1:51865', transport: 'socket'
Length of String: 100000 Time : 1354
Length of String: 100 Time : 2
Disconnected from the target VM, address: '127.0.0.1:51865', transport: 'socket'

可以看到产生了三个数量级的差距,这个差距还会随着 key 增长和元素增多而拉大。

结论

Key 的长度会影响到 HashMap.get 的效率,过多判等比较耗时的 Key 会导致 HashMap.get 变得很慢,所以尽量使用判等效率高的 Object 作为 Key 以获得最大的效率。

![Alt text](./onePasswordTheory/2a547a9f-4e08-4b9c-8ed7-4af67968591f.png)

OnePassword 解决的问题

网站账户太多导致需要记录的账号和密码太多,一般人很难记住很多不同的密码。于是很多人会在不同的网站使用相同的账号密码,但是一旦其中一个网站被拖库,所有网站的密码就都会遭到泄露,这种做法很不安全。OnePassword 就针对这个情况为人们提供了一个安全的生成,存储和登录自动填充密码的解决方案。

那么,OnePassword 是如何解决这个问题的呢?

OnePassword 原理

主要功能

首先我们来看一下 OnePassword 的交互时序,为了方便起见,我画了一幅时序图放在下面

Alt text

可以看到它在注册和登录两个步骤都起到了关键作用,在注册的时候替用户生成了复杂密码,避免了用户自己思考密码的不便和不安全;在登录时又将之前保存的用户名和密码填充到目标网站或者 App 中,解决了用户记不住密码的问题。那么它又是如何保证安全性的呢?

密码安全性

Alt text

为了实现功能在多个终端都可以方便地使用,OnePassword 设计了一个密码仓库的同步机制,会将设备上的密码密文存储到远端的服务器或者网盘中。当新的设备登录时,密码库将被下载到新的设备中以此来实现随时随地在任意设备上均可使用其功能。

用户的密码被传到服务器中,会不会出现密码泄露的问题呢?答案是不会的。 OnePassword 在保存密码时会对密码进行多重的 AES256 加密,而密钥只保存在本地不会传输到远端,也就是说用于加解密的密钥只有你自己掌握,加密和解密的工作都只由本地客户端来完成。所以即使有人攻破了 OnePassword 的服务器取得了所有被加密的密码,在不知道密钥的情况下也无法在合理时间内将密码破解出来。

那么,这个用于登录 OnePassword 账号的密码又会不会被人截获呢?答案也是否定的。为了解释这个问题,首先我们先介绍两个概念,主密码(Master Password)和密钥(Secret Key)

  • 主密码(Master Password)
    用过 OnePassword 的人们都知道,在你注册 OnePassword 时,需要设置一个你自己记得住的密码,这个密码就是主密码。主密码十分重要,当你需要存取其他密码,或者登录其他设备的时候,OnePassword 都会要求你输入主密码(指纹验证其实也依赖了主密码的校验)。主密码只能依赖个人的记忆,不可找回,这意味着一旦你遗失了主密码,那么 OnePassword 中保存的所有密码信息也就随之化为乌有。

  • 密钥(Secret Key)
    在注册阶段用户输入主密码之后,OnePassword 还会生成一个密钥,这个密钥是一个 ‘A3’ 开头的字符串,长下面这个样子
    Alt text

这个密钥被存储于用户的客户端本地,基本无法被破解,这个密钥有时也被称为隐藏密钥,因为他不像是你的主密码,可以容易地被人记住。

上面的连词符号其实并不是个人私钥的一部分,只是为了增强可读性做分割使用的。版本号既不随机也不保密,用户 ID 倒是随机的,可是并不保密,而剩下的部分既是随机的也是保密的。

介绍了以上两个概念,现在回到我们的问题。OnePassword 之所以可以保证用于登录账户的密码不会泄露,是因为它甚至根本没有在服务器中以任何形式存储这个密码。这里 OnePassword 运用了非对称加密中的一项名为 迪菲-赫尔曼密钥交换的技术。具体地说,客户端根据主密码密钥以及其他一些东西(比如加入一些随机数),计算出一个数字,我们称为A,在注册阶段,客户端根据 A 计算出一个 B;然后把 B 发送给服务器。注意,B 类似于 hash(A),具体的方式,可能对 A 做一点其他运算再 hash;不管具体是哪种方案,窃听者都无法通过 B 计算出 A,而且他因为不知道 A,所以也无法做碰撞分析;甚至是,即便窃听者知道了主密码,只要他不知道密钥,他仍无法做碰撞分析。客户端任何时候,都可以根据主密码和密钥计算出 A;同时,server 那边有了一个通过 A 计算出的 B;因为 A 和 B 之间这种特殊的关系,那么此后在登陆阶段:

  1. 客户端可以向服务端证明它有一个和 B 对应的 A
  2. 服务端可以向客户端证明它有一个和 A 对应的 B
  3. 同时让客户端和服务端协定出一个 C 作为后续双方通信信息的加密密钥

这里引用维基百科中的一个一般性描述来说明这个算法
Alt text

具体的数学原理可以参见维基百科的说明,这里不再赘述。总之,通过这个密钥交换协议客户端和 OnePassword 服务端实现了在不传输密码本身的条件下进行通信,以此最大程度上保护密码的安全性。

更换客户端

由于主密码和密钥均由客户自行保管,而密码的加密解密完全依赖于主密码和密钥,所以在新的客户端上使用 OnePassword 的时候,需要用户将自己保存的主密码和密钥两者都输入到新设备的客户端中,这样客户端才能进行登录账户和对密码进行加解密操作。

总结

OnePassword 是一个安全性很高的密码管理工具,它对密码明文的加密和解密工作均在客户端本地完成,且依赖的密钥也都由客户保管,不会在 OnePassword 服务端传输的过程中产生泄露。由此看来,如果不在乎它每年 $279 HKD 的订阅价格,它的确是一个能帮你在摆脱密码焦虑的同时提升你密码安全性的绝佳工具。

参考资料

迪菲-赫尔曼密钥交换
假如你忘了 1Password 的主密码,到底该怎么办?

0x00 前言

iOS 签名机制比较复杂,涉及到一堆证书,Provisioning Profile,entitlements,CertificateSigningRequest,p12,AppID 等等概念繁多,也很容易出错,下面我们就来看下这些概念到底是什么?是怎么和 iOS App 的签名机制一起工作的?了解这些会有助于理解 iOS 这套复杂的签名机制。

0x01 签名机制的目的

首先要知道苹果为什么要退出这一套把人搞晕的签名机制。众所周知 iOS 是一个封闭的生态体系,苹果希望能取得对平台上的所有应用的绝对控制权。所以 iOS 不会像 PC 上那样,无需任何签名验证即可随意安装任何地方获取到的软件,而这种不受控的软件安装也是导致盗版盛行和生态混乱的元凶。那么苹果要怎样做才能保证每一个安装到 iOS 设备上的 App 都是经过苹果官方许可的呢?这就要靠签名和签名的验证机制来实现。

0x02 非对称加密

Asymmetric Key System

RSA

具体的原理可以参考这两篇文章:RSA 算法原理(一)(二)以及相关的数论知识:欧拉定理费马小定理欧拉函数同余

非对称加密算法是数字签名的基石。它通过使用两份密钥对数据进行加解密操作的。这两个密钥分别是公钥和私钥,用公钥加密的数据,要用私钥才能解密,用私钥加密的数据,要用公钥才能解密。

0x03 数字签名

现在知道了有非对称加密这东西,那什么是数字签名呢?

数字签名的作用是我对某一份数据作个标记,表示我认可了这份数据(签名),然后我再发送给其他人,其他人通过验证签名就能知道这个数据是不是被我认可的,数据有没有被篡改过。

为了保证签名在传输过程中的安全性需要对签名进行加密,基于非对称加密算法,就可以实现上述的方案:

Alt text

  1. 首先用一种算法,算出原始数据的摘要。需满足:
    a. 若原始数据有任何变化,计算出来的摘要值都会变化。
    b. 摘要要够短。
    这里最常用的算法是MD5。
  2. 生成一份非对称加密的公钥和私钥,私钥我自己拿着,公钥公布出去。
  3. 对一份数据,算出摘要后,用私钥加密这个摘要,得到一份加密后的数据,称为原始数据的签名。把它跟原始数据一起发送给用户。
  4. 用户收到数据和签名后,用公钥解密得到摘要。同时用户用同样的算法计算原始数据的摘要,对比这里计算出来的摘要和用公钥解密签名得到的摘要是否相等,若相等则表示这份数据中途没有被篡改过,因为如果篡改过,摘要会变化。

之所以要有第一步计算摘要,是因为非对称加密的原理限制可加密的内容不能太大(不能大于上述 n 的位数,也就是一般不能大于 1024 位 / 2048 位),于是若要对任意大的数据签名,就需要改成对它的特征值签名,效果是一样的。

有了以上的铺垫,现在来看看怎样通过这个数字签名的机制保证每一个安装到 iOS 上的 APP 都是经过苹果允许的。

0x04 最简单的签名方案

200%

我们先来讨论一种最直接的验证方式。那就是,苹果官方自己生成一对公私钥对。将公钥内置在 iOS 系统之中,私钥则由苹果的后台保存。开发者将 App 上传到苹果 AppStore 进行发布的时候,苹果用私钥将 App 的数据进行签名,iOS 系统下载这个 App 的时候,用系统中内置的公钥来验证这个 App 的签名,如果签名正确,就可以认为这个 App 的数据是没有被篡改过的,是经过苹果认证的版本。这样就满足了苹果的要求:保证 iOS 上安装的 App 都是经过苹果官方允许的。

如果 iOS 上的 App 安装渠道只有 AppStore 一个的话,那现在这个方案已经可以满足需求了。但实际上因为除了从 AppStore 下载,我们还可以有其他三种方式安装 App:

  1. 开发 App 时可以直接使用 Xcode 将应用直接编译安装进手机进行调试。
  2. In-House 企业开发者证书内部分发,iOS 设备可以直接下载安装企业 In-House 证书签名后的 App。
  3. Ad-Hoc 相当于开发者分发的限制版,限制安装设备数量,较少用。

所以苹果同样要对用这三种方式安装的 App 进行控制,那么在新的场景之下,方案就无法像上面这样简单了。

0x05 新场景

a. XCode 安装

我们先来看第一个,开发时安装APP,它有两个问题:

  1. 安装包不需要传到苹果服务器,可以直接安装到手机上。如果你编译一个 APP 到手机前要先传到苹果服务器签名,这显然是不能接受的。
  2. 苹果必须对这里的安装有控制权,包括
    a. 经过苹果允许才可以这样安装。
    b. 不能被滥用导致非开发app也能被安装。

为了解决这两个问题,iOS 签名的复杂度也就开始增加了。
苹果的方案是使用双层签名,比较繁琐,流程大概是这样的:

110%

① 首先在你的 Mac 开发机上生成一对公私钥,我们称为公钥L,私钥L(L: Local)。
② 苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A (A:Apple)。
把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书
③ 在开发时,App 编译通过之后,用本地的私钥 L 对这个 App 进行签名,同时把第三步得到的证书一起打包进 App 里,安装到手机上。所以 App 的 IPA 安装包中同时包含了 可执行文件,可执行文件的签名以及证书
④ 在安装 App 时,iOS 系统从 App 包中取得证书,通过系统内置的公钥 A,去验证证书的数字签名是否正确。
⑤ 验证证书后确保了公钥 L 是苹果认证过的,没有被篡改,再用公钥 L 去验证 App 的签名,这里就间接验证了这个 App 安装行为是否经过苹果官方允许。(这个过程只验证安装行为,不验证 App 是否被修改,因为开发阶段 APP 内容总是不断变化的,苹果不需要管。)

0x06 进一步研究

上面讲的这一堆流程只解决了上面第一个问题,也就是需要经过苹果允许才可以安装 App,还未解决第二个避免证书被滥用的问题。针对这一点,苹果又添加加了两个限制,一是限制在苹果后台注册过的设备(UDID)才可以安装,二是限制签名只能针对某一个具体的 App 起作用。

那么具体是怎么做的呢?在上面的 ③,苹果用私钥 A 签名我们本地公钥 L 时,实际上除了签名公钥 L,还可以添加他需要的任何数据,这些数据都可以保证 App 是经过苹果官方认证的,不会有被篡改的可能。

120%

可以想到把 允许安装的设备 ID 列表 和 App对应的 AppID 等数据,都在 ③ 这里跟公钥L一起组成证书,再用苹果私钥 A 对这个证书签名。在 ⑤ 的验证时就可以拿到设备 ID(UDID) 列表,判断当前设备是否符合要求。根据数字签名的原理,只要数字签名通过验证,第 5 步这里的UDID 列表 / AppID / 公钥 L 就都是经过苹果认证的,无法被修改,苹果就可以限制可安装的设备和 APP,避免滥用。

0x07 最终方案

现在这个证书已经变得很复杂了,有很多额外信息,实际上除了 UDID 列表 / AppID,还有其他信息也需要在这里用苹果签名,App 里 iCloud / push / App Group / 后台运行 等权限和能力苹果都想控制,苹果称这些权限开关为 Entitlements,它也需要通过签名去授权。

另外,一个“证书”本来就有规定的格式规范,上面我们把各种额外信息塞入证书里是不合适的,其实在真正的生产环境下,苹果也没有这么做,他们另外搞了个叫 Provisioning Profile 的东西,一个 Provisioning Profile 里就包含了证书以及上述提到的所有额外信息,以及所有信息的签名。

所以流程就变成了这样:

Alt text

现在我们来总结一下完整的最终流程

在你的 Mac 开发机器生成一对公私钥,这里称为公钥L,私钥L。(L: Local)
苹果自己有固定的一对公私钥,跟上面 AppStore 例子一样,私钥在苹果后台,公钥在每个 iOS 设备上。这里称为公钥A,私钥A。(A: Apple)

  1. 把公钥 L 传到苹果后台,用苹果后台里的私钥 A 去签名公钥 L。得到一份数据包含了公钥 L 以及其签名,把这份数据称为证书。
  2. 在苹果后台申请 AppID,配置好设备 ID 列表和 APP 可使用的权限,再加上第③步的证书,组成的数据用私钥 A 签名,把数据和签名一起组成一个 Provisioning Profile 文件,下载到本地 Mac 开发机。
  3. 在开发时,编译完一个 App 后,用本地的私钥 L 对这个 APP 进行签名,同时把第④步得到的 Provisioning Profile 文件打包进 APP 里,文件名为 embedded.mobileprovision,把 APP 安装到手机上。
  4. 在安装时,iOS 系统取得证书,通过系统内置的公钥 A,去验证 embedded.mobileprovision 的数字签名是否正确,里面的证书签名也会再验一遍。
  5. 确保了 embedded.mobileprovision 里的数据都是苹果授权以后,就可以取出里面的数据,做各种验证,包括用公钥 L 验证APP签名,验证设备 ID 是否在 ID 列表上,AppID 是否对应得上,权限开关是否跟 App 里的 Entitlements 对应等。

开发者证书从签名到认证最终苹果采用的流程大致是这样。

0x08 概念和操作

  1. 第 1 步对应的是 keychain 里的 “从证书颁发机构请求证书”,这里就本地生成了一对公私钥,保存的 CertificateSigningRequest 就是公钥,私钥保存在本地电脑里。
  1. 第 2 步苹果处理,不用管。
  1. 第 3 步对应把 CertificateSigningRequest 传到苹果后台生成证书,并下载到本地。这时本地有两个证书,一个是第 1 步生成的,一个是这里下载回来的,keychain 会把这两个证书关联起来,因为他们公私钥是对应的,在XCode选择下载回来的证书时,实际上会找到 keychain 里对应的私钥去签名。这里私钥只有生成它的这台 Mac 有,如果别的 Mac 也要编译签名这个 App 怎么办?答案是把私钥导出给其他 Mac 用,在 keychain 里导出私钥,就会存成 .p12 文件,其他 Mac 打开后就导入了这个私钥。
  1. 第 4 步都是在苹果网站上操作,配置 AppID / 权限 / 设备等,最后下载 Provisioning Profile 文件。
  1. 第 5 步 XCode 会通过第 3 步下载回来的证书(存着公钥),在本地找到对应的私钥(第一步生成的),用本地私钥去签名 App,并把 Provisioning Profile 文件命名为 embedded.mobileprovision 一起打包进去。这里对 App 的签名数据保存分两部分,Mach-O 可执行文件会把签名直接写入这个文件里,其他资源文件则会保存在 _CodeSignature 目录下。
  1. 第 6 – 7 步的打包和验证都是 Xcode 和 iOS 系统自动做的事。

然后,在总结下上面说到的概念:

概念小结

证书:内容是公钥或私钥,由其他机构对其签名组成的数据包。
Entitlements:包含了 App 权限开关列表。
CertificateSigningRequest:本地公钥。
p12:本地私钥,可以导入到其他电脑。
Provisioning Profile:包含了 证书 / Entitlements 等数据,并由苹果后台私钥签名的数据包。

0x09 其他发布方式

  1. In-House, Ad-Hoc

  2. AppStore

前面以开发包为例子说了签名和验证的流程,另外两种方式 In-House 企业签名和 AD-Hoc 流程也是差不多的,只是企业签名不限制安装的设备数,另外需要用户在 iOS 系统设置上手动点击信任这个企业才能通过验证。

而 AppStore 的签名验证方式有些不一样,前面我们说到最简单的签名方式,苹果在后台直接用私钥签名 App 就可以了,实际上苹果确实是这样做的,如果去下载一个 AppStore 的安装包,会发现它里面是没有 embedded.mobileprovision 文件的,也就是它安装和启动的流程是不依赖这个文件,验证流程也就跟上述几种类型不一样了。

据猜测,因为上传到 AppStore 的包苹果会重新对内容加密,原来的本地私钥签名就没有用了,需要重新签名,从 AppStore 下载的包苹果也并不打算控制它的有效期,不需要内置一个 embedded.mobileprovision 去做校验,直接在苹果用后台的私钥重新签名,iOS 安装时用本地公钥验证 App 签名就可以了。

那为什么发布 AppStore 的包还是要跟开发版一样搞各种证书和 Provisioning Profile?猜测因为苹果想做统一管理,Provisioning Profile 里包含一些权限控制,AppID 的检验等,苹果不想在上传 AppStore 包时重新用另一种协议做一遍这些验证,就不如统一把这部分放在 Provisioning Profile 里,上传 AppStore 时只要用同样的流程验证这个 Provisioning Profile 是否合法就可以了。

所以 App 上传到 AppStore 后,就跟你的 证书 / Provisioning Profile 都没有关系了,无论他们是否过期或被废除,都不会影响 AppStore 上的安装包。

上述内容就是 iOS App 签名机制的原理和主流程了,希望能够通过这样简单的梳理让看到这篇文章的 iOS 开发者能够对这个复杂的流程有所认识和了解。

按照我司的现实情况来讲,app 团队基本上维持着一个 10 人以上的移动研发团队,团队里面有项目经理、开发和测试同学,在研发过程中基本上都是多个项目同时进行。在项目的发展中,其中有些项目会参与到各种运营活动之中,需要和其它团队进行协作。所有的代码都耦合在一个客户端里面,每个月至少要发布 2 个版本,大家开发周各自开发暂时相安无事,一旦到了测试周要合代码到一起就出现各种诡异问题导致构建不成功、功能不可用、测试需要反复无数次验证依然惴惴不安。如何才能更好地协作开发,以较高的效率完成 App 开发工作呢?

迭代的典型流程

Alt text

上面是一个应用开发的典型流程图,下面来对流程进行详细的分析。

规划

  • 工程解耦
    对于 10 人以上的研发团队,非常需要提高研发协同效率,首先工程需要解耦,各自独立,减少相互依赖和影响,尽量做到每个模块可以独立构建独立发布。在开发中我们也采用了这样的方式,将客户端代码,基础库(网络库等),App 内 使用到的 Web 页面代码分别建立成独立的仓库,在未来应该也会将运营活动的页面和代码建立独立的工程来管理。

  • PMO(Project Management Office):定义项目规范和版本计划
    PMO 作为统筹整个项目的关键环节,先根据团队人员配比等制定适合当前团队的项目管理方案和项目规约,同时定义清楚客户端版本发布计划,例如每周五发布一个灰度版本,每个月底发布一个正式版本,计划清晰后同步到所有人员,方便大家安排工作。

  • 产品经理:设计需求
    产品经理通过分析用户反馈、分析市场诉求,从而抽象出业务需求,研发人员也会自行添加技术升级类需求,统一沉淀到平台做需求管理。当然,在实践中研发人员自行提出的技术需求需要和项目经理以及测试人员进行会商,评估影响面积,风险敞口,测试工作量和测试资源的安排。

  • 项目经理:排期分工
    根据需求和版本计划,PM 应将需求拆分成多个并行项目,如技术改造项目、迭代需求项目、长线需求项目,运营活动项目,项目经理或者资深研发会把这些需求细化拆分成任务,分派给不同的具体开发人员。

开发

  • 研发:代码变更
    研发实现需求后,在各自研发项目和代码分支中添加变更,测试自己的变更,当然最好是能对公共部分代码添加或者补充单元测试用例以保障项目的质量。

  • 研发:持续集成(CI)
    使用 CI 解决方案配合构建脚本在开发过程中持续进行构建集成和单元测试,及时发现问题,尽可能将能发现的问题在开发阶段解决。

  • 研发:版本集成
    研发在项目空间中完成代码变更和测试后,按照约定的发版计划,再约定好的时间自行提交版本集成生成最终提供测试冒烟的版本。

测试

  • 测试:功能、验收测试
    参加集成后,测试会对最终集成完成准备发布的包进行功能回归和验收测试,只有测试通过后的包,才能发布到用户手中。

发布和运维

  • 研发:版本发布
    版本发布过程,支持丰富的灰度策略,需要支持多批次缓慢放量,发布过程中实时监控 Crash、用户反馈,发生问题可及时止血。在我们的实践中,灰度策略基于 3 个维度,分别是具体客户、客户端版本、ReactNative 版本。客户维度基于客户号实现,客户端和 ReactNative 版本都基于客户端上报的请求头来实现。更复杂的维度甚至可以包含系统版本,手机型号等等。对于一些功能发布出现问题时可以及时将灰度关闭,停止功能的发布上线,将影响降低。
    在 ReactNative 版本发布的过程中,由于我们的发布窗口在周五,我们采用先对占用户比重较小的新近原生版本放量发布,如果在周末没有大面积的异常情况出现,则在周一对其余的存量旧版本进行发布升级。出现问题的话,可以及时进行版本的回滚,减少问题的影响面积。

  • 研发:运维监控
    线上监控运维,除了实时 Crash 分析之外,还应提供线上问题快速定位分析的解决方案,用户日志跟踪等服务,方便及时发现问题。成熟的解决方案有 MTA,Bugly 和友盟等等开发商提供的 SDK。

控制迭代的节奏

在我们的具体实践中,迭代的具体步骤大致如下图所示。

Alt text

开发周

  1. 周一对上一个迭代周期的发布进行全量发布配置。
  2. 开发人员对本迭代需求进行开发,测试人员进行本迭代测试用例的编写
  3. 周五进行本迭代发布需求的测试用例评审

测试发布周

  1. 周一上午开发将需在本迭代发布需求的代码分支进行合并,并通过构建。
  2. 周一下午测试人员将开始对开发成果进行冒烟测试。
  3. 冒烟测试通过后,开始为期四天的迭代测试。
  4. 在周四,PM召集产品经理和开发负责人讨论并定稿下一迭代需求。
  5. 周五中午锁定开发分支,后续任何提交均需要进行审批。(防止有无限多的修改导致版本质量不可控)
  6. 下午进行回归测试和最小检查点测试。
  7. 晚上进行新版本的发布并对发布代码版本打 tag,发布完成之后进行新近版本的升级配置。

最后的话

以上三篇,算是对这几年客户端工作的梳理,也是对经验的一个总结。真正的移动应用开发大潮始于 2007 年 Apple 发布 iPhone 到现在已经过去了 12 个年头。这 12 年里设备在不断创新进步,移动应用开发的技术也在一步步向着成本更低,更可维护的方向发展着。我们也看到前端技术和客户端技术的大融合催生的一系列创新的技术方案,这些技术创新也在一定程度上推动着业务和开发流程的演进和发展。希望通过这几篇文章的分享能够给予刚进入移动应用开发领域的参与者一些有建设性的观点,以更好的方式,开发出更好的应用。

HTTPS 原理

HTTPS 相比 HTTP 而言多了一个 s,也就是 SSL(Secure Sockets Layer),安全套接层。SSL 做的事情就是将用于通信的对称加密密钥进行非对称加密,以此来实现高效且安全的通信。整个 HTTPS 通信的时序图如下,包含了 HTTPS 四次握手。

![Alt text](./LetsHttps/1e7e149c-7ffd-421b-aa19-302cd053a5d1.png)
  1. 客户端发起一个https的请求,把自身支持的一系列Cipher Suite(密钥算法套件,简称Cipher)发送给服务端
  2. 服务端,接收到客户端所有的Cipher后与自身支持的对比,如果不支持则连接断开,反之则会从中选出一种加密算法和HASH算法
    以证书的形式返回给客户端 证书中还包含了 公钥 颁证机构 网址 失效日期等等。
  3. 客户端收到服务端响应后会做以下几件事
  • 验证证书的合法性
    颁发证书的机构是否合法与是否过期,证书中包含的网站地址是否与正在访问的地址一致等
    证书验证通过后,在浏览器的地址栏会加上一把小锁(因浏览器而异)
  • 生成随机密码
    如果证书验证通过,或者用户接受了不授信的证书,此时浏览器会生成一串随机数,然后用证书中的公钥加密。
  • HASH握手信息
    用最开始约定好的HASH方式,把握手消息取HASH值, 然后用 随机数加密 “握手消息+握手消息HASH值(签名)” 并一起发送给服务端
    在这里之所以要取握手消息的HASH值,主要是把握手消息做一个签名,用于验证握手消息在传输过程中没有被篡改过。
  1. 服务端拿到客户端传来的密文,用自己的私钥来解密握手消息取出随机数密码,再用随机数密码 解密 握手消息与HASH值,并与传过来的HASH值做对比确认是否一致。然后用随机密码加密一段握手消息(握手消息+握手消息的HASH值 )给客户端
  2. 客户端用随机数解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。因为这串密钥只有客户端和服务端知道,所以即使中间请求被拦截也是没法解密数据的,以此保证了通信的安全

在客户端与服务端相互验证的过程中用的是对称加密,客户端与服务端相互验证通过后,以随机数作为密钥,对称加密 hash 算法也同时确认握手消息没有被篡改。

SSL For Free & Let’s Encrypt

通过上面的描述,可以看到这里我们需要一个由认证机构(CA)颁发的 SSL 证书才能实现 HTTPS 通信,一般而言申请和更新 SSL 证书都是需要一定费用的,但是 SSL For Free 为我们提供了一个免费获得 SSL 证书的渠道
Alt text
在 SSL For Free 上可以获得由开放证书认证机构 Let’s Encrypt 颁发的 SSL 证书,该机构受到很多企业和机构的捐赠以保持免费,这个证书获得世界上绝大多数的浏览器认可,可以说是一个福利。

施工过程

施工的过程相对来说很简单,以我的 Nginx 服务器为例分为以下几个步骤:

  1. www.sslforfree.com 上输入你的域名,然后点击按钮创建 SSL 证书
    Alt text
  2. 然后转圈过后会进入下一个页面
    Alt text
    按照上图的描述,选择手动认证,如果你有部署 FTP 可以选用第一个方式。然后点击最下方的按钮。将页面向下滚动之后会出现以下的内容。
    Alt text
    点击下载认证文件
    Alt text
    然后将下载好的认证文件按照图片中的说明部署到网站的目录中。通过下方提供的绿色链接可以验证部署是否成功。完成之后点击下方的绿色按钮。
    Alt text
  3. 到下一个页面中下载包含证书和密钥的压缩文件到本地,解压,然后将文件上传到服务器的某个位置。
  4. 配置 Nginx
    /etc/nginx/sites-available 中的站点配置文件里加上如下内容
1
2
3
4
5
6
7
 server {
...
listen 443 ssl;
ssl_certificate {证书目录}/certificate.crt;
ssl_certificate_key {证书目录}/private.key;
...
}

然后重启或者 reload Nginx。
5. 如果 443 端口处于关闭状态,则还需要到 iptables 配置中将 443 端口开放。

至此整个施工结束。

强制 HTTPS 访问

在做如上配置之后你会发现网站既可以通过 http 也可以通过 https 访问,如何强制使用 https 访问呢?也很简单,只需要再给 Nginx 站点配置中添加一条新的规则。

1
2
3
4
5
server {
listen 80;
server_name {域名};
return 301 https://{域名}$request_uri;
}

通过 301 跳转的方式将非 https 的访问变更为 https 方式实现了强制 https 访问。

0x00 目标产品决定技术方案的选择

短生命周期和长生命周期产品

短生命周期的产品通常要求快速起步并在短时间内出品,目的性极强,技术门槛低、代码随便写、不用考虑任何最佳实践。当它的使命结束时,这些代码会被直接抛弃。比如 项目 Demo,临时活动专用 App。 所以,对于这类产品类似前苏联式的 “快糙猛” 的技术是较好的选择。当然能 “快精猛” 更佳,但在现实的短周期开发中实际上很难做到。

而长生命周期的产品则会对可维护性和可扩展性要求十分强烈,因为它们在相当长的时间内都是无法报废的。甚至对于一些关键的生命线产品,连项目重写都会要求在重写期间线上系统要万无一失地平稳度过,完全平滑地迁移到新技术。这种高水平的要求对团队的工程化能力是个极端的考验。如果工程以及项目管理能力有限,其代价不比用新技术重新写一个功能相同的系统更低。

Read more »

0x00 Before takeoff

在这个 App 泛滥的时代,从无到有开发一个 App 看似是一个很简单的事情,网上也有大把的教程来告诉你如何开发各种 App 和网站等等。但是在实际工作中,当我们提到“做一个 App”的时候,含义却十分深奥,那么这个坑到底有多深呢?我希望能够通过几篇文章的篇幅来做一个简要的叙述。

要说明的是,这个系列的文章并不会和网上大量的 App 开发指南类文章一样只是简单着眼于技术细节。而是通过对几年工作经验的总结来讲述当你在一个企业中从事客户端或者前端开发工作,面临一个全新的 App 开发任务时需要如何从头开始考虑整个任务以及流程。

只是一点微小的见解,希望能够给观众一点帮助。如有考虑不周,欢迎指教;如有失误,请多包涵(作揖)。

Read more »

经常受困于 GitHub clone 速度过慢的问题,之前有很多号称加速 github 的方法都不是很有效。现在发现一个亲测有效的方法,记录一下。

打开 hosts 文件

1
$ sudo vim /etc/hosts

在文件末尾添加以下三行:

1
2
3
4
5
192.30.253.112     github.com

151.101.72.133 assets-cdn.github.com

151.101.193.194 github.global.ssl.fastly.net

搞定,现在开始享受飞速的 github 体验。

原型链是 JavaScript 里面很基础的概念,面试中和工作中也经常遇到,但是有的时候用起来还是会犹豫一下。尤其是存在继承关系的时候,有时候搞不清楚对象的原型是谁,这里来结合实验详细的梳理一下。

假设我们有一个 Person 类(构造函数)

1
2
3
4
class Person {		
foo () {
}
}

我们用 new 操纵符构造一个新的对象 person

1
let person = new Person()

那么 person 对象和 Person 的原型链是什么样的呢

  1. person.__proto__ 指向 Person.prototype

  2. person.constructor 指向 Person 类,因为该对象的构造函数就是 Person

  3. person.constructor.prototype 也就是 Person.prototype

  4. Person.prototype.constructor 指向 Person 自己

  5. Person.constructor 指向 Function

  6. Person.__proto__ 指向 Function.prototype

    至此,Person 类和它的实例对象的原型链基本分析完毕了,接下来我们顺着这条链一直走到底来看一下。

  7. Person.prototype.__proto__ 指向 Object.prototype

  8. Object 实例对象的 constructor 指向 Object.prototype.constructor

  9. Object.prototype.__proto__null

下面上一张完整的图,看了这张图,对 js 的原型链就可以一目了然了

构造函数原型链

总结下就是:

  1. 实例对象的 __proto__ 指向类(构造函数)的 prototype
  2. 实例对象的 constructor 指向类(构造函数)本身
  3. 类(构造函数)的 __proto__ 指向父类或者 Functionprototype
  4. Function 的基类是 Object
  5. 特别的 Object.__proto__ 指向一个空函数
  6. 特别的 Object.property.__proto__null

0x00 盒模型

盒模型分为两种,标准模式和怪异模式,他们的定义如下:

标准模式: (box-sizing: content-box)
padding和border不被包含在定义的width和height之内。对象的实际宽度等于设置的width值和border、padding之和,即 ( Element width = width + border + padding )

怪异模式: (box-sizing: border-box)
padding和border被包含在定义的width和height之内。对象的实际宽度就等于设置的width值,即使定义有border和padding也不会改变对象的实际宽度,即 ( Element width = width )

怪异模式是 IE 5.x 6.x 中的标准盒模型,虽然叫做怪异模式,但是这种模式的布局模型更符合我们的直觉,所以一般都会将 box-sizing 属性设置为 border-box

Read more »