UI

UIView 和 CALayer的关系

  • UIView是CALayer代理,参与响应链,处理触摸等事件,
  • CALayer负责显示内容

事件传递

1
2
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool


事件响应

1
2
3
4
5
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
SubView -> SuperView -> ViewController -> Window -> AppDelegate

图像显示原理

CPU工作

  • Layout: UI布局, 文本计算
  • Display: 绘制(DrawRect)
  • Prepare: 图片编码
  • Commit: 提交位图

GPU工作

UI卡顿掉帧原因

  • 在下一个VSYNC信号到来之前(16.7ms内),CPU和CPU未能完成一帧画面的合成

UIView绘制原理


异步绘制原理

[layer.delegate displayLayer:]

  • 代理负责生成对应bitmap
  • 设置该bitmap为layer的contents

离屏渲染

  • On-Screen Rendering: GPU在当前用于显示的屏幕缓冲区中进行的渲染操作
  • Off-Screen Rendering: GPU在当前用于显示的屏幕缓冲区之外新开辟缓冲区进行渲染操作
    总结:离屏渲染开辟了新的缓冲区,涉及上下文切换,增加了GPU工作量

离屏渲染何时会触发

  • 圆角(同时设置maskToBounds)
  • 图层蒙版
  • 阴影
  • 光栅化

一些iOS常用命令

显示Mach-O文件中的entitlement

1
$ codesign -d --entitlement :- {mach-O path}

查看当前安装的证书

1
$ security find-identity -v -p codesigning

查看权限文件

1
$ security cms -D -i xxx.app/embedded.mobileprovision

制作plist文件

1
$ security cms -D -i embedded.mobileprovision > profile.plist

获取描述文件中的某一字段

1
$ /usr/libexec/PlistBuddy -x -c 'Print:Entitlements'  profile.plist > Entitlements.plist

强制替换签名

1
$ codesign -fs "iPhone Developer: DevNameXXX (XXXX)" --no-strict {xxx.app|Framework path}

使用Entitlements.plist对xxx.app签名

1
$ codesign -fs "iPhone Developer: DevNameXXX (XXXX)" --no-strict --entitlements=Entitlements.plist {xxx.app path}

检测xxx.app签名是否合法

1
$ codesign -v {xxx.app path}

将信息重新保存到xxx.txt

1
$ otool -l xxx.app/xxx > xxx.txt

检测xxx.app签名是否合法

1
$ codesign -v {xxx.app path}

筛选出可执行文件的加密情况

1
$ otool -l xxx.app/xxx | grep cry

175场周赛

5335. 参加考试的最大学生数

给你一个 m * n 的矩阵 seats 表示教室中的座位分布。如果座位是坏的(不可用),就用 ‘#’ 表示;否则,用 ‘.’ 表示。

学生可以看到左侧、右侧、左上、右上这四个方向上紧邻他的学生的答卷,但是看不到直接坐在他前面或者后面的学生的答卷。请你计算并返回该考场可以容纳的一起参加考试且无法作弊的最大学生人数。

学生必须坐在状况良好的座位上。

示例 1:

输入:seats =
[[“#”,”.”,”#”,”#”,”.”,”#”],
[“.”,”#”,”#”,”#”,”#”,”.”],
[“#”,”.”,”#”,”#”,”.”,”#”]]

输出:4
解释:教师可以让 4 个学生坐在可用的座位上,这样他们就无法在考试中作弊。

示例 2:

输入:seats =
[[“.”,”#”],
[“#”,”#”],
[“#”,”.”],
[“#”,”#”],
[“.”,”#”]]

输出:3
解释:让所有学生坐在可用的座位上。

示例 3:

输入:seats =
[[“#”,”.”,”.”,”.”,”#”],
[“.”,”#”,”.”,”#”,”.”],
[“.”,”.”,”#”,”.”,”.”],
[“.”,”#”,”.”,”#”,”.”],
[“#”,”.”,”.”,”.”,”#”]]

输出:10
解释:让学生坐在第 1、3 和 5 列的可用座位上。

提示

  • seats 只包含字符 ‘.’ 和’#’
  • m == seats.length
  • n == seats[i].length
  • 1 <= m <= 8
  • 1 <= n <= 8

Solution

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
func maxStudents(_ seats: [[Character]]) -> Int {
let R = seats.count, C = seats[0].count
var validity = [Int]()
for i in 0..<R {
var cur = 0
for j in 0..<C {
cur = 2*cur + (seats[i][j] == "." ? 1 : 0)
}
validity.append(cur)
}
func oneCount(_ n: Int) -> Int {
var n = n
var ct = 0
while n > 0 {
n = n & (n-1)
ct += 1
}
return ct
}
var dp = [[Int]](repeating: [Int](repeating: -1, count: 1<<C), count: R+1)
dp[0][0] = 0
for i in 1...R {
let valid = validity[i-1]
for j in 0..<(1<<C) {
if (j&valid == j) && (j&(j>>1) == 0) { // 确保mask为j时当前行的是合法的
for k in 0..<(1<<C) where dp[i-1][k] != -1 { //确保mask为k时上一行是合法的
if (j&(k>>1) == 0) && ((j>>1)&k == 0) { //确保当前行不与上一行冲突
dp[i][j] = max(dp[i][j], dp[i-1][k] + oneCount(j))
}
}
}
}
}
return dp[R].max()!
}

18场双周赛

1330. 翻转子数组得到最大的数组值

给你一个整数数组 nums 。「 数组值」定义为所有满足 0 <= i < nums.length-1 的 |nums[i]-nums[i+1]| 的和。

你可以选择给定数组的任意子数组,并将该子数组翻转。但你只能执行这个操作 一次 。

请你找到可行的最大 数组值 。

示例 1:

输入:nums = [2,3,1,5,4]
输出:10
解释
通过翻转子数组 [3,1,5] ,数组变成 [2,5,1,3,4] ,数组值为 10 。

示例 2:

输入:nums = [2,4,9,24,2,1,10]
输出:68

提示

  • 1 <= nums.length <= 3*10^4
  • -10^5 <= nums[i] <= 10^5

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func maxValueAfterReverse(_ nums: [Int]) -> Int {
var maxi = Int.min/100, mini = Int.max/100
let n = nums.count
var change = 0
for i in 0..<n-1 {
let a = nums[i], b = nums[i+1]
change = max(change, abs(nums[0] - b) - abs(a - b));
change = max(change, abs(nums[n - 1] - a) - abs(a - b));
mini = min(mini, max(a, b));
maxi = max(maxi, min(a, b));
}
var srcSum = 0
for i in 0..<n-1 {
srcSum += abs(nums[i] - nums[i+1])
}
return srcSum + max(change, (maxi - mini) * 2)
}

173场周赛

5321. 阈值距离内邻居最少的城市

有 n 个城市,按从 0 到 n-1 编号。给你一个边数组 edges,其中 edges[i] = [fromi, toi, weighti] 代表 fromi 和 toi 两个城市之间的双向加权边,距离阈值是一个整数 distanceThreshold。

返回能通过某些路径到达其他城市数目最少、且路径距离 最大 为 distanceThreshold 的城市。如果有多个这样的城市,则返回编号最大的城市。

注意,连接城市 i 和 j 的路径的距离等于沿该路径的所有边的权重之和。

示例 1:

输入:n = 4, edges = [[0,1,3],[1,2,1],[1,3,4],[2,3,1]], distanceThreshold = 4
输出:3
解释:城市分布图如上。
每个城市阈值距离 distanceThreshold = 4 内的邻居城市分别是:
城市 0 -> [城市 1, 城市 2]
城市 1 -> [城市 0, 城市 2, 城市 3]
城市 2 -> [城市 0, 城市 1, 城市 3]
城市 3 -> [城市 1, 城市 2]
城市 0 和 3 在阈值距离 4 以内都有 2 个邻居城市,但是我们必须返回城市 3,因为它的编号最大。

示例 2:

输入:n = 5, edges = [[0,1,2],[0,4,8],[1,2,3],[1,4,2],[2,3,1],[3,4,1]], distanceThreshold = 2
输出:0
解释:城市分布图如上。

每个城市阈值距离 distanceThreshold = 2 内的邻居城市分别是:
城市 0 -> [城市 1]
城市 1 -> [城市 0, 城市 4]
城市 2 -> [城市 3, 城市 4]
城市 3 -> [城市 2, 城市 4]
城市 4 -> [城市 1, 城市 2, 城市 3]
城市 0 在阈值距离 4 以内只有 1 个邻居城市。

提示

  • 2 <= n <= 100
  • 1 <= edges.length <= n * (n - 1) / 2
  • edges[i].length == 3
  • 0 <= fromi < toi < n
  • 1 <= weighti, distanceThreshold <= 10^4
  • 所有 (fromi, toi) 都是不同的。

Solution

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
func findTheCity(_ n: Int, _ edges: [[Int]], _ distanceThreshold: Int) -> Int {
var dis = [[Int]](repeating: [Int](repeating: Int.max>>2, count: n+1), count: n+1)
for e in edges {
dis[e[0]][e[1]] = e[2]
dis[e[1]][e[0]] = e[2]
}
for i in 0..<n {
dis[i][i] = 0
}
for k in 0..<n {
for i in 0..<n {
for j in 0..<n {
dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j])
}
}
}
var mv = n+1
var ans = -1

for i in stride(from: n-1, to: -1, by: -1) {
var ct = 0
for j in 0..<n where dis[i][j] <= distanceThreshold {
ct += 1
}
if ct < mv {
mv = ct
ans = i
}
}
return ans
}

5322. 工作计划的最低难度

你需要制定一份 d 天的工作计划表。工作之间存在依赖,要想执行第 i 项工作,你必须完成全部 j 项工作( 0 <= j < i)。

你每天 至少 需要完成一项任务。工作计划的总难度是这 d 天每一天的难度之和,而一天的工作难度是当天应该完成工作的最大难度。

给你一个整数数组 jobDifficulty 和一个整数 d,分别代表工作难度和需要计划的天数。第 i 项工作的难度是 jobDifficulty[i]。

返回整个工作计划的 最小难度 。如果无法制定工作计划,则返回 -1 。

示例 1:

输入:jobDifficulty = [6,5,4,3,2,1], d = 2
输出:7
解释:第一天,您可以完成前 5 项工作,总难度 = 6. 第二天,您可以完成最后一项工作,总难度 = 1. 计划表的难度 = 6 + 1 = 7

示例 2:

输入:jobDifficulty = [9,9,9], d = 4
输出:-1
解释:就算你每天完成一项工作,仍然有一天是空闲的,你无法制定一份能够满足既定工作时间的计划表。

示例 3:

输入:jobDifficulty = [7,1,7,1,7,1], d = 3
输出:15

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func minDifficulty(_ jobDifficulty: [Int], _ d: Int) -> Int {
let n = jobDifficulty.count
if d > n { return -1 }
// dp[i][j] = min diff for doing first i jobs in j days?
let INF = 1000_000_009
var dp = [[Int]](repeating: [Int](repeating: INF, count: d+1), count: n+1)
for i in 0..<n {
var diff = jobDifficulty[i]
for j in stride(from: i, to: -1, by: -1) {
diff = max(diff, jobDifficulty[j])
if j == 0 {
dp[i][1] = diff
} else {
for k in 2..<d+1 {
dp[i][k] = min(dp[i][k], diff+dp[j-1][k-1])
}
}
}
}
return dp[n - 1][d]
}

172场周赛

1326. 灌溉花园的最少水龙头数目

在 x 轴上有一个一维的花园。花园长度为 n,从点 0 开始,到点 n 结束。

花园里总共有 n + 1 个水龙头,分别位于 [0, 1, …, n] 。

给你一个整数 n 和一个长度为 n + 1 的整数数组 ranges ,其中 ranges[i] (下标从 0 开始)表示:如果打开点 i 处的水龙头,可以灌溉的区域为 [i - ranges[i], i + ranges[i]] 。

请你返回可以灌溉整个花园的 最少水龙头数目 。如果花园始终存在无法灌溉到的地方,请你返回 -1 。

示例 1:

输入:n = 5, ranges = [3,4,1,1,0,0]
输出:1
解释
点 0 处的水龙头可以灌溉区间 [-3,3]
点 1 处的水龙头可以灌溉区间 [-3,5]
点 2 处的水龙头可以灌溉区间 [1,3]
点 3 处的水龙头可以灌溉区间 [2,4]
点 4 处的水龙头可以灌溉区间 [4,4]
点 5 处的水龙头可以灌溉区间 [5,5]
只需要打开点 1 处的水龙头即可灌溉整个花园 [0,5]。

示例 2:

输入:n = 3, ranges = [0,0,0,0]
输出:-1
解释:即使打开所有水龙头,你也无法灌溉整个花园。

示例 3:

输入:n = 7, ranges = [1,2,1,0,2,1,0,1]
输出:3

示例 4:

输入:n = 8, ranges = [4,0,0,0,0,0,0,0,4]
输出:2

示例 5:

输入:n = 8, ranges = [4,0,0,0,4,0,0,0,4]
输出:1

提示

  • 1 <= n <= 10^4
  • ranges.length == n + 1
  • 0 <= ranges[i] <= 100

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func minTaps(_ n: Int, _ ranges: [Int]) -> Int {
let INF = 1000_000_009
var pairs = [(Int, Int)]()
for i in ranges.indices {
pairs.append((max(0, i-ranges[i]), min(n, i+ranges[i])))
}
pairs = pairs.sorted { $0.1 < $1.1 }
var dp = [Int](repeating: INF, count: n+1)
var best = INF
for i in stride(from: n, to: -1, by: -1) {
if pairs[i].1 >= n {
dp[i] = 1
}
for j in i+1..<n+1 {
if pairs[i].1 >= pairs[j].0 {
dp[i] = min(dp[i], dp[j]+1)
}
}
if pairs[i].0 <= 0 {
best = min(best, dp[i])
}
}
return best == INF ? -1 : best
}

171场周赛

5310. 二指输入的的最小距离

二指输入法定制键盘在 XY 平面上的布局如上图所示,其中每个大写英文字母都位于某个坐标处,例如字母 A 位于坐标 (0,0),字母 B 位于坐标 (0,1),字母 P 位于坐标 (2,3) 且字母 Z 位于坐标 (4,1)。

给你一个待输入字符串 word,请你计算并返回在仅使用两根手指的情况下,键入该字符串需要的最小移动总距离。坐标 (x1,y1) 和 (x2,y2) 之间的距离是 |x1 - x2| + |y1 - y2|。

注意,两根手指的起始位置是零代价的,不计入移动总距离。你的两根手指的起始位置也不必从首字母或者前两个字母开始。

示例 1:

输入:word = “CAKE”
输出:3
解释
使用两根手指输入 “CAKE” 的最佳方案之一是:
手指 1 在字母 ‘C’ 上 -> 移动距离 = 0
手指 1 在字母 ‘A’ 上 -> 移动距离 = 从字母 ‘C’ 到字母 ‘A’ 的距离 = 2
手指 2 在字母 ‘K’ 上 -> 移动距离 = 0
手指 2 在字母 ‘E’ 上 -> 移动距离 = 从字母 ‘K’ 到字母 ‘E’ 的距离 = 1
总距离 = 3

示例 2:

输入:word = “HAPPY”
输出:6
解释
使用两根手指输入 “HAPPY” 的最佳方案之一是:
手指 1 在字母 ‘H’ 上 -> 移动距离 = 0
手指 1 在字母 ‘A’ 上 -> 移动距离 = 从字母 ‘H’ 到字母 ‘A’ 的距离 = 2
手指 2 在字母 ‘P’ 上 -> 移动距离 = 0
手指 2 在字母 ‘P’ 上 -> 移动距离 = 从字母 ‘P’ 到字母 ‘P’ 的距离 = 0
手指 1 在字母 ‘Y’ 上 -> 移动距离 = 从字母 ‘A’ 到字母 ‘Y’ 的距离 = 4
总距离 = 6

示例 3:

输入:word = “HAPPY”
输出:7

Solution

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
func minimumDistance(_ word: String) -> Int {
let chars = Array(word)
let table = Array("abcdefghijklmnopqrstuvwxyz".uppercased())
var charDic = [Character: Int]()
for i in table.indices {
charDic[table[i]] = i
}

var dp = [[[Int]]](repeating: [[Int]](repeating: [Int](repeating: 0, count: 301), count: 27), count: 27)
func cost(_ from: Int, _ to: Int) -> Int {
if from == 26 { return 0 }
return abs(from/6 - to/6) + abs(from%6 - to%6)
}

func distance(_ pos: Int, _ left: Int, _ right: Int) -> Int {
if pos >= word.count { return 0 }
if dp[left][right][pos] == 0 {
let to = charDic[chars[pos]]!
dp[left][right][pos] = min(cost(left, to) + distance(pos + 1, to, right),
cost(right, to) + distance(pos + 1, left, to))
}
return dp[left][right][pos]

}
return distance(0, 26, 26)
}

170场周赛

5304. 子数组异或查询

有一个正整数数组 arr,现给你一个对应的查询数组 queries,其中 queries[i] = [Li, Ri]。

对于每个查询 i,请你计算从 Li 到 Ri 的 XOR 值(即 arr[Li] xor arr[Li+1] xor … xor arr[Ri])作为本次查询的结果。

并返回一个包含给定查询 queries 所有结果的数组。

示例 1:

输入:arr = [1,3,4,8], queries = [[0,1],[1,2],[0,3],[3,3]]
输出:[2,7,14,8]
解释:
数组中元素的二进制表示形式是:
1 = 0001
3 = 0011
4 = 0100
8 = 1000
查询的 XOR 值为:
[0,1] = 1 xor 3 = 2
[1,2] = 3 xor 4 = 7
[0,3] = 1 xor 3 xor 4 xor 8 = 14
[3,3] = 8

示例 2:

输入:arr = [4,8,2,10], queries = [[2,3],[1,3],[0,0],[0,3]]
输出:[8,0,4,4]

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
func xorQueries(_ arr: [Int], _ queries: [[Int]]) -> [Int] {
var preXor = [Int](repeating: 0, count: arr.count+1)
for i in 0..<arr.count {
preXor[i+1] = arr[i] ^ preXor[i]
}

var ans = [Int]()
for q in queries {
let l = q[0], r = q[1]+1
ans.append(preXor[l]^preXor[r])
}
return ans
}

5305. 获取你好友已观看的视频

有 n 个人,每个人都有一个 0 到 n-1 的唯一 id 。

给你数组 watchedVideos 和 friends ,其中 watchedVideos[i] 和 friends[i] 分别表示 id = i 的人观看过的视频列表和他的好友列表。

Level 1 的视频包含所有你好友观看过的视频,level 2 的视频包含所有你好友的好友观看过的视频,以此类推。一般的,Level 为 k 的视频包含所有从你出发,最短距离为 k 的好友观看过的视频。

给定你的 id 和一个 level 值,请你找出所有指定 level 的视频,并将它们按观看频率升序返回。如果有频率相同的视频,请将它们按名字字典序从小到大排列。

示例 1

输入:watchedVideos = [[“A”,”B”],[“C”],[“B”,”C”],[“D”]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 1
输出:[“B”,”C”]
解释:
你的 id 为 0 ,你的朋友包括:
id 为 1 -> watchedVideos = [“C”]
id 为 2 -> watchedVideos = [“B”,”C”]
你朋友观看过视频的频率为:
B -> 1
C -> 2

示例 2

输入:watchedVideos = [[“A”,”B”],[“C”],[“B”,”C”],[“D”]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 2
输出:[“D”]
解释:
你的 id 为 0 ,你朋友的朋友只有一个人,他的 id 为 3 。

  • 提示:
    • n == watchedVideos.length == friends.length
    • 2 <= n <= 100
    • 1 <= watchedVideos[i].length <= 100
    • 1 <= watchedVideos[i][j].length <= 8
    • 0 <= friends[i].length < n
    • 0 <= friends[i][j] < n
    • 0 <= id < n
    • 1 <= level < n
    • 如果 friends[i] 包含 j ,那么 friends[j] 包含 i

Solution

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
func watchedVideosByFriends(_ watchedVideos: [[String]], _ friends: [[Int]], _ id: Int, _ level: Int) -> [String] {
var seen = Set<Int>([id])
var queue = [(id, 0)]
var dstFriends = Set<Int>()
while !queue.isEmpty {
let (node, d) = queue.removeFirst()
if d > level { break }
if d == level {dstFriends.insert(node) }
for nei in friends[node] where !seen.contains(nei) {
seen.insert(nei)
queue.append((nei, d+1))
}
}

var dic = [String: Int]()
for node in dstFriends {
for video in watchedVideos[node] {
dic[video, default: 0] += 1
}
}
let videos = Array(dic.keys)
return videos.sorted {
if dic[$0]! == dic[$1]! {
return $0 < $1
}
return dic[$0]! < dic[$1]!
}
}

苹果双向签名原理

代码签名

  • 代码签名是对可执行文件或脚本进行数字签名,用来确认软件在签名后未被修改或损坏的措施。和数字签名原理一样,只不过签名的数据是代码而已。

双层代码签名

  • 在Mac系统中用非对称加密算法生成公钥M,私钥M
  • 苹果自己有固定的一对公私钥,私钥在苹果后台,公钥在没个iOS系统中,这里称为公钥A,私钥A
  • 把公钥M以及一些开发者信息传到苹果后台(这个就是CSR文件),用苹果后台的私钥A去签名公钥M,得到一份数据包含了公钥M以及其签名,把这份数据称为证书。

描述文件

  • 描述文件(Provisioning profile)一般包括三样东西:证书、App ID、设备。当我们真机运行或打包一个项目的时候,证书用来证明我们的程序的安全性和合法性

Https handshake

一、SSL协议的握手过程

开始加密通信之前,客户端和服务器首先必须建立连接和交换参数,这个过程叫做握手(handshake)

假定客户端叫做爱丽丝,服务器叫做鲍勃,整个握手阶段分成五步:

  • 第一步,爱丽丝给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
  • 第二步,鲍勃确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
  • 第三步,爱丽丝确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给鲍勃。
  • 第四步,鲍勃使用自己的私钥,获取爱丽丝发来的随机数(即Premaster secret)。
  • 第五步,爱丽丝和鲍勃根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。

上面的五步,画成一张图,就是下面这样。

二、私钥的作用

握手阶段有三点需要注意。

  • (1)生成对话密钥一共需要三个随机数。

  • (2)握手之后的对话使用”对话密钥”加密(对称加密),服务器的公钥和私钥只用于加密和解密”对话密钥”(非对称加密),无其他作用。

  • (3)服务器公钥放在服务器的数字证书之中。

    从上面第二点可知,整个对话过程中(握手阶段和其后的对话),服务器的公钥和私钥只需要用到一次。这就是CloudFlare能够提供Keyless服务的根本原因。
    某些客户(比如银行)想要使用外部CDN,加快自家网站的访问速度,但是出于安全考虑,不能把私钥交给CDN服务商。这时,完全可以把私钥留在自家服务器,只用来解密对话密钥,其他步骤都让CDN服务商去完成。

    上图中,银行的服务器只参与第四步,后面的对话都不再会用到私钥了。

三、DH算法的握手阶段

  • 整个握手阶段都不加密(也没法加密),都是明文的。因此,如果有人窃听通信,他可以知道双方选择的加密方法,以及三个随机数中的两个。整个通话的安全,只取决于第三个随机数(Premaster secret)能不能被破解。
  • 虽然理论上,只要服务器的公钥足够长(比如2048位),那么Premaster secret可以保证不被破解。但是为了足够安全,我们可以考虑把握手阶段的算法从默认的RSA算法,改为 Diffie-Hellman算法(简称DH算法)。
  • 采用DH算法后,Premaster secret不需要传递,双方只要交换各自的参数,就可以算出这个随机数。

  • 上图中,第三步和第四步由传递Premaster secret变成了传递DH算法所需的参数,然后双方各自算出Premaster secret。这样就提高了安全性。

四、session的恢复

  • 握手阶段用来建立SSL连接。如果出于某种原因,对话中断,就需要重新握手。

  • 这时有两种方法可以恢复原来的session:一种叫做session ID,另一种叫做session ticket。

  • session ID的思想很简单,就是每一次对话都有一个编号(session ID)。如果对话中断,下次重连的时候,只要客户端给出这个编号,且服务器有这个编号的记录,双方就可以重新使用已有的”对话密钥”,而不必重新生成一把。

  • 上图中,客户端给出session ID,服务器确认该编号存在,双方就不再进行握手阶段剩余的步骤,而直接用已有的对话密钥进行加密通信。

  • session ID是目前所有浏览器都支持的方法,但是它的缺点在于session ID往往只保留在一台服务器上。所以,如果客户端的请求发到另一台服务器,就无法恢复对话。session ticket就是为了解决这个问题而诞生的,目前只有Firefox和Chrome浏览器支持。

  • 上图中,客户端不再发送session ID,而是发送一个服务器在上一次对话中发送过来的session ticket。这个session ticket是加密的,只有服务器才能解密,其中包括本次对话的主要信息,比如对话密钥和加密方法。当服务器收到session ticket以后,解密后就不必重新生成对话密钥了。