-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
231 lines (114 loc) · 157 KB
/
atom.xml
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Water&Melon's Blog</title>
<link href="https://kylinlingh.github.io/atom.xml" rel="self"/>
<link href="https://kylinlingh.github.io/"/>
<updated>2023-09-11T08:27:40.751Z</updated>
<id>https://kylinlingh.github.io/</id>
<author>
<name>kylinlin</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>[翻译]Twirp: a sweet new RPC framework for Go</title>
<link href="https://kylinlingh.github.io/2023/09/08/%E7%BF%BB%E8%AF%91-Twirp-a-sweet-new-RPC-framework-for-Go/"/>
<id>https://kylinlingh.github.io/2023/09/08/%E7%BF%BB%E8%AF%91-Twirp-a-sweet-new-RPC-framework-for-Go/</id>
<published>2023-09-08T09:56:04.000Z</published>
<updated>2023-09-11T08:27:40.751Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Twirp 是由 Twitch 公司开源的一个 RPC 框架,从 2018 年开源至今一直在国内默默无闻,我认为一个很重要的原因是国内的后端开发仍然停留在 RESTful API 的开发思维里,并没有多少开发者真正转型到 RPC 的开发生态里。并且因为鼎鼎大名的 Google 开源了 gRPC,进一步导致其他 RPC 框架无人问津。本文致力于翻译并精简 twich 公司的一篇<a href="https://blog.twitch.tv/en/2018/01/16/Twirp-a-sweet-new-rpc-framework-for-go-5f2febbf35f/">博文</a>来介绍这个好用且简单的 RPC 框架。</p><h1 id="为什么要使用-RPC"><a href="#为什么要使用-RPC" class="headerlink" title="为什么要使用 RPC"></a>为什么要使用 RPC</h1><p>在 2015 年,Twitch 公司做出了巨大的努力去将一个巨无霸 app 拆分成多个独立的服务,在进行微服务拆分的时候,需要考虑很多 API 设计的问题,包括:</p><ul><li><p>如何接收请求,譬如现在要开发一个更新用户 email 地址的接口,那么以下的 RESTful 格式 url 看起来都很合理:</p><ul><li>POST /users/:id/email</li><li>PUT /users/:id/email</li><li>POST(or PUT) /users/:username</li><li>PATCH /users/:username/info</li></ul></li><li><p>是否要设置一些特殊的 headers</p></li><li><p>url 的版本号如何体现</p></li></ul><p>上面还只是在创建 API 的阶段需要解决问题,在长期运营 API 的时候还需要处理如下问题:</p><ul><li>不同开发人员 / 不同开发阶段使用的 url 格式不一致,这样会导致指标采集很难统一处理请求日志,并且很难维护 API 的接口文档</li><li>服务端代码在版本迭代之后,你需要手写去更新客户端的代码</li></ul><p>Twirp 通过代码自动生成的方法来解决上述的问题,用户只需要编写一个简洁的 protobuf 文件来描述 API,Twirp 会生成一个go 文件,主要包含了 HTTP 的处理代码(包括路由,序列化等),举例如下:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">syntax = “proto3”;</span><br><span class="line">package Twitch.users.email;</span><br><span class="line"></span><br><span class="line">service EmailBoss {</span><br><span class="line"> rpc UpdateEmail(UpdateEmailRequest) returns (UpdateEmailResponse);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">message UpdateEmailRequest {</span><br><span class="line"> int64 user_id = 1;</span><br><span class="line"> string new_email = 2;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">message UpdateEmailResponse {</span><br><span class="line"> // EmailBoss may clean up the email it receives to trim whitespace</span><br><span class="line"> // or remove superfluous characters. The cleaned_email will be the</span><br><span class="line"> // email actually stored after this cleaning.</span><br><span class="line"> string cleaned_email = 1;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Twirp 生成的 go 文件主要包括以下内容</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> EmailBoss <span class="keyword">interface</span> {</span><br><span class="line"> UpdateEmail(context.Context, *UpdateEmailRequest) (*UpdateEmailResponse, <span class="type">error</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 将实现了 EmailBoss 接口的类都作为 http.Handler 暴露出去</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewEmailBossServer</span><span class="params">(svc EmailBoss)</span></span> http.Handler</span><br><span class="line"></span><br><span class="line"><span class="comment">// 同样提供了一个 client 的构造器来和 server 通信</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewEmailBossProtobufClient</span><span class="params">(hostport <span class="type">string</span>, client *http.Client)</span></span> EmailBoss</span><br></pre></td></tr></table></figure><p>有了这个自动生成的 go 文件,后端开发人员就不需要再考虑 url schema 等问题,只需要专注于实现接口的处理逻辑。而 client 也只需要指定 server 的 ip 和端口,就可以直接使用生成的实例来调用 server 的服务。</p><p>Twirp 生成的所有请求都是基于 Http1.1,并且只能使用 POST 请求提交参数,payload 既可以是 JSON,也可以是二进制编译的 protobuf。因此,对于上面的例子来说:</p><ul><li>url 为:/Twirp/Twitch.users.email.EmailBoss/UpdateEmail</li><li>http 请求方法为 POST,并且只能为 POST</li><li>请求的 payload 如果使用 json 格式,header 需要指定为:Content-Type: application/json,如果使用 protobuf,指定为:application/protobuf</li><li>如果请求出现问题,server 返回的 response 同样用json 编码,会包含出错信息和标准的错误码</li></ul><h1 id="为什么不使用-gRPC"><a href="#为什么不使用-gRPC" class="headerlink" title="为什么不使用 gRPC"></a>为什么不使用 gRPC</h1><p>Google 提供的 gRPC 框架同样也会自动生成代码,Twitch 公司一开始也是使用 gRPC,但遇到了四个核心的问题是 gRPC 无法解决的,从而创建了 Twirp 框架:</p><ul><li>不支持 HTTP1.1,gRPC 只支持 http/2,因为它依赖http/2 来提供全双工流,但 Twirp 可以同时支持 http1.1 和 http/2。因为很多负载均衡(软件和硬件,譬如 AWS<br>的 ELB)现阶段都只支持 http1.1。Twirp 最大的缺点就是不支持流式 RPC,但 Twitch 发现绝大部分的请求都不需要使用流式 rpc</li><li>gRPC 生成的 go 代码比较少,因此调用了一个大型的运行时库:grpc-go,不幸的是,这个库偶尔会出现重大更新,就会导致无法编译旧的客户端代码,从而需要大规模更新客户端代码才能保证 gRPC 版本的一致性。而 Twirp 生成的代码里几乎包含了所有需要用到的代码,并且尽量少地进行重大版本更新,以此来保证旧系统的平稳运行。</li><li>grpc-go 并没有使用标准的网络库,而是自己实现了一个完整的 http/2 ,以此引入了自定义的网络通信控制机制。但这样的做法也会引入一些其他人难以理解的代码,并引发bug。</li><li>gRPC 只支持二进制格式的 protobuf payload,而 Twirp 还另外支持 json 格式的 payload,这样的好处有:1、易于编写跨语言的客户端代码,毕竟 Restful API 也是使用 json 格式的 payload。2、很容易使用 cURL 工具来调试服务端代码。而 gRPC 使用的二进制 protobuf payload 使用起来会感觉完全不透明</li></ul><p>总的来说,gRPC 最大的优势就是支持双向流式 RPC,客户端和服务端之间可以不间断地发送数据流,而 Twirp 没有这样的功能,只支持一问一答的交互方式,在 Twitch 公司里并没有一定要使用 gRPC 双向流式 RPC 的场景。并且 grpc-go 还集成了一篮子的插件,从<a href="https://pkg.go.dev/github.com/grpc/grpc-go/balancer?utm_source=godoc">负载均衡</a>到<a href="https://pkg.go.dev/google.golang.org/grpc/resolver?utm_source=godoc">服务发现</a>,新的思想意味新的学习成本。</p><h1 id="Twirp-在生产环境的表现"><a href="#Twirp-在生产环境的表现" class="headerlink" title="Twirp 在生产环境的表现"></a>Twirp 在生产环境的表现</h1><p>总结上文,如果你在编写后台服务,使用结构化的 rpc 要比自己手动编写 Restful API 更好,Twitch 已经使用 Twirp 来编写大量的后台系统。Twirp 的简洁性尤其值得称赞,因为你只需要专注于后端的代码逻辑,而不用纠结这个请求应该使用 PUT / POST 还是 DELETE 请求。简洁意味着稳定,Twitch 尚未观测到任何一例因为 Twirp 的 bug 带来的业务中断,因为 Twirp 本身没有太多可能引发故障的情况。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p>[1] <a href="https://github.com/Twitchtv/Twirp/issues/70#issuecomment-470367807">Twirp: a sweet new RPC framework for Go</a></p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>Twirp 是由 Twitch 公司开源的一个 RPC 框架,从 2018 年开源至今一直在国内默默无闻,我认为一个很重要的原因是国内的后端</summary>
<category term="后端开发" scheme="https://kylinlingh.github.io/categories/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
<category term="Golang" scheme="https://kylinlingh.github.io/tags/Golang/"/>
<category term="Twirp" scheme="https://kylinlingh.github.io/tags/Twirp/"/>
</entry>
<entry>
<title>[源码]spicedb: 源码阅读之第二篇(k8s 部署和运行)</title>
<link href="https://kylinlingh.github.io/2023/07/19/%E6%BA%90%E7%A0%81-spicedb-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E4%B9%8B%E7%AC%AC%E4%BA%8C%E7%AF%87-k8s-%E9%83%A8%E7%BD%B2/"/>
<id>https://kylinlingh.github.io/2023/07/19/%E6%BA%90%E7%A0%81-spicedb-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E4%B9%8B%E7%AC%AC%E4%BA%8C%E7%AF%87-k8s-%E9%83%A8%E7%BD%B2/</id>
<published>2023-07-18T16:00:00.000Z</published>
<updated>2023-07-25T08:13:30.293Z</updated>
<content type="html"><![CDATA[<p>spiceDB 的<a href="https://authzed.com/docs/spicedb/operator">官网 doc </a>有教程说明如何在 k8s 上面部署 spiceDB 集群,对于 k8s 新手来说,寥寥无几的几个命令是完全弄不懂这个集群是如何部署的,因此本文主要描述k8s 是如何部署和管理集群的。</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">curl -v -XGET -H "Accept: application/json" -H "User-Agent: kubectl/v1.25.2 (darwin/arm64) kubernetes/5835544" 'https://kubernetes.docker.internal:6443/apis/apiextensions.k8s.io/v1/customresourcedefinitions'</span><br></pre></td></tr></table></figure><h1 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h1><blockquote><p>Install the Operator with kubectl: kubectl apply –server-side -k github.com/authzed/spicedb-operator/config</p></blockquote><p>两个重要参数:</p><ul><li>–server-side:当我们使用命令 <code>kubectl apply -f <xxx.yaml> --dry-run </code> 验证yaml 文件是否写错时,这个 yaml 文件默认在本地运行而不将数据送至服务器,但是服务器可能会由于各种原因修改掉 yaml 文件,如果不能将这个对象送至服务器查看其最终的样子,那么用户可能就还是不清楚 apply yaml 文件的时候产生的实际效果是什么</li><li>-k: 实际上是 –kustomize 的缩写,表示要应用包含 kustomization 文件的目录中的资源,Kustomize 是一个用来定制 Kubernetes 配置的工具,详情参考<a href="https://kubernetes.io/zh-cn/docs/tasks/manage-kubernetes-objects/kustomization/">官网</a></li></ul><p>该命令实际上会使用kustomize工具加载 github 项目 github.com/authzed/spicedb-operator 下的 config 文件夹里的所有 yaml文件,然后拉取镜像运行(authzed/spicedb-operator),并使用 Deployment 来管理该镜像生成的容器。观察 config 文件夹里的目录:</p><figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">config</span><br><span class="line">├── crds</span><br><span class="line">│ ├── authzed.com_spicedbclusters.yaml</span><br><span class="line">│ └── kustomization.yaml</span><br><span class="line">├── kustomization.yaml</span><br><span class="line">├── operator.yaml</span><br><span class="line">└── rbac</span><br><span class="line"> ├── kustomization.yaml</span><br><span class="line"> ├── role.yaml</span><br><span class="line"> ├── spicedb-operator-edit.yaml</span><br><span class="line"> └── spicedb-operator-view.yaml</span><br></pre></td></tr></table></figure><p>k8s本身提供的资源(Deployment, ServiceAccount等)就不在此赘述了,下面描述在普通的 k8s 教程里没见到过的Kustomization 和 CustomResourceDefinition</p><h2 id="Kustomization"><a href="#Kustomization" class="headerlink" title="Kustomization"></a>Kustomization</h2><p>观察到 config 目录下有三个名字一致的文件(kustomization.yaml),这个文件其实是用来管理项目资源的。<a href="https://kubernetes.io/zh-cn/docs/tasks/manage-kubernetes-objects/kustomization/">官网</a> 描述了 Kustomzie 工具的作用,其中一个就是本项目的使用场景:</p><blockquote><p>一种常见的做法是在项目中构造资源集合并将其放到同一个文件或目录中管理。 Kustomize 提供基于不同文件来组织资源并向其应用补丁或者其他定制的能力。Kustomize 支持组合不同的资源。kustomization.yaml 文件的 resources 字段定义配置中要包含的资源列表。 你可以将 resources 列表中的路径设置为资源配置文件的路径。 </p></blockquote><p>spiceDB 只是单纯通过 Kustomizie 工具来组织用到的各种 yaml 文件,看 config/kustomization.yaml 文件内容:</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 固定值</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">kustomize.config.k8s.io/v1beta1</span></span><br><span class="line"><span class="comment"># 固定值</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Kustomization</span></span><br><span class="line"><span class="comment"># 表示 k8s 资源的位置,这个可以是一个文件,也可以指向一个文件夹,读取的时候会按照顺序读取,路径可以是相对路径也可以是绝对路径,如果是相对路径那么就是相对于 kustomization.yml的路径</span></span><br><span class="line"><span class="comment"># 注意,resources下的资源文件顺序不管怎么放,kubectl apply -k -f config/ 的时候会自动处理资源依赖问题;如果不是用 kustomize 工具,那么直接 kubectl apply -f config/ 一个文件夹时会按照文件名顺序逐个加载,就可能出现先加载的文件找不到后加载文件的问题</span></span><br><span class="line"><span class="attr">resources:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">crds</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">rbac</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">operator.yaml</span></span><br><span class="line"><span class="comment"># Kustomize 还提供定制容器镜像或者将其他对象的字段值注入到容器中的能力,并且不需要创建补丁。 例如,可以通过在 kustomization.yaml 文件的 images 字段设置新的镜像来指定/更改容器中使用的镜像</span></span><br><span class="line"><span class="comment"># 此处用于指定 operatory.yaml 的 Deployment 里的 image 使用:ghcr.io/authzed/spicedb-operator:latest</span></span><br><span class="line"><span class="attr">images:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">ghcr.io/authzed/spicedb-operator</span></span><br><span class="line"> <span class="attr">newTag:</span> <span class="string">latest</span></span><br></pre></td></tr></table></figure><p>使用命令查看将这些资源合并后最终生成的资源</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看包含 kustomization 文件的目录中的资源,可以看到生成的资源是按照特定顺序的,首先是 Namespace,然后是 CustomResourceDefiniton...,下面有源码解析</span></span><br><span class="line">kubectl kustomize config</span><br><span class="line"></span><br><span class="line">kubectl get -k config</span><br><span class="line">kubectl describe -k config</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">然后再apply 进去k8s,就表示一次性将整个 config 目录下的所有 yaml 文件都加载到 k8s 里</span></span><br><span class="line">kubectl apply -k config</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">删除</span></span><br><span class="line">kubectl delete -k config</span><br></pre></td></tr></table></figure><p>查看 kustomzie 的源码,可以看到资源构建的顺序<a href="#refer-anchor"><sup>[3]</sup></a></p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> orderFirst = []<span class="type">string</span>{</span><br><span class="line"><span class="string">"Namespace"</span>,</span><br><span class="line"><span class="string">"ResourceQuota"</span>,</span><br><span class="line"><span class="string">"StorageClass"</span>,</span><br><span class="line"><span class="string">"CustomResourceDefinition"</span>,</span><br><span class="line"><span class="string">"ServiceAccount"</span>,</span><br><span class="line"><span class="string">"PodSecurityPolicy"</span>,</span><br><span class="line"><span class="string">"Role"</span>,</span><br><span class="line"><span class="string">"ClusterRole"</span>,</span><br><span class="line"><span class="string">"RoleBinding"</span>,</span><br><span class="line"><span class="string">"ClusterRoleBinding"</span>,</span><br><span class="line"><span class="string">"ConfigMap"</span>,</span><br><span class="line"><span class="string">"Secret"</span>,</span><br><span class="line"><span class="string">"Endpoints"</span>,</span><br><span class="line"><span class="string">"Service"</span>,</span><br><span class="line"><span class="string">"LimitRange"</span>,</span><br><span class="line"><span class="string">"PriorityClass"</span>,</span><br><span class="line"><span class="string">"PersistentVolume"</span>,</span><br><span class="line"><span class="string">"PersistentVolumeClaim"</span>,</span><br><span class="line"><span class="string">"Deployment"</span>,</span><br><span class="line"><span class="string">"StatefulSet"</span>,</span><br><span class="line"><span class="string">"CronJob"</span>,</span><br><span class="line"><span class="string">"PodDisruptionBudget"</span>,</span><br><span class="line">}</span><br><span class="line"><span class="keyword">var</span> orderLast = []<span class="type">string</span>{</span><br><span class="line"><span class="string">"MutatingWebhookConfiguration"</span>,</span><br><span class="line"><span class="string">"ValidatingWebhookConfiguration"</span>,</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>另外提一下,kustomize 还能用于打布丁,管理不同运行环境的配置文件等重要用途。</p><h2 id="CustomResourceDefinition"><a href="#CustomResourceDefinition" class="headerlink" title="CustomResourceDefinition"></a>CustomResourceDefinition</h2><p>对于文件 config/crds/authzed.com_spicedbclusters.yaml,明显能看到 kind 并不是我们熟知的 k8s 内置资源(Pod,Deployment,ReplicaSet 等),这是因为 spiceDB 使用了 k8s 的自定义资源(CRM, CustomResourceDefinitions)来扩展 k8s 的功能,使用 CRD 可以在不修改 k8s源码的基础上扩展 k8s的功能。</p><p>文件路径:<a href="https://github.com/authzed/spicedb-operator/tree/main/pkg/crds/authzed.com_spicedbclusters.yaml">https://github.com/authzed/spicedb-operator/tree/main/pkg/crds/authzed.com_spicedbclusters.yaml</a></p><p>挑出 CustomResourceDefinitions 中关键的字段如下:</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># CRD本身也是资源,大于1.7.0版本的集群可以使用apiextensions.k8s.io/v1beta1API访问CRD,大于1.16.0版本则可以使用apiextensions.k8s.io/v1API</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">apiextensions.k8s.io/v1</span></span><br><span class="line"><span class="comment"># kind 的值是固定的</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">CustomResourceDefinition</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line"> <span class="attr">annotations:</span></span><br><span class="line"> <span class="attr">controller-gen.kubebuilder.io/version:</span> <span class="string">v0.9.2</span></span><br><span class="line"> <span class="attr">creationTimestamp:</span> <span class="literal">null</span></span><br><span class="line"> <span class="comment"># 名称必须符合如下格式:<plural>.<group></span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">spicedbclusters.authzed.com</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line"> <span class="comment"># 组名称,用于 REST API: /apis/<组>/<版本></span></span><br><span class="line"> <span class="attr">group:</span> <span class="string">authzed.com</span></span><br><span class="line"> <span class="attr">names:</span></span><br><span class="line"> <span class="attr">categories:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">authzed</span></span><br><span class="line"> <span class="comment"># kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。</span></span><br><span class="line"> <span class="attr">kind:</span> <span class="string">SpiceDBCluster</span></span><br><span class="line"> <span class="attr">listKind:</span> <span class="string">SpiceDBClusterList</span></span><br><span class="line"> <span class="comment"># 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式></span></span><br><span class="line"> <span class="attr">plural:</span> <span class="string">spicedbclusters</span></span><br><span class="line"> <span class="attr">shortNames:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">spicedbs</span></span><br><span class="line"> <span class="comment"># 名称的单数形式,作为命令行使用时和显示时的别名</span></span><br><span class="line"> <span class="attr">singular:</span> <span class="string">spicedbcluster</span></span><br><span class="line"> <span class="attr">scope:</span> <span class="string">Namespaced</span></span><br><span class="line"> <span class="attr">versions:</span></span><br><span class="line"> <span class="comment"># kubectl get 命令要显示的列有哪些</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">additionalPrinterColumns:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">jsonPath:</span> <span class="string">.metadata.creationTimestamp</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">Age</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">date</span></span><br><span class="line"> <span class="comment"># 此 CustomResourceDefinition 所支持的版本</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">v1alpha1</span></span><br><span class="line"> <span class="attr">schema:</span></span><br><span class="line"> <span class="comment"># openAPIV3Schema 是验证自定义对象的模式。</span></span><br><span class="line"> <span class="attr">openAPIV3Schema:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">SpiceDBCluster</span> <span class="string">defines</span> <span class="string">all</span> <span class="string">options</span> <span class="string">for</span> <span class="string">a</span> <span class="string">full</span> <span class="string">SpiceDB</span> <span class="string">cluster</span></span><br><span class="line"> <span class="comment"># 定义创建容器的 yaml 文件的全部字段</span></span><br><span class="line"> <span class="attr">properties:</span></span><br><span class="line"> <span class="attr">apiVersion:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">'...'</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">string</span></span><br><span class="line"> <span class="attr">kind:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">'...'</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">string</span></span><br><span class="line"> <span class="attr">metadata:</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">object</span></span><br><span class="line"> <span class="attr">spec:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">ClusterSpec</span> <span class="string">holds</span> <span class="string">the</span> <span class="string">desired</span> <span class="string">state</span> <span class="string">of</span> <span class="string">the</span> <span class="string">cluster.</span></span><br><span class="line"> <span class="attr">properties:</span></span><br><span class="line"> <span class="comment"># 重点:spec.config 用来保存 spiceDB 启动时的各种参数</span></span><br><span class="line"> <span class="attr">config:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">Config</span> <span class="string">values</span> <span class="string">to</span> <span class="string">be</span> <span class="string">passed</span> <span class="string">to</span> <span class="string">the</span> <span class="string">cluster</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">object</span></span><br><span class="line"> <span class="comment"># 阻止无法识别的字段</span></span><br><span class="line"> <span class="attr">x-kubernetes-preserve-unknown-fields:</span> <span class="literal">true</span></span><br><span class="line"> <span class="comment"># spec.secretName 的值必须是 string 类型 </span></span><br><span class="line"> <span class="attr">secretName:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">SecretName</span> <span class="string">points</span> <span class="string">to</span> <span class="string">a</span> <span class="string">secret</span> <span class="string">(in</span> <span class="string">the</span> <span class="string">same</span> <span class="string">namespace)</span></span><br><span class="line"> <span class="string">that</span> <span class="string">holds</span> <span class="string">secret</span> <span class="string">config</span> <span class="string">for</span> <span class="string">the</span> <span class="string">cluster</span> <span class="string">like</span> <span class="string">passwords,</span> <span class="string">credentials,</span></span><br><span class="line"> <span class="string">etc.</span> <span class="string">If</span> <span class="string">the</span> <span class="string">secret</span> <span class="string">is</span> <span class="string">omitted,</span> <span class="string">one</span> <span class="string">will</span> <span class="string">be</span> <span class="string">generated</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">string</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">object</span></span><br><span class="line"> <span class="attr">status:</span></span><br><span class="line"> <span class="attr">description:</span> <span class="string">ClusterStatus</span> <span class="string">communicates</span> <span class="string">the</span> <span class="string">observed</span> <span class="string">state</span> <span class="string">of</span> <span class="string">the</span> <span class="string">cluster.</span></span><br><span class="line"> <span class="attr">properties:</span></span><br><span class="line"> <span class="string">//...</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">object</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">object</span></span><br><span class="line"> <span class="comment"># 开启/关闭该API</span></span><br><span class="line"> <span class="attr">served:</span> <span class="literal">true</span></span><br><span class="line"> <span class="comment"># 有且只能有一个版本要将storage设置为true</span></span><br><span class="line"> <span class="attr">storage:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">subresources:</span></span><br><span class="line"> <span class="attr">status:</span> {}</span><br></pre></td></tr></table></figure><p>将该文件 apply 之后,就可以看到该 CRD</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">kubectl apply -f authzed.com_spicedbclusters.yaml</span><br><span class="line"></span><br><span class="line">kubectl get crd</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">NAME CREATED AT</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">spicedbclusters.authzed.com 2023-07-10T07:08:48Z</span></span><br></pre></td></tr></table></figure><p>将该 CRD apply 之后,这样一个新的受名字空间约束的 RESTful API 端点会被创建在:/apis/authzed.com/v1alpha1/namespaces/*/spicedbclusters/… 今后如果发起对类型为 SpiceDBCluster 的对象的处理,k8s 的 apiserver 就能识别到该对象类型并对此路径发起 restful 请求,转发给对应的CRD controller(需要写一个 controller,即 spicedb-operator)。</p><p>接下来继续看官网 doc 是如何创建 SpiceDBCluster 对象的:</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">kubectl apply --server-side -f - <<EOF</span><br><span class="line">apiVersion: authzed.com/v1alpha1</span><br><span class="line">kind: SpiceDBCluster</span><br><span class="line">metadata:</span><br><span class="line"> name: dev</span><br><span class="line">spec:</span><br><span class="line"> config:</span><br><span class="line"> datastoreEngine: memory</span><br><span class="line"> secretName: dev-spicedb-config</span><br><span class="line">---</span><br><span class="line">apiVersion: v1</span><br><span class="line">kind: Secret</span><br><span class="line">metadata:</span><br><span class="line"> name: dev-spicedb-config</span><br><span class="line">stringData:</span><br><span class="line"> preshared_key: "averysecretpresharedkey"</span><br><span class="line">EOF</span><br></pre></td></tr></table></figure><p>执行该命令之后,就能成功创建 SpiceDBCluster 对象并生成容器运行。</p><p>从上面创建 SpiceDBCluster 容器的命令来看,甚至没有指明使用哪个镜像,那么容器是怎么被创建出来的呢?其实CRD 只是定义了 SpiceDBCluster 类型,要想真正使用这个类型来创建和管理容器,还需要跟自定义控制器(Controller)结合起来才能真正管理资源。自定义的控制器是通过镜像 spicedb-operator 实现的,详情查看另一篇博文。</p><h1 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h1><div id="refer-anchor"></div><p>[1] <a href="https://yanhang.me/post/2021-ssa/">Kubernetes 的 Server-Side Apply</a><br>[2] <a href="https://lailin.xyz/post/operator-04-kustomize-tutorial.html">kustomize 简明教程</a><br>[3] <a href="https://izsk.me/2020/12/27/Kubernetes-Apply-Directory-Order/">Kubernetes学习(当使用kubectl apply -f 多个资源时,资源创建的顺序是怎么样的)</a><br>[4] <a href="https://kubernetes.io/zh-cn/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/">使用 CustomResourceDefinition 扩展 Kubernetes API</a></p>]]></content>
<summary type="html"><p>spiceDB 的<a href="https://authzed.com/docs/spicedb/operator">官网 doc </a>有教程说明如何在 k8s 上面部署 spiceDB 集群,对于 k8s 新手来说,寥寥无几的几个命令是完全弄不懂这个集群是如何部署</summary>
<category term="源码阅读" scheme="https://kylinlingh.github.io/categories/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="授权" scheme="https://kylinlingh.github.io/tags/%E6%8E%88%E6%9D%83/"/>
<category term="零信任" scheme="https://kylinlingh.github.io/tags/%E9%9B%B6%E4%BF%A1%E4%BB%BB/"/>
</entry>
<entry>
<title>[源码]spicedb: 源码阅读之第一篇(热点缓存)</title>
<link href="https://kylinlingh.github.io/2023/06/28/%E6%BA%90%E7%A0%81-spicedb-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E4%B9%8B%E7%AC%AC%E4%B8%80%E7%AF%87-%E7%83%AD%E7%82%B9%E7%BC%93%E5%AD%98/"/>
<id>https://kylinlingh.github.io/2023/06/28/%E6%BA%90%E7%A0%81-spicedb-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E4%B9%8B%E7%AC%AC%E4%B8%80%E7%AF%87-%E7%83%AD%E7%82%B9%E7%BC%93%E5%AD%98/</id>
<published>2023-06-27T16:00:00.000Z</published>
<updated>2023-07-19T03:58:56.846Z</updated>
<content type="html"><![CDATA[<h1 id="Hotspot-Caching"><a href="#Hotspot-Caching" class="headerlink" title="Hotspot Caching"></a>Hotspot Caching</h1><p>由于 2/8 原则的存在,笼统来说,系统中百分之二十的数据占据了全部访问流量的百分之八十(其实还有更极端的 1/99 现象,百分之一的数据占据了流量的百分之九十九),因此热点数据的处理技术一直都是低时延和高可用系统的前沿研究对象,而 Hotspot caching(热点缓存)就是专门用于降低热点数据被高频访问时的延迟。</p><p>SpiceDB 处理热点数据的方法主要由两部分组成:</p><ul><li>在 node 上使用本地缓存将查询结果保存起来,但每个 node 都只缓存了在本 node 上计算的结果(也就是说不会对不同的 node 同步缓存数据)</li><li>使用一致性哈希算法,请求到达时先查询本地缓存,查询失败后会根据请求体的参数计算哈希值,然后往对应的 node 发送请求,一致性哈希算法的原理参考Reference 里的<a href="#refer-anchor"><sup>[1][2][3]</sup></a>。此处简单描述一下原理:<img src="https://uploads.toptal.io/blog/image/122756/toptal-blog-image-1492519276035-d42459e7afcbed4a7ccb88d45b3d750b.jpg" width="50%" height="50%" align=center></li></ul><p>在上图中,要缓存的值为白色节点,将这些要散列的值映射到一个圆环上(计算哈希值,然后将哈希值与圆环本身的角度做关联),然后加入三个服务器节点(A/B/C:同样计算三个服务器节点的哈希值并与圆环本身的角度做关联),此时定义一个规则将两者关联起来:每个 key 分配到逆时针方向(或顺时针)上离它最近的服务器,由此得到了下面的映射关系:<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-07-18-10-42-57.png"></p><p><strong>从编程的角度来看,要做的是保存一个服务器值的有序列表(可以是角度或数字列表),然后遍历此列表(或使用二分查找)以找到第一个值大于或等于检索的key的hash值的服务器,然后从该服务器取出key对应的value</strong>。<a href="#refer-anchor"><sup>[1]</sup></a>为了确保 kv 能够均匀分配到服务器上,将每个真实的 server 虚拟出多个服务器节点出来,譬如 A 服务器性能更强,就虚拟出 10 个节点,B 服务器性能弱就虚拟 5 个节点,此时就能实现负载的调节,在 spiceDb 中,权重的参数名为 replicationFactor。</p><h1 id="Cache-Entry"><a href="#Cache-Entry" class="headerlink" title="Cache Entry"></a>Cache Entry</h1><p>在 SpiceDB 中,一条 cache entry 的格式大概是这样的:<code><object>#<relation>@<user>@<snapshot timestamp> → <result></code>,result 有两种表示:PERMISSIONSHIP_HAS_PERMISSION(拥有权限)/ PERMISSIONSHIP_NO_PERMISSION(没有权限)。举例两个cache entry:</p><ul><li><code>document:doc1#reader@user:francesca@12345 → PERMISSIONSHIP_HAS_PERMISSION</code></li><li><code>document:doc1#owner@user:francesca@12345 → PERMISSIONSHIP_NO_PERMISSION</code></li></ul><p>请求落到了 server 之后,如果没有命中 snapshot timestamp,则不能直接使用缓存里的数据,而 snapshot timestamp 的选择策略又与 consistency level 参数有关(<a href="">参考这里</a>)。</p><h1 id="gRPC-负载均衡"><a href="#gRPC-负载均衡" class="headerlink" title="gRPC 负载均衡"></a>gRPC 负载均衡</h1><p>gRPC 的负载均衡是基于每次调用,而不是基于连接的,因为 gRPC 客户端会与所有的 server 都预先建立好连接。并且 gRPC 采取的是客户端负载均衡,由客户端在每次发起请求时根据策略选择连接。原理<a href="https://github.com/grpc/grpc/blob/master/doc/load-balancing.md">如下</a>:</p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/iShot_2023-07-10_11.41.37.png" width="70%" height="70%" align=center><!-- ![](https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/iShot_2023-07-10_11.41.37.png) --><p>过程:</p><ol><li>gRPC 客户端启动的时候会先请求 name resolver 服务器,获取服务端名解析出来的全部 ip 列表,还有服务端的服务配置信息(主要用于指定服务端的负载均衡策略和其他属性信息)</li><li>客户端实例化负载均衡策略,并向其传递服务端的配置信息,还有 ip 地址列表和其他属性</li><li>客户端的负载均衡实例会与resolver 返回的全部 ip 都建立连接,同时会监视所有连接的连接状态,以便及时重连</li><li>负载均衡实例会在客户端发起 rpc 连接时根据策略选取对应的连接来发送请求</li></ol><p>gRPC 内置了服务治理功能, 支持自定义 Resolver 来实现自定义的服务发现机制,自定义 Balancer 来实现自定义的负载均衡策略:</p><ul><li>Resolver 是解析器,用于从注册中心实时获取当前服务端的 ip:port 列表,同步发送给 Balancer</li><li>Balancer 是平衡器,主要有两个作用<ul><li>接收 Resolver 发来的服务端 ip:port 列表,同时与所有服务端建立并维护长连接状态(使用长连接可以避免每次 rpc 调用时创建新连接的开销)</li><li>当客户端发起 rpc 调用时,按照负载均衡算法从连接池中选择一个连接进行 rpc 调用</li></ul></li></ul><h1 id="SpiceDB-的负载均衡"><a href="#SpiceDB-的负载均衡" class="headerlink" title="SpiceDB 的负载均衡"></a>SpiceDB 的负载均衡</h1><p>下图是 spiceDB 处理一个权限验证请求的过程:</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-07-19-11-57-23.png"></p><h2 id="自定义-Resolver"><a href="#自定义-Resolver" class="headerlink" title="自定义 Resolver"></a>自定义 Resolver</h2><p>SpiceDB 使用自定义的名称解析器 <a href="https://github.com/sercand/kuberesolver">kuberesolver</a> 来自动发现上线的 node,如果是在 Kubernets 上运行 SpiceDB,kuberresolver 就会使用 kubernetes API 来发现和监控服务的 IP 地址(需要在 server 的启动参数里添加如下参数):</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">--dispatch-upstream-addr=kubernetes:///spicedb.default:50053</span><br></pre></td></tr></table></figure><p>就可以在 service 为”spicedb”,namespace 为”default”下找到所有的 node 实例。当客户端通过 Dial 方法对指定服务进行拨号时,grpc resolver 查找注册的 Builder 实例调用其 Build() 方法构建自定义 kubeResolver。</p><p>代码执行流程如下:</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewServeCommand</span><span class="params">(programName <span class="type">string</span>, config *server.Config)</span></span> *cobra.Command {</span><br><span class="line"> <span class="keyword">return</span> &cobra.Command{</span><br><span class="line"> Use: <span class="string">"serve"</span>,</span><br><span class="line"> Short: <span class="string">"serve the permissions database"</span>,</span><br><span class="line"> Long: <span class="string">"A database that stores, computes, and validates application permissions"</span>,</span><br><span class="line"> PreRunE: server.DefaultPreRunE(programName),</span><br><span class="line"> <span class="comment">// RunE 绑定的函数是真正要运行的函数</span></span><br><span class="line"> RunE: termination.PublishError(<span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="type">string</span>)</span></span> <span class="type">error</span> {</span><br><span class="line"> server, err := config.Complete(cmd.Context())</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"> signalctx := SignalContextWithGracePeriod(</span><br><span class="line"> context.Background(),</span><br><span class="line"> config.ShutdownGracePeriod,</span><br><span class="line"> )</span><br><span class="line"> <span class="keyword">return</span> server.Run(signalctx)</span><br><span class="line"> }),</span><br><span class="line"> Example: server.ServeExample(programName),</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Config)</span></span> Complete(ctx context.Context) (RunnableServer, <span class="type">error</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 初始化调度器</span></span><br><span class="line"> dispatcher, err = combineddispatch.NewDispatcher(</span><br><span class="line"> combineddispatch.UpstreamAddr(c.DispatchUpstreamAddr),</span><br><span class="line"> combineddispatch.UpstreamCAPath(c.DispatchUpstreamCAPath),</span><br><span class="line"> combineddispatch.GrpcPresharedKey(dispatchPresharedKey),</span><br><span class="line"> <span class="comment">// hashingConfigJSON 的默认值:{"loadBalancingConfig":[{"consistent-hashring":{"replicationFactor":100,"spread":1}}]}</span></span><br><span class="line"> combineddispatch.GrpcDialOpts(</span><br><span class="line"> grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),</span><br><span class="line"> grpc.WithDefaultServiceConfig(hashringConfigJSON),</span><br><span class="line"> ),</span><br><span class="line"> combineddispatch.MetricsEnabled(c.DispatchClientMetricsEnabled),</span><br><span class="line"> combineddispatch.PrometheusSubsystem(c.DispatchClientMetricsPrefix),</span><br><span class="line"> combineddispatch.Cache(cc),</span><br><span class="line"> combineddispatch.ConcurrencyLimits(concurrencyLimits),</span><br><span class="line"> )</span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewDispatcher</span><span class="params">(options ...Option)</span></span> (dispatch.Dispatcher, <span class="type">error</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// 在 server 的启动参数里指定了 --dispatch-upstream-addr 后,会在这里初始化resolver 实例。只有在该参数设置为:kubernetes:///spicedb.default:50053 时才会调用自定义的 kuberResolver,如果设置为ip:port(如:localhost:500053),就会使用默认的 resolver:passthroughResolver</span></span><br><span class="line"> <span class="keyword">if</span> opts.upstreamAddr != <span class="string">""</span> {</span><br><span class="line"> <span class="keyword">if</span> opts.upstreamCAPath != <span class="string">""</span> {</span><br><span class="line"> customCertOpt, err := grpcutil.WithCustomCerts(grpcutil.VerifyCA, opts.upstreamCAPath)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> opts.grpcDialOpts = <span class="built_in">append</span>(opts.grpcDialOpts, customCertOpt)</span><br><span class="line"> opts.grpcDialOpts = <span class="built_in">append</span>(opts.grpcDialOpts, grpcutil.WithBearerToken(opts.grpcPresharedKey))</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> opts.grpcDialOpts = <span class="built_in">append</span>(opts.grpcDialOpts, grpcutil.WithInsecureBearerToken(opts.grpcPresharedKey))</span><br><span class="line"> opts.grpcDialOpts = <span class="built_in">append</span>(opts.grpcDialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> opts.grpcDialOpts = <span class="built_in">append</span>(opts.grpcDialOpts, grpc.WithDefaultCallOptions(grpc.UseCompressor(<span class="string">"s2"</span>)))</span><br><span class="line"></span><br><span class="line"> <span class="comment">// gRPC 连接 upstreamAddr</span></span><br><span class="line"> conn, err := grpc.Dial(opts.upstreamAddr, opts.grpcDialOpts...)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> redispatch = remote.NewClusterDispatcher(v1.NewDispatchServiceClient(conn), conn, remote.ClusterDispatcherConfig{</span><br><span class="line"> KeyHandler: &keys.CanonicalKeyHandler{},</span><br><span class="line"> DispatchOverallTimeout: opts.remoteDispatchTimeout,</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// gRPC 内部经过多个调用链:grpc.Dial -> grpc.DialContext -> grpc.newCCResolverWrapper -></span></span><br><span class="line"><span class="comment">// 最终调用到了自定义的 resolver</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(b *kubeBuilder)</span></span> Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, <span class="type">error</span>) {</span><br><span class="line"> <span class="keyword">if</span> b.k8sClient == <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">if</span> cl, err := NewInClusterK8sClient(); err == <span class="literal">nil</span> {</span><br><span class="line"> b.k8sClient = cl</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ti, err := parseResolverTarget(target)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> ti.serviceNamespace == <span class="string">""</span> {</span><br><span class="line"> ti.serviceNamespace = getCurrentNamespaceOrDefault()</span><br><span class="line"> }</span><br><span class="line"> ctx, cancel := context.WithCancel(context.Background())</span><br><span class="line"> r := &kResolver{</span><br><span class="line"> target: ti,</span><br><span class="line"> ctx: ctx,</span><br><span class="line"> cancel: cancel,</span><br><span class="line"> cc: cc,</span><br><span class="line"> rn: <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>{}, <span class="number">1</span>),</span><br><span class="line"> k8sClient: b.k8sClient,</span><br><span class="line"> t: time.NewTimer(defaultFreq),</span><br><span class="line"> freq: defaultFreq,</span><br><span class="line"></span><br><span class="line"> endpoints: endpointsForTarget.WithLabelValues(ti.String()),</span><br><span class="line"> addresses: addressesForTarget.WithLabelValues(ti.String()),</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 在一个 goroutinue 里持续监控所有注册到 k8s 的 endpoint</span></span><br><span class="line"> <span class="keyword">go</span> until(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"> r.wg.Add(<span class="number">1</span>)</span><br><span class="line"> err := r.watch()</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> && err != io.EOF {</span><br><span class="line"> grpclog.Errorf(<span class="string">"kuberesolver: watching ended with error='%v', will reconnect again"</span>, err)</span><br><span class="line"> }</span><br><span class="line"> }, time.Second, ctx.Done())</span><br><span class="line"> <span class="keyword">return</span> r, <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewInClusterK8sClient</span><span class="params">()</span></span> (K8sClient, <span class="type">error</span>) {</span><br><span class="line"> <span class="comment">// 在 pod 里获取环境变量,最终构造成 k8s 内部 api 的调用地址</span></span><br><span class="line"> host, port := os.Getenv(<span class="string">"KUBERNETES_SERVICE_HOST"</span>), os.Getenv(<span class="string">"KUBERNETES_SERVICE_PORT"</span>)</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">len</span>(host) == <span class="number">0</span> || <span class="built_in">len</span>(port) == <span class="number">0</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">"unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined"</span>)</span><br><span class="line"> }</span><br><span class="line"> token, err := ioutil.ReadFile(serviceAccountToken)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> ca, err := ioutil.ReadFile(serviceAccountCACert)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> certPool := x509.NewCertPool()</span><br><span class="line"> certPool.AppendCertsFromPEM(ca)</span><br><span class="line"> transport := &http.Transport{TLSClientConfig: &tls.Config{</span><br><span class="line"> MinVersion: tls.VersionTLS10,</span><br><span class="line"> RootCAs: certPool,</span><br><span class="line"> }}</span><br><span class="line"> httpClient := &http.Client{Transport: transport, Timeout: time.Nanosecond * <span class="number">0</span>}</span><br><span class="line"></span><br><span class="line"> client := &k8sClient{</span><br><span class="line"> <span class="comment">// k8s 内部 api 的 url 地址</span></span><br><span class="line"> host: <span class="string">"https://"</span> + net.JoinHostPort(host, port),</span><br><span class="line"> token: <span class="type">string</span>(token),</span><br><span class="line"> httpClient: httpClient,</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取 SpiceDB 的可用 node,其实就是访问 k8s 提供的服务注册 api 实现的</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getEndpoints</span><span class="params">(client K8sClient, namespace, targetName <span class="type">string</span>)</span></span> (Endpoints, <span class="type">error</span>) {</span><br><span class="line"> u, err := url.Parse(fmt.Sprintf(<span class="string">"%s/api/v1/namespaces/%s/endpoints/%s"</span>,</span><br><span class="line"> client.Host(), namespace, targetName))</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> Endpoints{}, err</span><br><span class="line"> }</span><br><span class="line"> req, err := client.GetRequest(u.String())</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> Endpoints{}, err</span><br><span class="line"> }</span><br><span class="line"> resp, err := client.Do(req)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> Endpoints{}, err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line"> <span class="keyword">if</span> resp.StatusCode != http.StatusOK {</span><br><span class="line"> <span class="keyword">return</span> Endpoints{}, fmt.Errorf(<span class="string">"invalid response code %d for service %s in namespace %s"</span>, resp.StatusCode, targetName, namespace)</span><br><span class="line"> }</span><br><span class="line"> result := Endpoints{}</span><br><span class="line"> err = json.NewDecoder(resp.Body).Decode(&result)</span><br><span class="line"> <span class="keyword">return</span> result, err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">---------------------------------------------------</span><br><span class="line"><span class="comment">// 监控 endpoint</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(k *kResolver)</span></span> watch() <span class="type">error</span> {</span><br><span class="line"> <span class="keyword">defer</span> k.wg.Done()</span><br><span class="line"> <span class="comment">// watch endpoints lists existing endpoints at start</span></span><br><span class="line"> sw, err := watchEndpoints(k.ctx, k.k8sClient, k.target.serviceNamespace, k.target.serviceName)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> {</span><br><span class="line"> <span class="keyword">select</span> {</span><br><span class="line"> <span class="keyword">case</span> <-k.ctx.Done():</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> <span class="comment">// 兜底策略:定期每 30 分钟获取一次全部 endpoint 的实例信息,检查是否有新的 endpoint。resolve()函数内部也是调用了 handle()函数</span></span><br><span class="line"> <span class="keyword">case</span> <-k.t.C:</span><br><span class="line"> k.resolve()</span><br><span class="line"> <span class="keyword">case</span> <-k.rn:</span><br><span class="line"> k.resolve()</span><br><span class="line"> <span class="comment">// 发现新的 endpoint</span></span><br><span class="line"> <span class="keyword">case</span> up, hasMore := <-sw.ResultChan():</span><br><span class="line"> <span class="keyword">if</span> hasMore {</span><br><span class="line"> k.handle(up.Object)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 持续监控所有注册到 k8s 的 endpoint</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">watchEndpoints</span><span class="params">(ctx context.Context, client K8sClient, namespace, targetName <span class="type">string</span>)</span></span> (watchInterface, <span class="type">error</span>) {</span><br><span class="line"> u, err := url.Parse(fmt.Sprintf(<span class="string">"%s/api/v1/watch/namespaces/%s/endpoints/%s"</span>,</span><br><span class="line"> client.Host(), namespace, targetName))</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> req, err := client.GetRequest(u.String())</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> req = req.WithContext(ctx)</span><br><span class="line"> resp, err := client.Do(req)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> resp.StatusCode != http.StatusOK {</span><br><span class="line"> <span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">"invalid response code %d for service %s in namespace %s"</span>, resp.StatusCode, targetName, namespace)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> newStreamWatcher(resp.Body), <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>总结调用流程:</p><ul><li>客户端启动时,通过 kuberesolver.RegisterInCluster() 注册自定义的 kubeResolver</li><li>在cobra 的 Command(serve)中通过 grpc.Dial(opts.upstreamAddr, opts.grpcDialOpts…) 来初始化自定义的kubeResolver<ul><li>grpc.DialContext()方法内部解析 URI(kubernetes:///spicedb.default:50053),解析到协议类型为 kubernetes,因此匹配到了自定义的kubeResolver,调用kubeBuilder.Build()方法构建 kubeResolver,同时开启 goroutinue,通过此 resolver 更新被调用服务(spicedb.default)对应的实例列表(所有注册到该服务的server node)</li></ul></li><li>grpc 底层 LB 库会对每个服务实例创建一个 subConnection,最终根据自定义的负载均衡策略,在每次发起 gRPC 调用时选择合适的 subConnection 处理请求</li></ul><p>参考其他人做的流程图(把图中的nsResolver替换成kubeResolver即可):<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-07-19-11-38-45.png"></p><h2 id="自定义-Balancer"><a href="#自定义-Balancer" class="headerlink" title="自定义 Balancer"></a>自定义 Balancer</h2><p>自定义的 resolver 解析到了所有 spiceDB node 的真实地址列表后,Balancer 负责控制客户端和这些<strong>服务端的地址之一</strong>建立连接(只会建立一个正常的连接)并使用该连接处理所有 rpc 请求。</p><p>通过 kubersolver,grpc-go 客户端就可以找到所有SpiceDB 的 node,SpiceDB 自定义了一个负载均衡器来支持一致性哈希算法,该算法会聚合request 里的不同参数(如下所示)再计算哈希值:</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">cr/<req.Metadata.AtRevision>@<req.ResourceRelation>@<req.ResourceIds>@<req.Subject>@<req.ResultsSetting></span><br></pre></td></tr></table></figure><p>计算 hash key 的算法</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="comment">// checkRequestToKey converts a check request into a cache key based on the relation</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">checkRequestToKey</span><span class="params">(req *v1.DispatchCheckRequest, option dispatchCacheKeyHashComputeOption)</span></span> DispatchCacheKey {</span><br><span class="line"><span class="keyword">return</span> dispatchCacheKeyHash(checkViaRelationPrefix, req.Metadata.AtRevision, option,</span><br><span class="line">hashableRelationReference{req.ResourceRelation},</span><br><span class="line">hashableIds(req.ResourceIds),</span><br><span class="line">hashableOnr{req.Subject},</span><br><span class="line">hashableResultSetting(req.ResultsSetting),</span><br><span class="line">)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>回顾上面的图,dispatchServiceClient.DispatchCheck 函数通过 gRPC 调用其他 node 的时候,底层实际上调用了grpc.(*pickerWrapper).pick() 函数,spiceDB 自定义的 Balancer 实现接口:balancer.Picker:</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"><span class="comment">//每次 rpc 调用时,返回 chosen 对应的连接</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *consistentHashringPicker)</span></span> Pick(info balancer.PickInfo) (balancer.PickResult, <span class="type">error</span>) {</span><br><span class="line"><span class="comment">// 存储在 context 中的 key(CtxKey)用于计算哈希值,这个 key 就是上面所说的 request 参数被聚合后的结果</span></span><br><span class="line"> key := info.Ctx.Value(CtxKey).([]<span class="type">byte</span>)</span><br><span class="line"> <span class="comment">// FindN 函数用于从哈希环里选择节点</span></span><br><span class="line">members, err := p.hashring.FindN(key, p.spread)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> balancer.PickResult{}, err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">index := <span class="number">0</span></span><br><span class="line"> <span class="comment">// 如果 spread 大于 1,会选择一个随机数</span></span><br><span class="line"><span class="keyword">if</span> p.spread > <span class="number">1</span> {</span><br><span class="line"><span class="comment">// <span class="doctag">TODO:</span> should look into other options for this to avoid locking; we mostly use spread 1 so it's not urgent</span></span><br><span class="line"><span class="comment">// rand is not safe for concurrent use</span></span><br><span class="line">p.Lock()</span><br><span class="line">index = p.rand.Intn(<span class="type">int</span>(p.spread))</span><br><span class="line">p.Unlock()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">chosen := members[index].(subConnMember)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> balancer.PickResult{</span><br><span class="line">SubConn: chosen.SubConn,</span><br><span class="line">}, <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 从哈希环里选择 num 个虚拟节点返回</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *Hashring)</span></span> FindN(key []<span class="type">byte</span>, num <span class="type">uint8</span>) ([]Member, <span class="type">error</span>) {</span><br><span class="line">h.RLock()</span><br><span class="line"><span class="keyword">defer</span> h.RUnlock()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="type">int</span>(num) > <span class="built_in">len</span>(h.nodes) {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, ErrNotEnoughMembers</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">keyHash := h.hasher(key)</span><br><span class="line"> <span class="comment">// 哈希环可以展开成一个首尾相连的数组,此处用二分法查找</span></span><br><span class="line">vnodeIndex := sort.Search(<span class="built_in">len</span>(h.virtualNodes), <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>)</span></span> <span class="type">bool</span> {</span><br><span class="line"><span class="keyword">return</span> h.virtualNodes[i].hashvalue >= keyHash</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line">alreadyFoundNodeKeys := <span class="keyword">map</span>[<span class="type">string</span>]<span class="keyword">struct</span>{}{}</span><br><span class="line">foundNodes := <span class="built_in">make</span>([]Member, <span class="number">0</span>, num)</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="built_in">len</span>(h.virtualNodes) && <span class="built_in">len</span>(foundNodes) < <span class="type">int</span>(num); i++ {</span><br><span class="line"> <span class="comment">// 连续选择 num 个虚拟节点</span></span><br><span class="line">boundedIndex := (i + vnodeIndex) % <span class="built_in">len</span>(h.virtualNodes)</span><br><span class="line">candidate := h.virtualNodes[boundedIndex]</span><br><span class="line"><span class="keyword">if</span> _, ok := alreadyFoundNodeKeys[candidate.members.nodeKey]; !ok {</span><br><span class="line">foundNodes = <span class="built_in">append</span>(foundNodes, candidate.members.member)</span><br><span class="line">alreadyFoundNodeKeys[candidate.members.nodeKey] = <span class="keyword">struct</span>{}{}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> foundNodes, <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>哈希环的处理</p><figure class="highlight golang"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// 发现新的 node 时,往哈希环里添加缓存节点</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *Hashring)</span></span> Add(member Member) <span class="type">error</span> {</span><br><span class="line"> <span class="comment">// nodeKeyString 的值:ip+port</span></span><br><span class="line">nodeKeyString := member.Key()</span><br><span class="line"></span><br><span class="line">h.Lock()</span><br><span class="line"><span class="keyword">defer</span> h.Unlock()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> _, ok := h.nodes[nodeKeyString]; ok {</span><br><span class="line"><span class="comment">// already have node, bail</span></span><br><span class="line"><span class="keyword">return</span> ErrMemberAlreadyExists</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">nodeHash := h.hasher([]<span class="type">byte</span>(nodeKeyString))</span><br><span class="line"></span><br><span class="line">newNodeRecord := nodeRecord{</span><br><span class="line">nodeHash,</span><br><span class="line">nodeKeyString,</span><br><span class="line">member,</span><br><span class="line"><span class="literal">nil</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// virtualNodeBuffer是一个 10 bit 的数组,高 8 位存储了节点的哈希 key,最后 2 位存储了对应的虚拟节点索引值</span></span><br><span class="line">virtualNodeBuffer := <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="number">10</span>)</span><br><span class="line">binary.LittleEndian.PutUint64(virtualNodeBuffer, nodeHash)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i := <span class="type">uint16</span>(<span class="number">0</span>); i < h.replicationFactor; i++ {</span><br><span class="line">binary.LittleEndian.PutUint16(virtualNodeBuffer[<span class="number">8</span>:], i)</span><br><span class="line"> <span class="comment">// 组装成 10 位的数组后再计算一次哈希值</span></span><br><span class="line">virtualNodeHash := h.hasher(virtualNodeBuffer)</span><br><span class="line"></span><br><span class="line">virtualNode := virtualNode{</span><br><span class="line">virtualNodeHash,</span><br><span class="line">newNodeRecord,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">newNodeRecord.virtualNodes = <span class="built_in">append</span>(newNodeRecord.virtualNodes, virtualNode)</span><br><span class="line">h.virtualNodes = <span class="built_in">append</span>(h.virtualNodes, virtualNode)</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// 将所有的虚拟节点排序</span></span><br><span class="line">sort.Sort(h.virtualNodes)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Add the node to our map of nodes</span></span><br><span class="line">h.nodes[nodeKeyString] = newNodeRecord</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 删除服务节点对应的所有虚拟节点</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(h *Hashring)</span></span> Remove(member Member) <span class="type">error</span> {</span><br><span class="line">nodeKeyString := member.Key()</span><br><span class="line"></span><br><span class="line">h.Lock()</span><br><span class="line"><span class="keyword">defer</span> h.Unlock()</span><br><span class="line"></span><br><span class="line">foundNode, ok := h.nodes[nodeKeyString]</span><br><span class="line"><span class="keyword">if</span> !ok {</span><br><span class="line"><span class="comment">// don't have the node, bail</span></span><br><span class="line"><span class="keyword">return</span> ErrMemberNotFound</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">indexesToRemove := <span class="built_in">make</span>([]<span class="type">int</span>, <span class="number">0</span>, h.replicationFactor)</span><br><span class="line"><span class="keyword">for</span> _, vnode := <span class="keyword">range</span> foundNode.virtualNodes {</span><br><span class="line">vnode := vnode</span><br><span class="line">vnodeIndex := sort.Search(<span class="built_in">len</span>(h.virtualNodes), <span class="function"><span class="keyword">func</span><span class="params">(i <span class="type">int</span>)</span></span> <span class="type">bool</span> {</span><br><span class="line"><span class="keyword">return</span> !h.virtualNodes[i].less(vnode)</span><br><span class="line">})</span><br><span class="line"><span class="keyword">if</span> vnodeIndex >= <span class="built_in">len</span>(h.virtualNodes) {</span><br><span class="line"><span class="keyword">return</span> spiceerrors.MustBugf(<span class="string">"unable to find vnode to remove: %020d:%020d:%s"</span>, vnode.hashvalue, vnode.members.hashvalue, vnode.members.nodeKey)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">indexesToRemove = <span class="built_in">append</span>(indexesToRemove, vnodeIndex)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">sort.Slice(indexesToRemove, <span class="function"><span class="keyword">func</span><span class="params">(i, j <span class="type">int</span>)</span></span> <span class="type">bool</span> {</span><br><span class="line"><span class="comment">// <span class="doctag">NOTE:</span> this is a reverse sort!</span></span><br><span class="line"><span class="keyword">return</span> indexesToRemove[j] < indexesToRemove[i]</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">len</span>(indexesToRemove) != <span class="type">int</span>(h.replicationFactor) {</span><br><span class="line"><span class="keyword">return</span> spiceerrors.MustBugf(<span class="string">"found wrong number of vnodes to remove: %d != %d"</span>, <span class="built_in">len</span>(indexesToRemove), h.replicationFactor)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将要删除的元素都放到数组的末尾</span></span><br><span class="line"><span class="keyword">for</span> i, indexToRemove := <span class="keyword">range</span> indexesToRemove {</span><br><span class="line"><span class="comment">// Swap this index for a later one</span></span><br><span class="line">h.virtualNodes[indexToRemove] = h.virtualNodes[<span class="built_in">len</span>(h.virtualNodes)<span class="number">-1</span>-i]</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Truncate and sort the nodelist</span></span><br><span class="line">h.virtualNodes = h.virtualNodes[:<span class="built_in">len</span>(h.virtualNodes)-<span class="built_in">len</span>(indexesToRemove)]</span><br><span class="line">sort.Sort(h.virtualNodes)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Remove the node from our map</span></span><br><span class="line"><span class="built_in">delete</span>(h.nodes, nodeKeyString)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>一个权限检查请求的查找过程如下:</p><ul><li>根据要查找的对象计算哈希值</li><li>哈希值写入 gRPC 的 context 中发起 gRPC 调用</li><li>gRPC 根据该哈希值查找哈希环里 >= 该值的一个虚拟节点</li><li>从虚拟节点里找到对应的真实server node,gRPC 使用该连接对 node 发起请求</li><li>spiceDB node 接受请求后同样使用哈希值查找本身的 cache</li><li>cache 没有命中的话就查找数据库,然后将结果 set 进 cache 中(ttl 为 20s),在 cache 里的 key 就是该对象的哈希值</li></ul><p>注意:</p><ul><li>哈希环存储的是所有虚拟节点的哈希值,并不存储 cache 里的哈希值</li><li>不管 spiceDB 的 node 是增加还是减少,其实都不会迁移 cache 里的值</li><li>如果新增了一个 node,就会在哈希环里虚拟出多个虚拟节点。此时 node 的 cache 是空的,所有落到该 node 的请求都必须查询数据库后才能写入 cache,如果其他 node 已经缓存了要查询的结果也没用,只等等到 ttl 过期后淘汰。删除也同理</li></ul><h1 id="Others"><a href="#Others" class="headerlink" title="Others"></a>Others</h1><p>在 Zanzibar 中,除了使用热点缓存技术之外,还使用了其他缓存技术,包括:relationship cache(将一个热点 object 相关的全部 relation tuple 提前加载到内存中);Leopard Indexing System(用于持续更新用户权限的非结构化数据,可以支持权限集合的快速计算)。SpiceDB 目前对这两种技术的实现还停留在 <a href="https://github.com/authzed/spicedb/issues/207">proposal</a> 阶段。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><div id="refer-anchor"></div><p>[1] <a href="https://berryjam.github.io/2018/05/%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C(Consistent-hashing)%E7%AE%97%E6%B3%95/">一致性哈希(Consistent hashing)算法</a><br>[2] <a href="https://web.stanford.edu/class/cs168/l/l1.pdf">CS168: The Modern Algorithmic Toolbox Lecture #1: Introduction and Consistent Hashing</a><br>[3] <a href="https://itnext.io/introducing-consistent-hashing-9a289769052e">Introducing Consistent Hashing</a><br>[4] <a href="https://blog.csdn.net/u013536232/article/details/108556544">grpc进阶篇之resolver</a><br>[5] <a href="https://toutiao.io/posts/tazdpo4/preview">gRPC Name Resolver 原理及实践</a></p>]]></content>
<summary type="html"><h1 id="Hotspot-Caching"><a href="#Hotspot-Caching" class="headerlink" title="Hotspot Caching"></a>Hotspot Caching</h1><p>由于 2/8 原则的存在,笼统来说,</summary>
<category term="源码阅读" scheme="https://kylinlingh.github.io/categories/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="授权" scheme="https://kylinlingh.github.io/tags/%E6%8E%88%E6%9D%83/"/>
<category term="零信任" scheme="https://kylinlingh.github.io/tags/%E9%9B%B6%E4%BF%A1%E4%BB%BB/"/>
</entry>
<entry>
<title>[笔记]Zanzibar: Google’s Consistent, Global Authorization System</title>
<link href="https://kylinlingh.github.io/2023/06/19/%E7%AC%94%E8%AE%B0-Zanzibar-Google%E2%80%99s-Consistent-Global-Authorization-System/"/>
<id>https://kylinlingh.github.io/2023/06/19/%E7%AC%94%E8%AE%B0-Zanzibar-Google%E2%80%99s-Consistent-Global-Authorization-System/</id>
<published>2023-06-18T16:00:00.000Z</published>
<updated>2023-06-19T16:00:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="TL-DR"><a href="#TL-DR" class="headerlink" title="TL;DR"></a>TL;DR</h1><blockquote><p><a href="https://research.google/pubs/pub48190/">原文: Zanzibar: Google’s Consistent, Global Authorization System</a></p></blockquote><h1 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h1><p>Zanzibar 是 google 开发和部署的一个全球授权系统,用于评估全球用户对 google 数百个应用的访问权限(包括:Calendar, Cloud, Drive, Maps, Photos, YouTube等)。目前已经存储了上万亿条 ACL(access control list),每秒钟处理来自数十亿用户的数百万个请求,并且在过去的三年里做到了将 95% 分位的请求响应时延控制在 10ms以内,系统的可用性大于99.999%。本文主要是描述了 Zanzibar 系统在工程实现上遇到的挑战和解决方案。</p><p>Zanzibar 系统的几个设计目标:</p><ul><li>正确性:保证用户设置的访问控制策略能被正确实现(譬如不能将用户的私人图片开放给别人访问)</li><li>灵活性:可以同时满足用户和应用对访问策略的多种个性化需求</li><li>低时延:因为授权请求位于访问请求的干路上,因此授权请求的响应速度决定了用户的体验</li><li>高可用:当授权系统不可用时,用户的访问请求默认被拒绝,从而导致所有系统不可访问,因此高可用性再怎么强调都不为过</li><li>大规模:系统需要存储数十亿用户的数据,并且还要全球部署来保证用户就近访问</li></ul><h1 id="Model-Language-and-API"><a href="#Model-Language-and-API" class="headerlink" title="Model, Language, and API"></a>Model, Language, and API</h1><h2 id="Relation-Tuples"><a href="#Relation-Tuples" class="headerlink" title="Relation Tuples"></a>Relation Tuples</h2><p>在 Zanzibar 里,一条 ACL(又被称为 relation tuple) 其实就是描述了一个 object-user 或者 object-object 之间的关系。Zanzibar 设计了一个创新性的描述语言来描述一个 ACL(对应下图中的<tuple>) :</p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-19-17-36-29.png" width="50%" height="50%" align=left><p><em><strong>通过下图的几个例子就可以弄清楚这个描述语言的原理,注意上图中的<userset>是可以进行递归解析的,由此可以描述出一个非常复杂的权限关系,如下图划红线的语句: group:eng#member</strong></em></p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-19-17-44-29.png"></p><p>为了应对复杂的权限描述,Zanzibar 还支持权限之间的集合操作(并集,交集等)</p><h2 id="New-enemy-problem"><a href="#New-enemy-problem" class="headerlink" title="New enemy problem"></a>New enemy problem</h2><p>考虑两个场景:</p><ul><li><p>场景一:</p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-19-18-19-57.png" width="50%" height="50%" align=left>正常情况下Bob 是看不到 Charile往文件夹里添加的新文件,但如果步骤 1 和步骤 2 在写入数据库时乱序了,就可能出现这种情况:步骤 2 已经落库了,但步骤 1 还没落库,此时发生了一次数据库查询,就会导致 Bob 还能看到 Charile 添加的新文件,因为 Bob 的权限还没移除。但是只要保证两个步骤落库时一定是严格按照时间的,那么在步骤 1 落库之后的任何时间点查询数据库,Bob 都看不到新增加的文件</li><li><p>场景二:</p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-19-20-38-14.png" width="50%" height="50%" align=left>假如两个步骤严格按照时间落库了,Bob 还能看到文件里新增内容的唯一可能就是在查询权限的时候,没有查询到最新的这两个 ACL 内容(注意:有两种场景会导致这个情况:<ul><li>场景一:这两个 ACL 落库时写入到主库之后还没同步到从库,此时查询从库就会看不到这两个 ACL</li><li>场景二:ACL 已经落库了,但是在查询的时候限定错了时间范围,导致没有查询到最新的这两个 ACL</li></ul></li></ul><p>该论文并没有明确给 “new enemy problem” 下具体的定义,只是说如果没有严格遵从 ACL 更新的因果顺序或者采用了过时的 ACL 就会导致出现该问题。Zanzibar 解决 “new enemy problem” 的方法如下(<strong>其实就是做两件事:1.保证 ACL 严格按照因果关系落库;2.客户端在查询数据库时提供一个具体的时间戳,保证数据库能查询到该时间戳之前落库的 ACL</strong>):</p><ul><li>external consistency(外部一致性):</li></ul><blockquote><p>外部一致性就是事务在数据库内的执行序列不能违背外部观察到的顺序。举例来说,事务在一个节点写入一条数据,完成后立即另启一个事务在另一个节点读取,能成功读到刚刚写入的数据吗?如果没读到,可以理解成在数据库层面,后一个事务先于前一个事务运行了,这样就违背了外部所观察到的顺序<a href="#refer-anchor"><sup>[1]</sup></a>。Zanzibar 依赖于google 的 Spanner 分布式数据库来保证外部一致性,而 Spanner 则通过 TrueTime 来提供该特性,关于该原理可以参考论文:<a href="https://ying-zhang.github.io/time/2017-spanner-truetime-cap-cn/">[译]Spanner,TrueTime和CAP定理</a><br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-14-11-41-16.png"></p></blockquote><ul><li>snapshot reads with bounded staleness(受限过期的可读快照):由于外部一致性保证了 ACL 在落库时是严格遵守因果关系的,因此要读取时间戳为 T 的 ACL 快照时就一定能读取到所有在 T 之前落库的 ACL 内容。</li></ul><p>Zanzibar 在响应客户端的权限查询请求时需要指定一个数据库的快照时间戳来查询 ACL,如果是单体数据库,那么每次查询 ACL 的时候只需要使用最新一个事务的时间戳即可。但是在分布式数据库的场景下,由于数据库节点之间存在数据不一致的问题,如果每次查询都使用最新事务的时间戳,就会导致数据库节点之间为了同步数据而进行大量的跨区域通信,从而导致高时延。Zanzibar 为了避免这种情况,设计了以下协议:</p><ul><li>当客户端要更新 ACL 时,Zanzibar 会触发一次 content-change ACL check,然后为这次变更生成一个 token(命名为 zookie),该 token 内置编码了该事务插入数据库时的时间戳,并且在数据库里,ACL 的变更内容会和该 token 一起写入数据库(注:为什么 zookie 不直接使用明文时间戳?这是为了阻止客户端在查询 ACL 的时候传入任意的时间戳,而不是真正有效的时间戳)</li><li>客户端在后续调用 ACL check API 时需要传入该 token,以此来保证数据库一定能查询到该 token 对应的时间戳数据;如果没有传入该 token,Zanzibar 就会根据本数据库节点已有的数据来查询,换句话说,假如当前的数据库节点还没来得及同步最新的更新数据,并且客户端也没有传入 token,就会导致 Zanzibar 返回错误的结果。</li></ul><p>总的来说:外部一致性能保证 ACL 落库时是严格按照用户的变更顺序进行的,因为任何的乱序都会导致权限系统返回错误的结果。而 zookie 则是为了提高系统响应速度而设计的,在分布式数据库里,不同数据中心之间的数据库并不是强一致的,当某个查询请求到达任意一个数据中心的数据库时,最快的响应方法就是基于该数据库已有的数据直接查询返回,而不是等待整个数据库进行一次全球同步后再查询返回。换个角度来理解,zookie 的用途就是为了检查某个数据库是否能够直接查询返回,如果 zookie 编码的数据在该数据库上不存在,此时在进行一次数据库同步即可,相反,如果不存在再进行同步。</p><h2 id="Namespace-Configuration"><a href="#Namespace-Configuration" class="headerlink" title="Namespace Configuration"></a>Namespace Configuration</h2><p>在 Zanzibar 里,namespace 是对权限的抽象描述,如下所示:</p><ul><li>所有的 owner 都是 editor,所有的 editor 都是 viewer,对应到中文语意里就是该文档的所有者都拥有编辑权限,拥有编辑权限的用户同时都拥有阅读权限。</li><li>对该文档所在文件夹具有阅读权限的用户同时具有阅读本文档的权限</li><li>其中,union 表示各种权限之间的并集关系</li></ul><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-26-11-14-22.png" width="50%" height="50%" align=left><p>另外,namepsace 里其实还存储了一些重要参数,譬如本 namespace 对应的 ACL 要保存多久,以便垃圾回收进程定时清空过期的 ACL 数据。</p><h2 id="API"><a href="#API" class="headerlink" title="API"></a>API</h2><p>Zanzibar 的所有 API 都使用 gRPC 封装后暴露出来,主要包括了 Read,Write,Check,Watch,Expand 这五个 API,具体的作用看论文,这里不赘述</p><h1 id="Architecture-and-Implementation"><a href="#Architecture-and-Implementation" class="headerlink" title="Architecture and Implementation"></a>Architecture and Implementation</h1><p>Zanzibar 的系统架构如下所示:</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-21-09-59-32.png"></p><ul><li>aclserver:接收请求(Check, Read, Expand, Write),计算结果后返回,注意:aclserver 在处理请求的时候可能会并发出大量的请求到其他服务器上</li><li>watchserver:专门用于响应 Watch 的 API 请求,实时跟踪 changelog 以便更新缓存里的数据,避免每次处理请求时都要加载数据库。但是不同节点在读取 changlog 的时候可能因为网络时延导致数据不一致,此时就需要有一个监控进程来跟踪所有节点都生效的数据并将该数据广播给全局节点</li><li>periodic offline pipeline:定期备份数据和回收一些过期的 ACL</li><li>Leopard:一个索引系统,专门给 watchserver 发送 Watch 请求来实时获取某些 namespace 的变更事件并生成索引,用于加速搜索一些涉及到权限集合的操作(并集,交集等)</li></ul><h2 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h2><ul><li>Namespace Config Storage(namespace 配置):用两个表来保存,一个保存具体的 namespace 定义,另一个表保存 namespace 的变更日志(使用时间戳来索引)。保存变更日志是为了持续监控该表,当发现数据更新时就刷新内存</li><li>Relation Tuple Storage(权限元组):namespace 定义了抽象的权限描述,relation tuple就是定义了具体的权限描述。举例来说,namespace 定义了 “所有的 owner 都是 editor,所有的 editor 都是 viewer”,那么 relation tuple 就是定义“用户 Alice 是文档的 owner,用户 Bob 是文档的 viewer”。所以 relation tuple 是用于指定具体的 user-object 或者 object-object 之间的关系,在数据库中,每条 tuple 对应一行记录,并由多列主键索引(shard ID, object ID, relation, user, commit timestamp).其中 shard ID 的生成规则由 namespace 来指定,一般与 ObjectID 相同。</li><li>Changelog(变更日志):当 relation tuple 更新了内容后就会同步在 changelog 中写入新的记录(二者的操作在同一个事务中),专门用于响应Leopard 发出的 Watch 请求</li><li>Replication(副本):每一个数据中心都保存了全部 ACL 数据的副本</li></ul><h1 id="Serving"><a href="#Serving" class="headerlink" title="Serving"></a>Serving</h1><h2 id="Evaluation-Timestamp"><a href="#Evaluation-Timestamp" class="headerlink" title="Evaluation Timestamp"></a>Evaluation Timestamp</h2><p>前面已经提到过客户端如果没有传入 token(编码的时间戳) 时,Zanzibar 为了性能考虑,会尽量避免数据库之间进行跨区域通信来同步数据。此时 Zanzibar 就会猜测一个时间戳来查询数据库,然后根据查询的结果和复杂的统计方式来动态调整猜测的时间戳(统计方式可以查看原文,此处不再描述)</p><h2 id="Config-Consistency"><a href="#Config-Consistency" class="headerlink" title="Config Consistency"></a>Config Consistency</h2><p>如果某个 namespace 的配置文件发生了变更,由于网络延迟的原因会导致不同的 aclserver 加载到了不同版本(此处的版本其实指的是namespace 落库时的时间戳)的 namespace 内容,此时就会导致服务出错。Zanzibar 会使用一个监控任务来收集所有 aclserver 都可以使用的 namespace 版本号,当请求到达 acvlserver 后,aclserver 会从这些版本号中选择一个使用。这样做的好处是即使 aclserver 不能读取数据库,也能保证整个集群可以继续运行。</p><h2 id="Check-Evaluation"><a href="#Check-Evaluation" class="headerlink" title="Check Evaluation"></a>Check Evaluation</h2><p>Zanzibar 会将一个权限检查的请求转化为一个布尔表达式求值问题,譬如要检查用户 U 是否对某个 object 拥有 relation 关系可以转化为表达式:</p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-06-26-15-44-58.png" width="50%" height="50%" align=left><p>其中,U’ 表示的是深层嵌套后需要递归查询的 user,举个例子,某个文件放在一个文件夹下面,拥有该文件夹编辑权限的用户也可以编辑本文件。数据库里有一条明确的 ACL 说 Bob 拥有该文件的编辑权限,此时要查询 Alice 是否也拥有该文件的编辑权限。那么在数据库里是查询不到一条具体 ACL 说 Alice 拥有文件的编辑权限,此时就需要递归查询拥有文件夹编辑权限的全部用户里是否有 Alice 这个用户。如果该文件夹又嵌套在另一个文件夹里,就会引发很深层次的递归查询。Zanzibar 会并发查询这些表达式,如果某个表达式得到了结果,就可以取消还没查询完成的其他结果,从而加快查询的速度。同时,Zanzibar 也会将这些请求整合成一个批量查询来减轻后端数据库的压力。</p><h2 id="Leopard-Indexing-System"><a href="#Leopard-Indexing-System" class="headerlink" title="Leopard Indexing System"></a>Leopard Indexing System</h2><p>Zanzibar 为了加速查询上面所述的权限深度嵌套问题,引入了 Leopard 索引系统,该索引系统的原理可以查看原文</p><h2 id="Handling-Hot-Spots"><a href="#Handling-Hot-Spots" class="headerlink" title="Handling Hot Spots"></a>Handling Hot Spots</h2><p>热点数据的处理对 Zanzibar 来说非常重要,因为 2/8 原则的存在,20%的对象占据了 80%的查询请求。Zanzibar 的一个 server 集群会组成一个分布式缓存,然后使用一致性哈希算法<a href="#refer-anchor"><sup>[2]</sup></a>来实现请求的负载均衡。</p><ul><li>当权限查询请求到达后,server 会根据要查询的 ObjectID 计算出一个 forwarding key,然后根据该 forwarding key 和一致性哈希算法将请求路由到不同的 server 上</li><li>每个缓存的条目都有对应的时间戳(毫秒级)</li><li>在每个 server 上维持一个 lock table 来跟踪热点对象,当多个请求要访问同一个 cache key 时,实际上只有一个请求得到执行,其他的请求只需要等待返回即可。</li><li>如果探测到了热点对象(统计读取该对象的请求数量),就会直接加载该对象相关的所有 tuple 到内存中,避免查询数据库</li><li>如果有多个请求在访问 lock table 里的同一个对象,那么已经在执行的请求就不会被取消,即使该请求可能因为关联请求已经得到响应而无需继续执行。这样做的原因是为了将请求的结果缓存起来,以供被 lock table 阻塞的请求得到结果,而无需再重新发起一次新的调用</li></ul><h2 id="Performance-Isolation"><a href="#Performance-Isolation" class="headerlink" title="Performance Isolation"></a>Performance Isolation</h2><p>论文里的这部分准确来说是资源隔离,目的是为了在分布式系统中进行故障容错</p><ul><li>在 server 上计算每个 RCP 请求所消耗的 CPU 时钟,当server 的 CPU 使用率/内存超过阈值之后会对该 server 的请求进行限流</li><li>限制同一个客户端或请求对 Spanner 数据库并发查询的最大数量,避免 Spanner 被某个请求或客户端完全独占</li></ul><h2 id="Tail-Latency-Mitigation"><a href="#Tail-Latency-Mitigation" class="headerlink" title="Tail Latency Mitigation"></a>Tail Latency Mitigation</h2><p>为了加速处理慢任务,Zanzibar 使用请求对冲<a href="#refer-anchor"><sup>[3]</sup></a>的方法,原理如下:</p><ul><li>实时评估一个请求的响应时间,如果超过阈值就会被认为是一个慢任务,此时就会发出第二个一模一样的请求,当某一个请求得到响应后就取消掉另一个请求</li><li>每一个 server 会根据所有请求的响应时间来动态计算慢任务的响应时间阈值,以便控制请求对冲导致的无效流量比例</li><li>请求对冲时,不同的请求一定是针对存放了副本的其他server 集群,因为在同一个 server 集群里反复发起请求只会导致执行更多的慢任务,使得性能更差</li></ul><h1 id="Experience"><a href="#Experience" class="headerlink" title="Experience"></a>Experience</h1><p>这部分的内容用几个指标(存储,QPS,响应时延,可用性等)描述了 Zanzibar 系统在过去 5 年里的运行情况,更详细的信息请阅读原文</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><div id="refer-anchor"></div><p>[1] <a href="https://disksing.com/external-consistency/">数据库的外部一致性</a><br>[2] <a href="https://berryjam.github.io/2018/05/%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C(Consistent-hashing)%E7%AE%97%E6%B3%95/">一致性哈希(Consistent hashing)算法</a><br>[3] <a href="https://fuzhe1989.github.io/2020/01/02/the-tail-at-scale/">[笔记] The Tail at Scale</a></p>]]></content>
<summary type="html"><h1 id="TL-DR"><a href="#TL-DR" class="headerlink" title="TL;DR"></a>TL;DR</h1><blockquote>
<p><a href="https://research.google/pubs/pub4819</summary>
<category term="论文笔记" scheme="https://kylinlingh.github.io/categories/%E8%AE%BA%E6%96%87%E7%AC%94%E8%AE%B0/"/>
<category term="权限系统" scheme="https://kylinlingh.github.io/tags/%E6%9D%83%E9%99%90%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>2023年计划与总结</title>
<link href="https://kylinlingh.github.io/2023/01/31/2023%E5%B9%B4%E8%AE%A1%E5%88%92%E4%B8%8E%E6%80%BB%E7%BB%93/"/>
<id>https://kylinlingh.github.io/2023/01/31/2023%E5%B9%B4%E8%AE%A1%E5%88%92%E4%B8%8E%E6%80%BB%E7%BB%93/</id>
<published>2023-01-30T16:00:00.000Z</published>
<updated>2023-09-15T03:43:10.228Z</updated>
<content type="html"><![CDATA[<h1 id="开源项目阅读计划"><a href="#开源项目阅读计划" class="headerlink" title="开源项目阅读计划"></a>开源项目阅读计划</h1><ul><li><a href="https://github.com/DerekYRC/mini-spring">mini-spring</a>:2022年已经读完</li></ul><blockquote><p>mini-spring是简化版的spring框架,能帮助你快速熟悉spring源码和掌握spring的核心原理。抽取了spring的核心逻辑,代码极度简化,保留spring的核心功能,如IoC和AOP、资源加载器、事件监听器、类型转换、容器扩展点、bean生命周期和作用域、应用上下文等核心功能。</p></blockquote><p>阅读感想:项目写得非常好,由浅至深地描述了Spring框架的核心原理,启发非常大。</p><ul><li><a href="https://github.com/marmotedu/iam">marmotedu/iam</a>: 2023.3 - 2023.4(已完成)<blockquote><p>企业级的 Go 语言实战项目:认证和授权系统(带配套课程)</p></blockquote></li></ul><blockquote><p>个人评价:质量非常高的go项目,可以抽象出来做项目模板,以后直接用在生产项目上,并且在该项目里还引发了很多对缓存一致性问题的思考</p></blockquote><ul><li><a href="https://github.com/ory/keto">ory/keto</a>:2023.4 - 2023.5(已完成)</li></ul><blockquote><p>Open Source (Go) implementation of “Zanzibar: Google’s Consistent, Global Authorization System”. Ships gRPC, REST APIs, newSQL, and an easy and granular permission language. Supports ACL, RBAC, and other access models.</p></blockquote><blockquote><p>个人评价:该项目是google一篇论文的开源实现,用于构建一个全局IAM,但是业界用得更多的是Casbin这个开源IAM项目,并且该项目非常不完善,很多论文里的细节并没有实现,后续发现了更好的项目:authzed/spicedb</p></blockquote><ul><li><a href="https://github.com/authzed/spicedb">authzed/spicedb</a>:Open Source, Google Zanzibar-inspired fine-grained permissions database:2023.8 完成70%(2023.6 - )</li></ul><blockquote><p> 个人评价:质量非常高的项目,尤其是高并发的设计,通过哈希环将要缓存的内容分布到不同的机器上,然后通过调度器先计算请求的哈希值,接着根据哈希值将不同的请求调度到不同的机器(该机器会缓存要请求的资源)上,从而实现了分布式缓存。并且没有引入 redis 或 dcache 等组件,可以减少对上下游的依赖。另一方面,对权限 schema 的描述也非常牛逼,可以用非常复杂的语言(权限的交集,并集等)去描述一个权限。另外,也是通过该项目,才学习到了很多 k8s 的基础知识。本人觉得这个项目是 IAM 系统的不二之选</p></blockquote><ul><li><a href="https://github.com/go-kiss/sniper">go-kiss/sniper</a>:轻量级 go 业务框架:2023.9 完成 50%(2023.9 - )</li></ul><blockquote><p>个人评价:这个项目是通过一个<a href="https://taoshu.in/go/sniper.html">个人博客站点</a>发现的,由 bilibili 的涛叔开发实现,看完之后大呼过瘾,以前看别的开源项目过程中积累的一些疑问也得到了解答(譬如为什么要慎用 context),我希望能按照这个为模板,构思一个基于 grpc stream 的轻量级框架,把上面 spicedb 里的分布式缓存和请求调度融入进去,从而提供一个流式的请求框架</p></blockquote><ul><li><a href="https://github.com/spring-projects/spring-security">Spring security</a>:计划2023上半年完成(暂时作废)</li></ul><blockquote><p>Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.</p></blockquote><blockquote><p>Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements</p></blockquote><p><a href="https://github.com/dgraph-io/ristretto/tree/master">dgraph-io/ristretto</a>:计划于2023年下半年开展(暂时作废)</p><blockquote><p>A high performance memory-bound Go cache</p></blockquote><blockquote><p>个人评价:高性能缓存的实现,是学习高并发的绝好资料,在其中需要研读很多论文和资料,可以将整个缓存体系的知识都补完</p></blockquote><p>辅助资料:</p><ul><li><a href="https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Spring%20Security%20%E8%AF%A6%E8%A7%A3%E4%B8%8E%E5%AE%9E%E6%93%8D">Spring Security详解与实操</a></li></ul><h1 id="阅读计划"><a href="#阅读计划" class="headerlink" title="阅读计划"></a>阅读计划</h1><p>非技术类:</p><ul><li>《置身事内:中国政府与经济发展》</li><li><del>《财报就像一本故事书》</del></li><li>《贝佐斯致股东的信》</li><li>《巴菲特致股东的信》</li><li>《心里突破:审判中的心理学原理与方法》</li><li>《货币金融学》</li><li>《货币战争》</li><li><del>《当下的力量》</del></li><li><del>《反脆弱:从不确定性中获益》</del></li><li>《系统之美:决策者的系统思考》</li><li>《格鲁夫给经理人的第一课》</li><li>《平衡积分卡实践战略》</li><li><del>《一本书读懂财报》</del></li><li><del>《一往无前》</del></li></ul><p>技术类:</p><ul><li><del>《敏捷应用程序安全》</del></li><li>《Google系统架构解密:构建安全可靠的系统》:</li><li><del>《决战618探秘京东技术取胜之道》</del></li><li><del>《尽在双11——阿里巴巴技术演进与超越》</del></li><li>《Designing data-intensive applications》</li><li>《TCP/IP 详解 卷一》</li></ul><p>极客时间:</p><ul><li><del>《Kubernetes 入门实践课》</del></li></ul><p>好玩的游戏:</p><ul><li>《RimWorld 环世界》:这破游戏,我能玩几年</li></ul>]]></content>
<summary type="html"><h1 id="开源项目阅读计划"><a href="#开源项目阅读计划" class="headerlink" title="开源项目阅读计划"></a>开源项目阅读计划</h1><ul>
<li><a href="https://github.com/DerekYRC/min</summary>
<category term="个人感悟" scheme="https://kylinlingh.github.io/categories/%E4%B8%AA%E4%BA%BA%E6%84%9F%E6%82%9F/"/>
<category term="个人感悟" scheme="https://kylinlingh.github.io/tags/%E4%B8%AA%E4%BA%BA%E6%84%9F%E6%82%9F/"/>
</entry>
<entry>
<title>企业账号安全体系建设之双因素认证系统设计</title>
<link href="https://kylinlingh.github.io/2023/01/17/%E4%BC%81%E4%B8%9A%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8%E4%BD%93%E7%B3%BB%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%8F%8C%E5%9B%A0%E7%B4%A0%E8%AE%A4%E8%AF%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
<id>https://kylinlingh.github.io/2023/01/17/%E4%BC%81%E4%B8%9A%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8%E4%BD%93%E7%B3%BB%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%8F%8C%E5%9B%A0%E7%B4%A0%E8%AE%A4%E8%AF%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</id>
<published>2023-01-17T12:49:05.000Z</published>
<updated>2023-02-07T14:34:46.599Z</updated>
<content type="html"><![CDATA[<h1 id="双因素身份认证-2FA"><a href="#双因素身份认证-2FA" class="headerlink" title="双因素身份认证(2FA)"></a>双因素身份认证(2FA)</h1><h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><p>所谓的双因素,就是在系统登录的时候除了证明你是谁之外(输入你的账号密码),还需要证明你拥有什么(输入验证码)。双因素认证能极大地提高用户的账号安全水平,因为在今天数据泄漏事件层出不穷的情况下,攻击者窃取用户的账号密码是比较容易的,但是攻击者要同时窃取用户的登录设备(一般是手机)就非常困难了。</p><p>这个验证码被称为 OTP(One-Time Password,一次性密码),是通过在客户端和服务端共享密钥来实现的,有两个实现方法:</p><ol><li>HOTP (HMAC-Based One-Time Password Algorithm),是基于 HMAC 算法生成的一次性密码,也称<strong>事件同步</strong>的动态密码。实现规范是 <a href="https://www.rfc-editor.org/rfc/rfc4226">RFC 4226</a> ,对应的计算公式:</li></ol><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))</span><br><span class="line">K:客户端和服务器事先协商好的一个密钥K</span><br><span class="line">C:客户端和服务器各有一个事件计数器C,并且事先将计数值同步</span><br><span class="line">Truncate:将HMAC-SHA-1产生的20字节的字符串转换为若干位十进制数字的算法</span><br></pre></td></tr></table></figure><ol start="2"><li>TOTP (Time-Based One-Time Password Algorithm),是 OTP 的算法变种,使用当前时间替换掉 HOTP 的事件计数器 C,也称<strong>时间同步</strong>的动态密码。实现规范是 <a href="https://www.rfc-editor.org/rfc/rfc6238">RFC 6238</a> (<strong>注意:该RFC包含了服务端的源码,需要仔细研究</strong>),对应的计算公式:</li></ol><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">TOTP = Truncate(HMAC-SHA-1(K,T))</span><br><span class="line"></span><br><span class="line">T = (当前的Unix格式时间戳 - T0) / X</span><br><span class="line">T0 是初试时间,默认为 0</span><br><span class="line">X 是时间步长,默认30秒</span><br><span class="line">官方文档中的例子:假设当前unix时间=59,T0=0,X=30,则T=1;假设当前unix时间=60,T0=0,X=30,则T=2</span><br><span class="line">也就是对T的值向下取整</span><br></pre></td></tr></table></figure><p>只要保证客户端与服务端的时间一致,TOTP 就能根据同一个密钥种子分别在两端计算出同一个验证码,所以生成验证码的时候是完全不依赖网络的。针对 TOTP 的攻击只有针对共享密钥的暴力穷举法,因此防护重点在于共享密钥<a href="#refer-anchor"><sup>[1]</sup></a>:</p><ul><li>密钥生成算法必须采用真随机数或者是用加密强度高的伪随机数生成算法</li><li>客户端与服务端沟通密钥时必须使用安全的传输方法,譬如 IPsec 隧道或是基于 SSL/TLS 的通信协议</li><li>密钥需要加密落盘,只在验证随机码的时候短暂解密,然后再重新加密罗盘,缩短在内存中暴露的时间</li><li>密钥需要存储在安全的地方并限制只有验证系统可以访问</li></ul><h2 id="产品"><a href="#产品" class="headerlink" title="产品"></a>产品</h2><p>TOTP 的生成算法/源码是公开的,所以并不存在服务端产品,下面的两个产品指的是用于扫描二维码的移动端应用,都支持 android 和 ios。</p><ul><li><a href="https://www.microsoft.com/zh-cn/security/mobile-authenticator-app">微软</a></li><li><a href="">谷歌</a>,在github上开源了客户端的源码:<a href="https://github.com/google/google-authenticator-android">google-authenticator-android</a></li></ul><h2 id="系统场景设计"><a href="#系统场景设计" class="headerlink" title="系统场景设计"></a>系统场景设计</h2><p>场景一:用户在登录系统时,浏览器弹出二维码,用户扫码后再输入验证码:<a href="#refer-anchor"><sup>[2]</sup></a></p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-13-10-56-39.png"></p><p>场景二:用户在登录系统时,直接打开企业微信或其他内部开发的移动端应用,该应用上自动生成验证码,用户输入该验证码即可</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-13-11-17-10.png"></p><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><div id="refer-anchor"></div><p>[1] <a href="https://www.rfc-editor.org/rfc/rfc6238">TOTP: Time-Based One-Time Password Algorithm</a><br>[2] <a href="https://juejin.cn/post/7142687165095346207">nodejs 接入 google 身份验证器(authenticator)</a></p>]]></content>
<summary type="html"><h1 id="双因素身份认证-2FA"><a href="#双因素身份认证-2FA" class="headerlink" title="双因素身份认证(2FA)"></a>双因素身份认证(2FA)</h1><h2 id="概念"><a href="#概念" class="he</summary>
<category term="安全建设思路" scheme="https://kylinlingh.github.io/categories/%E5%AE%89%E5%85%A8%E5%BB%BA%E8%AE%BE%E6%80%9D%E8%B7%AF/"/>
<category term="双因素认证" scheme="https://kylinlingh.github.io/tags/%E5%8F%8C%E5%9B%A0%E7%B4%A0%E8%AE%A4%E8%AF%81/"/>
<category term="账号安全" scheme="https://kylinlingh.github.io/tags/%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8/"/>
</entry>
<entry>
<title>企业账号安全体系建设之单点登录系统设计</title>
<link href="https://kylinlingh.github.io/2022/10/17/%E4%BC%81%E4%B8%9A%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8%E4%BD%93%E7%B3%BB%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
<id>https://kylinlingh.github.io/2022/10/17/%E4%BC%81%E4%B8%9A%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8%E4%BD%93%E7%B3%BB%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</id>
<published>2022-10-17T04:49:05.000Z</published>
<updated>2023-06-27T07:58:25.528Z</updated>
<content type="html"><![CDATA[<h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>单点登录(SSO,Single Sign On)是一种身份验证解决方案,可让用户通过一次性用户身份验证登录多个相互信任的应用程序和网站。</p><p>SSO能解决的问题:</p><ul><li>提高用户体验:用户不必在每个系统中都进行注册、登录</li><li>减轻开发人员的负担:开发人员不需要为每个系统都设计一个单独的账号和登录系统,并且很难几乎不可能要求所有开发人员保证这些系统都达到合格的安全水位以上</li><li>改善安全状况:通过收敛用户的登录入口,可以审计用户的登录行为</li></ul><p>SSO有以下几种协议来对用户进行身份验证:</p><ul><li>SAML(Security Assertion Markup Language, 安全断言标记语言):SAML 使用 XML 来交换用户标识数据,基于 SAML 的 SSO 服务提供更好的安全性和灵活性,因为应用程序不需要在其系统上存储用户凭证。因为该协议不在本文介绍的重点之内,详情参考<a href="#refer-anchor"><sup>[7]</sup></a>。</li><li>OIDC(OpenID Connect,):是使用一组用户凭证访问多个站点的方法,譬如使用微信账号登录其他网站的场景,就是这种开放标准的应用。</li><li>Kerberos:一种基于票证的身份验证系统,可让两方或多方在网络上相互验证其身份。它使用安全密码学来防止未经授权访问在服务器、客户端和密钥分发中心之间传输的标识信息。<strong>所谓的票证,可以理解为多个请求体参数,这些参数主要包括客户端的Name,IP,需要访问的网络服务的地址Server IP,ST的有效时间,时间戳以及用于客户端和服务端之间通信的Session Key(只在一次Session会话中起作用,即使密钥被劫持,等到密钥被破解可能这次会话都早已结束)</strong></li></ul><h1 id="Kerberos协议"><a href="#Kerberos协议" class="headerlink" title="Kerberos协议"></a>Kerberos协议</h1><p>感慨于kerberos协议的精妙,所以研究了一下整个认证过程,概括如下<a href="#refer-anchor"><sup>[2]</sup></a></p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-06-15-23-43.png" alt="kerberos协议认证过程"></p><ol><li>认证服务器首先在数据库里存储客户端和服务端的身份信息(譬如ip,用户名)和双方的密钥(该密钥通过密码生成)</li><li>客户端首先与认证服务器连接,主要是验证客户端是否已经在数据库里(用户名和ip),认证通过后返回一个被加密的<strong>票据授予票据</strong>(主要包含了客户端的Name,IP,当前时间戳等),客户端用自己的密钥解密部份票据内容后会根据时间戳判断该时间戳与自己发送请求时的时间之间的差值是否大于5分钟,如果大于五分钟则认为该认证服务器是伪造的,认证至此失败;另一部份票据内容是用票据授予服务器的密钥加密的,里面保存了客户端的身份信息,客户端无法解密。</li><li>客户端一次性将三种内容发送给<strong>票据授予服务器</strong>,包括:服务端IP,自身的身份信息,第2步里无法解密的部份票据授予票据。票据授予服务器首先验证服务端IP是否在数据库内,再判断时延,最后票据授予服务器用自己的密钥解开票据授予票据获取到认证服务器封装好的客户端信息,将该信息与客户端发送过来的自身身份信息比较。一致后就认为通过了验证并返回用服务端密钥加密的票证。</li><li>客户端用服务端的密钥加密自身的身份信息,与第三步获得的票据一起发送给服务端。服务端解密后验证身份信息是否与票据里的身份信息一致,然后将接受请求的响应加密后返回。客户端解密后会验证服务端身份,至此双方完成了身份验证。</li></ol><p>总结,核心步骤:</p><ol><li>验证client与server的ip是否在数据库中</li><li>验证时间戳是否超出时延</li><li>AS用TGS的密钥加密client的身份信息,让TGS解密后验证是否一致</li><li>TGS用server的密钥加密client的身份信息,让server解密后验证是否一致</li></ol><h1 id="OIDC协议"><a href="#OIDC协议" class="headerlink" title="OIDC协议"></a>OIDC协议</h1><p>OIDC 是基于 OAuth 2.0 构建的身份认证框架协议,但是 OIDC 与 OAuth2.0 有概念的区别:</p><ul><li>OAuth2.0 是一种授权协议,主要<strong>用于资源授权(只做一件事)</strong></li><li>OIDC 是 OAuth 2.0 协议的超集,能够<strong>认证用户并完成资源授权(做了两件事)</strong></li></ul><p>在介绍OIDC之前,先介绍OAuth协议,OAuth 1.0 现在已经是废弃状态,不需要研究。OAuth 2.0 的授权流程如下:</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-06-16-13-53.png" alt="OAuth2.0 授权流程"></p><ol><li>第三方应用要求用户登录,用户选择了使用微信帐号登录</li><li>第三方应用拉起微信APP,微信APP会弹出一个登录确认框</li><li>用户点击确认按钮后微信会给微信服务平台发送一个请求</li><li>微信服务平台给微信返回一个授权码,然后微信重新拉起第三方应用并附带授权码</li><li>第三方应用APP会将授权码传递给服务端,由服务端向微信的服务平台发起请求,并附带上授权码,app_id和app_secret(这两者都是第三方应用在微信服务平台上注册时生成的)</li><li>微信服务平台返回一个access_token,如果需要,还会返回 refresh_token。</li><li>第三方应用使用这个access_token访问微信服务平台,微信服务平台验证了access_token之后就返回用户的微信帐号信息</li></ol><p>可以看到,整个过程中唯一可能让用户输入帐号密码的地方就是第二步拉起微信APP时,如果你此时在微信退出了登录就会让你输入账号密码来认证,但这个认证过程不在 OAuth2.0 协议的流程里,因此 OAuth2.0 并不是身份认证协议。</p><p>问题1: 为什么第四步要引入授权码,没有授权码环节可以吗?<br>如果没有了授权码,那么第四步就变成了微信服务平台直接给第三方应用服务端返回 access_token,<strong>注意,不能将 access_token 返回给第三方应用APP的客户端,这样会导致 access_token 存在失窃的安全风险</strong>,然后第三方应用服务端就直接拿着 access_token 去拿数据。此时,原本第四步里微信会重新拉起第三方应用的这个步骤就消失了,意味着第三方应用实际上已经登录完成了,但你仍然停留在微信的界面上并且没人通知你登录成功,想想就知道这个用户体验就很差。所以授权码的作用就是为了<strong>重新建立起第三方应用的一次连接,但又不能让访问令牌暴露出去</strong>。授权码就是一个临时的、间接的凭证,并且可以直接返回给第三方应用APP,甚至直接暴露在网络上也没问题,而 access_token 是安全保密性要求极高的令牌,只能在服务端之间用https传输。</p><p>问题2: 如果窃取了授权码,是否也可以获取到 access_token?<br>需要同步窃取到第三方应用的app_id和app_secret,并且在授权码尚未过期的时间内可以。</p><p>问题3: 不使用授权码,在第四步让微信重新拉起第三方应用并用https传递 access_token 是否可以?<br>https是用来保证传输安全的,access_token 到达了第三方应用APP端后会遭遇怎样的风险是未知的,可能移动端本身已经被hack了,黑客已经拿到了https的证书,此时就可以直接解开并获得 access_token 了。所以传输过程安全了,并不代表存储就是安全的。</p><hr><p>看完了 OAuth2.0 协议之后,再看 OIDC 协议的流程:</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-06-18-24-06.png" alt="OIDC协议工作时许图"></p><p>可以看到,用 OAuth 2.0 实现 OIDC 的最关键的方法是在原有 OAuth 2.0 流程的基础上增加 ID 令牌(id_token)和 UserInfo 端点(可以理解为认证平台的一个接口,用于给第三方应用获取用户信息,譬如: /oauth2/userInfo)。id_token是一个jwt格式的令牌,得益于jwt的自包含性,紧凑性以及防篡改机制,使得 id_token 可以安全的传递给第三方客户端程序并且容易被验证。第三方软件可以通过解析 id_token 获取关键用户标识信息(其实就是user_id,如果第三方系统不需要获取更详细的用户信息,就不需要房屋UserInfo端点)来记录用户状态,然后通过 Userinfo 端点来获取更详细的用户信息。<strong>有了用户态和用户信息,也就理所当然地实现了一个身份认证</strong>。</p><blockquote><p>ID Token的主要构成部分如下(使用OAuth2流程的OIDC)<a href="#refer-anchor"><sup>[4]</sup></a>。</p><ol><li>iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是一个https的url(不包含querystring和fragment部分)。</li><li>sub = Subject Identifier:必须。iss提供的EU的标识,在iss范围内唯一。它会被RP用来标识唯一的用户。最长为255个ASCII个字符。</li><li>aud = Audience(s):必须。标识ID Token的受众。必须包含OAuth2的client_id</li><li>exp = Expiration time:必须。过期时间,超过此时间的ID Token会作废不再被验证通过。</li><li>iat = Issued At Time:必须。JWT的构建的时间。</li><li>auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送AuthN请求的时候携带max_age的参数,则此Claim是必须的。</li><li>nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID Token和RP本身的Session信息。</li><li>acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。</li><li>amr = Authentication Methods References:可选。表示一组认证方法。</li><li>azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。</li></ol></blockquote><p>典型的例子:</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> "iss": "https://server.example.com",</span><br><span class="line"> "sub": "24400320",</span><br><span class="line"> "aud": "s6BhdRkqt3",</span><br><span class="line"> "nonce": "n-0S6_WzA2Mj",</span><br><span class="line"> "exp": 1311281970,</span><br><span class="line"> "iat": 1311280970,</span><br><span class="line"> "auth_time": 1311280969,</span><br><span class="line"> "acr": "urn:mace:incommon:iap:silver"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>OIDC的AuthN请求中scope参数必须要有一个值为的openid的参数(后面会详细介绍AuthN请求所需的参数),用来区分这是一个OIDC的Authentication请求,而不是OAuth2的Authorization请求。</p><h1 id="基于-OIDC-协议实现的-SSO-系统"><a href="#基于-OIDC-协议实现的-SSO-系统" class="headerlink" title="基于 OIDC 协议实现的 SSO 系统"></a>基于 OIDC 协议实现的 SSO 系统</h1><p>假设一个第三方软件有三个子应用,对应的域名分别是 a1.com、a2.com、a3.com,现在要设计一个SSO系统让用户登录了 a1.com 之后也能顺利登录其他两个域名,这就是SSO(单点登录:一次登录,畅通所有)</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-09-14-35-54.png" alt="基于OIDC实现的SSO"></p><p>当用户在 a1.com 登录成功后,OP会为该用户创建 session,用户在登录a2.com或a3.com时会在 cookie 中携带 session_id,从而避免了反复输入密码。</p><p>Q1:用 access_token 也可以拿到用户信息,为什么还要引入 id_token ?<a href="#refer-anchor"><sup>[1]</sup></a><br>access_token 永远不能被任何第三方软件去解析,就是一个令牌,用来后续请求受保护资源;而 id_token 是可以直接被第三方软件解析的。而且,这两种令牌还具有不同的生命周期,id_token 通常会很快过期,而 access_token 可以在用户离开后的很长时间内用于获取受保护资源。</p><h1 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h1><div id="refer-anchor"></div><p>[1] <a href="https://zq99299.github.io/note-book/oath2/">OAuth 2.0 实战</a><br>[2] <a href="https://seevae.github.io/2020/09/12/%E8%AF%A6%E8%A7%A3kerberos%E8%AE%A4%E8%AF%81%E6%B5%81%E7%A8%8B/">详解kerberos认证原理</a><br>[3] <a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow">Microsoft identity platform and OAuth 2.0 authorization code flow</a><br>[4] <a href="https://www.cnblogs.com/linianhui/p/openid-connect-core.html">[认证 & 授权] 4. OIDC(OpenId Connect)身份认证(核心部分)</a><br>[5] <a href="https://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0 incorporating errata set 1</a><br>[6] <a href="https://developers.google.com/identity/openid-connect/openid-connect#java">OpenID Connect - Google</a><br>[7] <a href="https://help.aliyun.com/document_detail/174224.html?utm_content=g_1000230851&spm=5176.20966629.toubu.3.f2991ddcpxxvD1#CWelL">SAML</a></p>]]></content>
<summary type="html"><h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>单点登录(SSO,Single Sign On)是一种身份验证解决方案,可让用户通过一次性用户身份验证登录多个相互信任的应用程序和网站。</</summary>
<category term="安全建设思路" scheme="https://kylinlingh.github.io/categories/%E5%AE%89%E5%85%A8%E5%BB%BA%E8%AE%BE%E6%80%9D%E8%B7%AF/"/>
<category term="账号安全" scheme="https://kylinlingh.github.io/tags/%E8%B4%A6%E5%8F%B7%E5%AE%89%E5%85%A8/"/>
<category term="SSO" scheme="https://kylinlingh.github.io/tags/SSO/"/>
</entry>
<entry>
<title>企业DevSecOps流程建设之工具篇</title>
<link href="https://kylinlingh.github.io/2022/06/21/%E4%BC%81%E4%B8%9ADevSecOps%E6%B5%81%E7%A8%8B%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%B7%A5%E5%85%B7%E7%AF%87/"/>
<id>https://kylinlingh.github.io/2022/06/21/%E4%BC%81%E4%B8%9ADevSecOps%E6%B5%81%E7%A8%8B%E5%BB%BA%E8%AE%BE%E4%B9%8B%E5%B7%A5%E5%85%B7%E7%AF%87/</id>
<published>2022-06-21T00:49:05.000Z</published>
<updated>2023-06-27T08:25:08.031Z</updated>
<content type="html"><![CDATA[<p>任何一个安全项目或体系的建设始终围绕着三个核心点来进行:组织,流程,工具:</p><ul><li>组织:确立项目目标和建设实施团队</li><li>流程:建立制度,明确各方责任</li><li>工具:支撑流程的运转</li></ul><p>本文着眼于DevSecOps流程建设中所使用的工具,暂时先不讨论其他两个点。</p><h1 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h1><p>在讲究小步快跑和快速迭代的互联网企业里,效率的优先级要比质量高,在发展中解决质量问题是大家的共识。这种观点在书本《Google系统架构解密》<a href="#refer-anchor"><sup>[8]</sup></a>中是这样评价的:</p><blockquote><p>区分初始速度和持续速度是很重要的。在项目初期选择不考虑安全性、可靠性和可维护性等关键需求,确实会提高项目早期的速度。然而经验表明,这样做通常会在项目中后期显著地拖慢进度。为了适应涌现特性的需求而重构设计的后期成本可能非常高。此外,为解决安全和可靠性风险而进行侵入性的后期更改本身可能会带来更多的安全和可靠性风险。</p></blockquote><p>结合我在互联网企业里的项目开发经历来看,在没有建设好安全文化和流程(换句话说,不重视安全)的互联网公司里,传统的SDL流程还是稍显笨重。如果真的强求每个项目都在立项之处就从安全威胁建模做起,光是安全团队的规模就很庞大;另一方面,追求项目快速迭代的团队也不希望被团队外的安全人员卡点。目前来看,应用安全建设里最可靠的方法还是建设好DevOps的工具链,在工具链里融入可以高度自动化的安全测试工具,在项目的编码,集成,发布和运行阶段都进行安全测试并提供足够的安全故障自助处理工具让开发人员将项目的安全水位提升到及格线之上。</p><h1 id="DevSecOps流程建设"><a href="#DevSecOps流程建设" class="headerlink" title="DevSecOps流程建设"></a>DevSecOps流程建设</h1><p>建设DevSecOps流程需要一个体系化的建设思维,现在随便谷歌一下也能找到一大堆解决各个单点问题的开源工具,但我觉得要真的做好DevSecOps流程建设,需要把握几个关键点:</p><ul><li>全局把握安全建设的重点方向和节奏,了解清楚各个单点问题的轻重缓急,先做哪个后做哪个其实很重要,毕竟企业愿意往安全方向投入的资源本来就很少,如何用很少的资源去支撑整个安全团队的持续发展也是一个需要思考的问题。</li><li>如何将各个单点工具串联起来并自动化运营,也就是所谓的安全编排,毕竟工具越多,告警越多,需要投入的安全人力就越多。如果没有自动化运营的能力,搞到最后就会出现部署一大堆东西但是没人管的局面。</li></ul><p>首先思考代码的漏洞来自何方,我们才能知道究竟要解决DevOps中的哪些安全问题:</p><ul><li>编码过程引入的漏洞<br>典型的例子是后端程序在接收到参数时直接拼接成sql语句执行,很容易就会遭到sql攻击</li><li>代码中引入了开源组件中的漏洞<br>如fastjson,log4j等应用非常广泛的第三方组件在你使用的时候是安全的,但保不准哪一天就会爆出致命漏洞</li><li>代码的运行环境配置不正确<br>没有遵照代码运行环境(包括容器,云上和云下的服务器)的安全基线来配置启动参数</li><li>供应链被投毒<br>简而言之,就是第三方供应商提供的软件/硬件里包含了漏洞,譬如dockerhub上的投毒镜像</li></ul><h2 id="工具建设"><a href="#工具建设" class="headerlink" title="工具建设"></a>工具建设</h2><p>首先看一下DevSecOps流程中每个环节要用到的工具,如下图所示:<br><img src="https://www.softwaretestinghelp.com/wp-content/qa/uploads/2020/12/Application-Sec.png"><br>图中对应的四个阶段分别是:开发,集成(代码编译,生成镜像),发布(推送到运行环境)和运行。本章节会先介绍上图中经常出现的SAST,DAST,IAST这三个极其容易让人混淆的概念,然后再比较IAST与RASP的关系,最后介绍SCA,至于代码质量管控,威胁建模和镜像扫描则不在本文中介绍。</p><h3 id="AST"><a href="#AST" class="headerlink" title="AST"></a>AST</h3><p>AST类工具是测试工具,并不是漏洞挖掘工具。</p><ul><li><p>SAST(Static Application Security Testing,静态应用安全测试):SAST是白盒测试(<strong>可以看到应用的所有源码</strong>)的一种。简单来说,就是<strong>在应用编译前进行源码扫描</strong>,特点是误报率高。最好的实现方式是开发IDE的插件,以便开发人员在编码的时候就可以直接检测代码;另外,也可以把安全编码规范融入到IDE的插件中(例如<a href="https://blog.csdn.net/Monsterof/article/details/108239250">Alibaba Java Coding Guidelines</a>),这样的做法要比对开发人员进行线下编码规范培训的效果好。</p></li><li><p>DAST(Dynamic Application Security Testing,动态应用安全测试):DAST是黑盒测试(<strong>看不到应用的源码</strong>)的一种,简单来说,就是先把应用运行起来,然后不断地输入各种测试案例,通过观察生成的输出来判断是否有漏洞,常见的web扫描器就是DAST的一种工具。</p></li><li><p>IAST(Interactive Application Security Testing,交互式应用安全测试):IAST是灰盒测试的一种。(<strong>灰盒测试就是你能定位到代码问题的精确位置,譬如哪个源码文件的哪一行,但是你并不知道是什么原因导致的;白盒测试就是打开整个项目的源码,通读代码后找出导致这个问题的原因;而黑盒测试就是在项目运行的时候跑了一大堆测试后看到输出结果不正常,至于错在哪里,怎么错的一概不知</strong>)通过部署在服务器上的agent收集应用运行时的各种数据(接收到的请求,执行的函数,传递的参数等)来判断是否有漏洞(参考下面的洞态 IAST),如果想要做得更加精确,可以将业务请求放到扫描器上重放(参考下面的Baidu openrast-iast)。</p></li></ul><p><strong>三者之间的关系</strong><br>从下图中的左边部份可以看到SAST与DAST演化出了IAST,而WAF和IDS/IPS技术则逐渐演化出了RASP(在下一小节会介绍)。</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-13-04-59.png" alt="三者之间的关系"></p><p>下面通过两个IAST的开源产品来分析一下它的工作原理。</p><p><a href="https://doc.dongtai.io/docs/introduction/iast">洞态IAST</a>,它的产品文档里的原话是这样描述的:</p><blockquote><p>IAST 相当于 DAST 和 SAST 的组合,是一种相互关联的运行时安全检测技术。它通过使用部署在 Web 应用程序上的 Agent 来监控运行时发送的流量并分析流量流以实时识别安全漏洞。<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-18-23-49.png" alt="三者的区别"></p></blockquote><p>其实我觉得IAST算不上是DAST和SAST的组合,这是三个不同功能的产品,并不是说有了IAST就可以替代SAST和DAST了。结合上图再重新看我对IAST的概念解析就一目了然了。</p><p>再看其架构图<a href="#refer-anchor"><sup>[8]</sup></a>,原文如下:</p><blockquote><p>首先,在服务器上安装 IAST Agent。当 IAST 启动,用户访问 Agent 服务后,Agent 便开始采集数据,并与 OpenAPI 服务通信,进行上报数据和 Hook 规则的拉取。OpenAPI 将数据存储到数据库中,包括 MySQL 和 Redis。<br>然后,Agent 对 Engine 发送通知,Engine 便会来消费数据库中的数据,并在分析(<a href="https://doc.dongtai.io/docs/introduction/architecture#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8E%9F%E7%90%86">漏洞分析原理</a>)完毕后将漏洞信息回写到数据库中。<br>最后,用户通过 WebAPI 查看数据库中漏洞的数据信息。<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-18-20-29.png" alt="洞态IAST架构"></p></blockquote><p><a href="https://rasp.baidu.com/doc/hacking/architect/iast.html">百度 openrasp-iast </a>,关于系统架构的原话:</p><blockquote><p>IAST(交互式扫描)技术是一种实时动态交互的漏洞检测技术,通过在服务端部署agent程序,收集、监控Web应用程序运行时函数执行、数据传输,并与扫描器端进行实时交互,高效、准确的识别安全缺陷及漏洞。目前OpenRASP项目已实现相当于IAST agent端的OpenRASP agent,在此基础上引入一个扫描端,即可实现一个完整的IAST扫描工具。<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-19-59-19.png"></p></blockquote><p>从架构上可以看到它跟洞态 IAST 的不同之处是引入了一个扫描器来对每一个正常的业务请求进行流量重放。</p><h3 id="RASP"><a href="#RASP" class="headerlink" title="RASP"></a>RASP</h3><p>RASP(Runtime Application Self-Protection,运行时应用程序自我保护),其运行原理如下图所示:<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-10-25-52.png" alt="RASP技术原理"></p><ol><li><p><strong>收集数据</strong>:在运行APP的服务器上需要部署一个agent,这个agent会将防护功能”<strong>注入</strong>“到应用程序中,与应用程序融为一体。这里所谓的“注入”在不同的开发语言中会有不同的实现,以java举例,它的实现方式是通过Instrumentation接口(java.lang.instrument)编写一个agent,在 agent 中加入 hook 点,当程序运行流程到了 hook 点时,将检测流程插入到字节码文件中,统一进入JVM中执行(<a href="https://rasp.baidu.com/doc/hacking/architect/java.html">更底层的实现原理参考这里</a>)。<strong>既可以在APP运行前就注入,也可以在APP运行时注入</strong>,尤其是运行时注入的能力提供了在APP不重启的情况下进行漏洞热修复的功能。实现原理如下图所示:<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-12-51-14.png"></p></li><li><p><strong>阻断执行</strong>,agent还可以根据请求的参数和应用运行时的上下文(堆栈信息)来判断是否需要阻断该请求。RASP一般都会拿来与WAF(Web Application Firewall)进行比较,因为WAF也是在APP处理请求前先过滤掉请求,<strong>但WAF只能通过检测流量的特征,而不能获取到应用运行时的上下文</strong>,所以会造成很多误报。同时WAF严重依赖于恶意流量的特征库,只要特征库更新不及时就很容易被绕过。举例来说,log4j漏洞在刚爆发时,短时间内就出现了很多poc,WAF是很难及时将全部poc都同步到规则库中;并且攻击者可以通过流量混淆工具来隐藏恶意poc,导致WAF出现误报和漏报。但是RASP就可以很好地处理这个难题,因为只要agent检测到log4j漏洞使用的JNDI的框架,就可以结合上下文信息来判断是否需要停止应用的业务响应流程,从而拦截恶意请求,如下图所示:<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/202212291243917.png"></p></li></ol><p>RASP的开源产品有<a href="https://rasp.baidu.com/">百度 openRASP</a>,上面提到的百度 openrasp-iast 就是在此产品的基础上添加了一个扫描器而成,由此可见 IAST 与 RASP 的紧密联系。</p><p>Q:是否有了RASP就完全不需要WAF呢?<br>A:我认为二者是互相补充,而不是替代关系。RASP的确可以比WAF看到更多的信息(如下图所示),但并不是所有的应用都可以通过RASP来做保护,目前RASP最广泛的应用场景还是在java语言开发的APP上;另外一个问题时RASP对APP造成的性能影响你又是否能接受呢;并且万一RASP引入了新的故障点,谁来负责处理也是个问题。<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-29-12-56-11.png"></p><p>额外提一下,我认为RASP在一种特殊场景下会比较有用,那就是保护那些已经运行很久的遗留项目或者是没有源代码的第三方项目,并且这些项目对性能的要求并不高。</p><hr><p>最后展示一下这四个工具在DevSecOps流程中发挥作用的阶段,如下图所示<a href="#refer-anchor"><sup>[1]</sup></a>,其中圆点的实心比例表示工具的自动化程度:<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-28-18-20-21.png" alt="DevSecOps 安全工具在不同阶段的自动化程度"></p><p>至此,已经介绍完了DevSecOps里常用的几个漏洞测试工具,下面继续分析剩余的其他工具。</p><h3 id="SCA"><a href="#SCA" class="headerlink" title="SCA"></a>SCA</h3><p>SCA(Software component analysis,软件成分分析):如字面意思,此工具就是用来分析软件成分的,何为软件成分?那就先引入一个新的名词:<strong>SBOM<a href="#refer-anchor"><sup>[1]</sup></a>(Software Bill of Materials,软件物料清单)</strong> 其概念来自于制造业,譬如汽车制造商为每辆汽车维护的BOM就列出了原始设备制造商和第三方供应商的零件清单,用于在发现零件缺陷的时候可以根据批次信息来跟踪处理。<strong>而在软件上,BOM描述的是软件包依赖树的一系列元数据</strong>,包括组件名称、版本号、许可证、版权等多项关键信息。目的是为了发现潜在的<strong>安全问题</strong>(软件漏洞)和<strong>许可证问题</strong>(假如你引用的开源软件用的是GPL许可证,那么你开发的软件也必须公开全部源代码,<a href="https://xie.infoq.cn/article/e3be19b16c33494a173458c4c">可以参考这个案例</a>)。</p><p>SBOM的两大作用:</p><ol><li>维护了一份软件的资产清单,在处理组件漏洞的时候可以快速定位到受影响的软件资产,避免了两眼一抹黑,连自家有哪些软件受漏洞影响都不知道的情况。</li><li>在DevSecOps的流程中嵌入SCA(软件成分分析)工具,通过对生成的SBOM进行漏洞和许可证匹配,在软件上线之前就检测出安全问题和许可证问题,可以减少上线后才发现问题而带来的很多麻烦。</li></ol><p>一份完整的SBOM生产流程如下图所示<a href="#refer-anchor"><sup>[1]</sup></a>,注意SBOM需要跟随软件的迭代而更新,因此需要使用自动化工具来创建,而SCA提供了这样的能力。</p><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2022-12-28-18-34-02.png" alt="SBOM生产流程"></p><p>既然已经有了SBOM,那就剩下用漏洞库去匹配这个清单,找出其中有漏洞的组件。公开的漏洞库包括有:NVD/CNVD/CNNVD,但这些国家级漏洞库收录的漏洞数量非常多,并且很多都是很陈旧的,越庞大的漏洞库虽然能带来越高的漏洞发现率,但也意味着匹配的时间越长。并且一个项目是否通过检查出来的漏洞数量来衡量其安全水位呢,也值得你去思考。我更推荐<a href="https://security.snyk.io/">snyk vulnerability db</a>,漏洞的数据格式一致,不需要用多个不同的html解析模板,并且数据的速度也够快,但使用爬虫去高频爬取这个漏洞库可能会存在法律风险。</p><p>下面介绍一下SCA里的一个知名开源产品:<a href="https://jeremylong.github.io/DependencyCheck/">OWASP Dependency-Check</a>,它的工作原理如下:</p><ol><li>收集项目依赖信息:JarAnalyzer模块从Java项目的 Manifest, pom.xml 和 JAR 文件里的包名收集足够的项目依赖信息。</li><li>分析并索引漏洞库:将NVD的漏洞库解析出来并通过Lucene索引存储</li><li>匹配依赖与漏洞库:CPEAnalyzer模块会匹配项目中的每个依赖与漏洞库,最后将所有命中的漏洞库生成一个报告,报告内容如下<a href="#refer-anchor"><sup>[12]</sup></a>:</li></ol><p><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-03-16-53-00.png" alt="漏洞报告"></p><p>注意,OWASP Dependency-Check 并没有收集详细的SBOM,也没有致力于解决许可证风险,并不是SCA的完全体。而另一个开源项目 <a href="https://github.com/murphysecurity/murphysec/blob/master/README_ZH.md">墨菲安全 - murphysec</a> 则收集了SBOM并列举了许可证风险,可以参考一下。</p><h3 id="ASOC"><a href="#ASOC" class="headerlink" title="ASOC"></a>ASOC</h3><p>大家看到安全编排这个词时可能先想起的是 SOAR(Security Orchestration, Automation and Response,安全编排自动化与响应),但我在此文里强调的更多是应用安全,SOAR的概念要比 ASOC(Application Security Orchestration and Correlation,应用安全编排和关联)庞大且复杂。为了始终聚焦 DevSecOps ,我在此采用的是 ASOC 这个概念, <a href="https://www.synopsys.com/glossary/what-is-application-security-orchestration-and-correlation.html">synopsys</a> 是这样来定义这个概念的:</p><blockquote><p>应用安全编排和关联(Application Security Orchestration and Correlation,ASOC)是一种<strong>通过自动化的工作流</strong>来帮助简化漏洞测试和修复过程的应用安全解决方案,ASOC从各种应用安全工具(如SAST,DAST和IAST)<strong>收集数据</strong>并将他们合并到一个数据库中,然后<strong>通过关联分析这些数据来确定漏洞修复工作的优先级</strong>,最终使安全团队能够以知情且高效的方式来精简应用安全的运营工作。</p></blockquote><p>从定义中就可以看到关键词:工作流,大家在日常工作中通过OA里发起的各种审批流程就是一个工作流:发起申请 -> 工作流创建并开始;各级人员审批 -> 工作流执行;流程归档 -> 工作流结束。因此 <strong>ASOC 工具的关键就是创建工作流,在不同的人员中自动化流转漏洞处理工单</strong>。</p><p>对应的商业软件有 <a href="https://www.synopsys.com/software-integrity/code-dx.html">synopsys Code Dx</a> ,但并没从中找到什么参考价值。反而可以从 <a href="https://juejin.cn/post/6844904167463485453">工作流引擎原理-打造一款适合自己的工作流引擎</a> 这篇文章里找到工作流的实现方法,下图展示了我们自己设计的漏洞工单工作流,在漏洞的修复流程中主要关联了开发人员和安全人员,在一个漏洞被修复的过程中,整个工单经过的流程如下所示:<br><img src="https://hexo-pic-1300331708.cos.ap-guangzhou.myqcloud.com/2023-01-04-18-00-06.png" alt="漏洞工作流"></p><p>我们所有的漏洞测试系统都关联到这个工作流中,当某个系统检测出了漏洞之后会自动创建工作流的实例并自动在企业微信上通知相关人员修复漏洞,当工单负责人修复好了漏洞之后,在我们的安全平台上点击“确定修复”之后,系统就会自动测试该漏洞是否真的已经被修复,只要复测通过就自动结束该流程,关闭工单。从整个流程可以看到,基本不需要安全人员介入,极大地节省了人力。</p><h2 id="产品与工具矩阵图"><a href="#产品与工具矩阵图" class="headerlink" title="产品与工具矩阵图"></a>产品与工具矩阵图</h2><p>下面罗列一下各种工具对应的开源和商业产品,有兴趣的可以挑出几个来分析。</p><table><thead><tr><th align="left"></th><th align="left">SAST</th><th align="left">DAST</th><th align="left">IAST</th><th align="left">RASP</th><th align="left">SCA</th></tr></thead><tbody><tr><td align="left">开源</td><td align="left">NodeJsScan, SonarQube,</td><td align="left">OWASP ZAP</td><td align="left">洞态 IAST , baidu openrasp-iast, Shadowd</td><td align="left">OWASP AppSensor, 百度 openRASP</td><td align="left">墨菲安全, OWASP Dependency-Check</td></tr><tr><td align="left">商业</td><td align="left">SonarQube, Synk, CheckMarx, Bandit, GitLeaks, Synopsys</td><td align="left">Acunetix, Synopsys, AppSpider, Acunetix, Burp Suite, Checkmarx</td><td align="left">灵脉IAST</td><td align="left">云鲨RASP, Contrast Security, Waratek</td><td align="left">Blackduck, DepScan, HCL AppScan, Synk, Sonatype, Synopsys</td></tr></tbody></table><h1 id="我的建设思路"><a href="#我的建设思路" class="headerlink" title="我的建设思路"></a>我的建设思路</h1><p>结合我司的安全建设历程和效果,我认为整个工具链可以按照以下顺序来建设:</p><ol><li>SCA:软件成分分析应该摆在第一位,原因有以下几点:<ul><li>现在爆发严重漏洞的次数有越来越多的趋势,并且每个都很致命。在我司建设SCA之前,遇到这种情况就只能发整改表,依赖于业务自查;一方面会导致沟通成本非常高,另一方面,有些项目在交接过很多次之后就被人遗忘了,无法做到一个全局的掌握。将SCA嵌入到DevOps的流程中,在项目上线前先进行软件成分分析,从而掌握线上生产项目的资产表。如果不这样做,而是对公司的整个代码仓库进行软件成分分析,这样是没有意义的,因为我们并不关注那些没有在生产环境运行的项目代码。</li><li>SCA可以做到高度自动化,如果没有漏洞编排工具,那么将SCA系统与公司的IM软件(如企业微信,钉钉等)和告警系统打通也能实现一定程度上的漏洞编排功能。SCA并不仅仅是漏洞应急工具,更应该是一个日常运行的工具,毕竟随时会有开发人员无意间使用含有漏洞的第三方库。</li></ul></li><li>ASOC:安全编排工具可以与SCA进行同步开发,因为ASOC可以赋予SCA高度自动化的能力,并且在SCA上线前优化用户体验的时候也需要同步调整ASOC。有了ASOC之后,所有的安全服务都可以编排出来并按照流程自动化运行,彻底走出了手工excel运营的阶段。</li><li>制品扫描:曾经发生过的 XcodeGhost 风波是攻击者在非官方的 Xcode 软件中植入了后门从而将恶意代码植入到开发者生成的iOS应用程序中,属于供应链攻击的一种。所以即使代码经过了SCA的测试,最终生成的制品仍然可能是不安全的。最好的办法就是对生成的二进制程序或docker镜像再进行一次扫描,可以检查出这些SCA无法发现的问题:安全配置问题,编译选项检查,敏感信息检查等。</li><li>漏洞扫描器:安全团队一般都用来做日常的定期巡检,其实也可以对业务/SRE团队开放特定的扫描能力,用于在业务上线前进行web安全测试。</li><li>SAST:我之前用过SonarQube来做静态代码扫描,但代码质量这个东西不好推广,因为每个项目都能检出一大堆问题,并且误报还不少,所以在试用过之后也搁置了。</li><li>RASP:正如前文所说,RASP比较难推广,越是重要的公司级业务就越追求性能表现,现在为了加强安全而消耗掉部份性能,对业务来说是很难接受的;另一方面,万一RASP引入了新的故障点,这个也会严重破坏安全与业务人员和SRE之间的团队关系。但是RASP的确带来了很多新的优势,尤其是可以防那些未披露的漏洞,<strong>该技术可以保持长久的关注</strong>。</li><li>Fuzzing(模糊测试):这里引入的模糊测试在常规的DevOps流程中并不常见,在此做一下简单介绍:模糊测试是通过自动生成随机的数据值或文件去测试API,用于解决数据验证中的问题,并查看当传递了错误的值时系统会发生什么情况。安全研究人员通常使用Fuzzing技术来查找代码中的漏洞,但Fuzzing存在一个大的问题是在测试中可能使得系统崩溃,并且并不总是能提供很明确的错误反馈,需要人员投入大量的时间进行排查,因此该技术在自动化测试的流程中并不常见。</li></ol><div id="refer-anchor"></div><h1 id="本文引用"><a href="#本文引用" class="headerlink" title="本文引用"></a>本文引用</h1><p>[1] <a href="https://www.dsocon.cn/2021/#/down">软件供应链安全白皮书2021 by 悬镜</a><br>[2] <a href="https://www.jrasp.com/guide/">RASP安全技术</a><br>[3] <a href="https://tech.meituan.com/2019/11/07/java-dynamic-debugging-technology.html">Java 动态调试技术原理及实践</a><br>[4] <a href="https://www.anquanke.com/post/id/263539">Apache Log4j2,RASP防御优势及原理</a><br>[5] <a href="https://www.softwaretestinghelp.com/differences-between-sast-dast-iast-and-rasp/">Differences Between SAST, DAST, IAST, And RASP</a><br>[6] <a href="https://www.51cto.com/article/717663.html">How Instrumentation-based IAST and RASP Revolutionize Vulnerability Assessment for Application</a><br>[7] <a href="https://www.synopsys.com/glossary/what-is-application-security-orchestration-and-correlation.html">Application Security Orchestration and Correlation</a><br>[8] <a href="https://jishuin.proginn.com/p/763bfbd710bd">重磅推荐: 全球唯一开源的 IAST 自动化漏洞检测工具</a><br>[9] 《Google系统架构解密:构建安全可靠的系统》 希瑟·阿德金斯, 贝齐·拜尔, 保罗·布兰肯希普, 彼得·莱万多夫斯基, 阿那·奥普雷亚, 亚当·斯塔布菲尔德 著;周雨阳 刘志颖 译<br>[10]《敏捷应用程序安全》 Laura Bell, Michael Brunton-Spall, Rich Smith, Jim Bird 著;杨宏焱, 刘恒屹 译<br>[11]《DevSecOps敏捷安全》 子芽 著<br>[12] <a href="https://blog.xujiuming.com/ming/61a3df4c.html">owasp扫描依赖漏洞笔记</a><br>[13] <a href="https://juejin.cn/post/6844904167463485453">工作流引擎原理-打造一款适合自己的工作流引擎</a></p>]]></content>
<summary type="html"><p>任何一个安全项目或体系的建设始终围绕着三个核心点来进行:组织,流程,工具:</p>
<ul>
<li>组织:确立项目目标和建设实施团队</li>
<li>流程:建立制度,明确各方责任</li>
<li>工具:支撑流程的运转</li>
</ul>
<p>本文着眼于DevSec</summary>
<category term="安全建设思路" scheme="https://kylinlingh.github.io/categories/%E5%AE%89%E5%85%A8%E5%BB%BA%E8%AE%BE%E6%80%9D%E8%B7%AF/"/>
<category term="DevSecOps" scheme="https://kylinlingh.github.io/tags/DevSecOps/"/>
<category term="依赖检测" scheme="https://kylinlingh.github.io/tags/%E4%BE%9D%E8%B5%96%E6%A3%80%E6%B5%8B/"/>
</entry>
</feed>