# 从 II 登陆到与后端交互，发生了什么？

By [Kjsghuh](https://paragraph.com/@kjsghuh) · 2022-05-17

---

![图 - 1](https://storage.googleapis.com/papyrus_images/2dfecfd53913360373884f23e8ef299c77eb3e07a5248158c6c7e1a5346003cb.png)

图 - 1

先考虑如图 - 1所示的几个基本的数据结构，其中Delegation Identity、Ed25519KeyIdentity都是继承于SignIdentity。

接下来我将简单介绍一下从点击登陆按钮开始到通过agent.call 调用后端这一过程发生了什么：

II 登陆：
------

通常，在用户点击login 按钮之后，会调用 AuthClient.login()

![图 - 2](https://storage.googleapis.com/papyrus_images/81a26ad805e1201f1644b09ef5c617710611aff8665d2a5ac2dbf294fdd98b8d.png)

图 - 2

其函数签名如 图 -2

其中identity Provider可以指定 II 服务的提供者，默认为 https://identity.ic0.app, 如果你是用的本地的II，这里需要填上本地II对应的URL，详细可见：

[https://kluy7-aiaaa-aaaad-qamvq-cai.ic.fleek.co/2021/07/Gh202107aa106da32145b6840e146a99bff34771/](https://kluy7-aiaaa-aaaad-qamvq-cai.ic.fleek.co/2021/07/Gh202107aa106da32145b6840e146a99bff34771/)

maxTimeToLive可以指定这一次登陆的存活时间，在未过期的这段时间里访问应用可以不用再登陆。

![图 - 3](https://storage.googleapis.com/papyrus_images/455e359c95693cade77b596bcaf42dda5a2c5ed09d4df84d0c8f0d6bf61dbbd8.png)

图 - 3

上图为login函数体，其首先通过Ed25519KeyIdentity.generate()生成一个Ed25519KeyIdentity实例，其包含一对公私钥，然后将其存入local Storage，然后根据options里面的Identity Provider打开对应的窗口，其url应如下所示：

![](https://storage.googleapis.com/papyrus_images/a52dc020690386af55e0ccbfade614fc0ddb5ebfbc6bad55e29a2cff4e9e9d91.png)

![图 - 4](https://storage.googleapis.com/papyrus_images/7e4ab39b95f6c4ae0f1c0c02b9f46fef44e4c0a6df00525950a4a512d9e456d7.png)

图 - 4

图 - 4 为provider的入口文件，首先是一系列的检查，判断你的Provider是否正规且安全，随后会调用 login flow里面的login函数：

![图 - 5](https://storage.googleapis.com/papyrus_images/a0555a8261d062f1cae2c2f19ed3e32ccb983a9cd1ff23eee161d64f40bc96be.png)

图 - 5

其首先会判断一下你是否为新用户(userNumber是否已知)，已知则调用loginKnown Anchor，这里讨论已知的情况：

![图 - 6](https://storage.googleapis.com/papyrus_images/c32362e403a0f32f805facf070b0cbb3f11dc83deeb002b626bedc59f6940ba6.png)

图 - 6

这里就是我们熟知的Authenticate页面：

![图 - 7](https://storage.googleapis.com/papyrus_images/3c2de1e12f04f6429eface32b0adf7d8c78c093d2dc1dffd7856ed8edb8cd26a.png)

图 - 7

当点击了Authenticate按钮之后会调用 IIconnection里面的login方法，参数为usernumber：

![图 - 8](https://storage.googleapis.com/papyrus_images/021e08a1acb35fbb61e94a09217f6e55484a4a9fa71db9caeb8edcc0f430dcdb.png)

图 - 8

首先调用II Connection内部的lookupAuthenticators方法获取用户所有已认证的设备，如果设备数为0则返回，否则将user Number和devices传入fromWebauthnDevices函数：

![图 - 9](https://storage.googleapis.com/papyrus_images/5e9106b670ab6f8fdf42a9933f24bdfa11367c30de127c6e40249c495d1dfd55.png)

图 - 9

接下来就是你触摸Yubikey或者指纹的时候了，完成这一过程之后会生成一个经过你认证的MultiWebAuthnIdentity，其也是继承于 SignIdentity，然后将其传入requestFEDelegation函数，

![图 - 10](https://storage.googleapis.com/papyrus_images/93e5931f405919b7e480364c07f245eccb04eda4d7107a8dc6ae7f135456f9ec.png)

图 - 10

在这里会另外生成一个Ed25519KeyIdentity 和一个过期时间，将传入的identity加上Ed25519KeyIdentity的publicKey以及过期时间和一个targets(作用域，这里的canisterId为II的canisterId) 作为参数传入DelegationChain的create函数，这个函数会返回一个DelegationChain：

![图 - 11](https://storage.googleapis.com/papyrus_images/a85a78bbd801841d1f375b738afd849fd36d6a155411a8dd49ef58304b0c5e3f.png)

图 - 11

create方法可以调用多次，在options里面传入之前的Chain就可以将当前的Delegation添加到那个Chain的后面，所以这个chain是可以增长的，但这里只调用了一次；函数内部首先会根据传入的Identity、需要被委托的PublicKey（被委托后他就具有用户的权利，可以签名消息）、过期时间、options 生成一个单一的SignedDelegation：

![图 - 12](https://storage.googleapis.com/papyrus_images/19b0cb8c4b519a02c9c890bca095a0c31a95146089a4f03f3e02d2d32ab323b4.png)

图 - 12

一个delegation如 **图 - 1**所示有三个属性，这里根据传入的参数构建一个普通的delegation，过期时间为10分钟，然后用传入的 SignIdentity（为当时的MultiWebAuthnIdentity）对这个delegation的requestId加上一个前缀进行一次签名，然后将delegation和signature合成一个SignedDelegation返回，这里的delegation好比机构的一封没有盖章的文件，虽然里面有指定谁能进行重要的操作，但并不具有效应，当机构进行盖章之后（签名），这个文件才能生效（SignedDelegation）。

回到**图 - 11**，create方法将生成的SignedDelegation结合MultiWebAuthnIdentity的公钥（如果options里面传入了之前的Chain，则为这个chain的公钥）组成一个DelegationChain返回。

再回到**图-10**，requestFEDelegation方法将create方法返回的Chain加上刚才生成的Ed25519KeyIdentity合起来生成一个DelegationIdentity返回。

再再回到**图-9**，随后会根据requestFEDelegation返回的DelegationIdentity调用IIConnection的createActor方法创建一个和 II 后端交互的Actor，由于此DelegationIdentity是经过你验证的 identity委托的，所以它能代表是你向后端发出的消息，create Actor如下：

![图 - 13](https://storage.googleapis.com/papyrus_images/76e5857bcaba86d0712f43dfd3bb9033c9e6555ff2d3587630983471f5335839.png)

图 - 13

通过MultiWebAuthnIdentity、DelegationIdentity和Actor构建一个 IIConnection，联合着user Number返回。

再再再回到图-4，会取到刚才返回的userNumber和Connection，然后会判断当前URL的hash，如果为 “#authorize” 则会调用auth.ts 里面的setup方法：

![图 - 14](https://storage.googleapis.com/papyrus_images/0ed756304a52d1cf47623af1f96e93c8b2d89c38ec3a16448d0f429c00cb122d.png)

图 - 14

这里会向源窗口发送一条 "authorize-ready" 信息告诉它我准备好了，在图 - 3中可以看到源窗口在之前就添加了一个事件监听，所以其可以收到这条消息，收到消息之后会调用\_getEventHandler来处理这条消息：

![图 - 15](https://storage.googleapis.com/papyrus_images/78d64bd4b32152d1fe903ca9a9584c83d64051f06a784fd27466d5219622f355.png)

图 - 15

还记得图-3第一步就生成的Ed25519KeyIdentity吗？这里将其publicKey取出并转成Der格式，加上你设置的MaxTimeToLive，构建一个request结构体，并将其回复给Provider。

回到图-14，provider收到源窗口回复的信息之后，会将userNumber、connection、回复的message、源窗口URL传入handleAuthRequest函数：

![图 - 16](https://storage.googleapis.com/papyrus_images/18f5270f75430f988435c5e924fbc0cd8d627022b15c3855dfb1e12dc7417606.png)

图 - 16

首先会调用connection的getPrincipal方法，得到用户的Principal，这里就不贴图了，其函数内部首先会检查这个connection里面的delegation Identity是否已经过期了（10分钟），如果过期，会让你重新认证一次，没过期就用connection里面的Actor调用后端的get Principal方法拿到用户的principal；然后将principal和URL传入confirmRedirect函数：

![图 - 17](https://storage.googleapis.com/papyrus_images/33f699f42a9fffea91f6c89585791ed243e95d1284415b970ebf6ae92cd22ed5.png)

图 - 17

可见，这里就是我们所熟知的proceed页面，当点击了proceed之后会返回 true，然后执行如下语句：

![图 - 18](https://storage.googleapis.com/papyrus_images/bd1a289c766c7d39519e2ddfef4cb2b66ee776c023c622c0317ac3cdc4e9a8e3.png)

图 - 18

首先拿到源窗口回复的message里面的sessionPublicKey（就是最最最开始生成的Ed25519KeyIdentity的公钥），将其联合其他几个参数传入prepareDelegation方法，这里也不贴图了，其内部首先也会检查这个connection是否过期，没过期就调用后端的prepare\_delegation方法，会返回一个userKey和timestamp:

![图 - 19](https://storage.googleapis.com/papyrus_images/e48dcc8c63b5947c80396adf8d38d67f8a193b69365b112b585d5facf6f2a27e.png)

图 - 19

首先是经典的一系列检查，然后根据user Number和前端URL生成一个seed，对这个seed以及一系列message进行签名，更新一下rootHash，最后将seed转化一下变成userKey联合着过期时间返回。

回到图-18，随后会调用retryGetDelegation方法：

![图 - 20](https://storage.googleapis.com/papyrus_images/94819be1a20ae8aedf08c6f7fdcd465913b6bfe898cce2989a2ace0d8dc33b6c.png)

图 - 20

这里会去循环调用后端的get\_delegation方法，如果后端返回了则跳出循环，否则抛出异常，循环次数最大为5次。

![图 - 21](https://storage.googleapis.com/papyrus_images/ab6c027532d8a418142c4b58be31d529ad8ff07b960101479e7d1fbaa9904a51.png)

图 - 21

这个函数会返回一个signedDelegation，其中delegation由最开始的Ed25519KeyIdentity的公钥、过期时间（timestamp，其实就是那个maxTimeToLive）、targets（Null）组成，联合着刚才的signature返回。

将get\_delegation方法返回的signedDelegation转化一下变成一个新的signedDelegation（里面内容没变），将其放入一个空数组，联合上userKey返回。

回到图 - 14，利用handleAuthRequest函数返回的 \[signedDelegation\]、userKey生成一个response回复给源窗口。

![图 - 22](https://storage.googleapis.com/papyrus_images/e3356c3e1fc25c345beaf6775f8fbed61be10f901e4ccba539ed27850d65d527.png)

图 - 22

源窗口收到回复后调用\_handleSuccess方法（如上图），首先会根据message里面的 \[signedDelegation\]生成一个delegations（其实就是copy一份），然后结合着message里面的userPublicKey（后端返回的那个userKey）生成一个delegationChain，将其写入localStorage，最后一步了，利用最最最开始生成的那个Ed25519KeyIdentity加上这个Chain生成一个delegationIdentity。这就是我们和后端交互的时候创建agent的时候传入的那个Identity。

ok，到这里我们可以梳理一下最终的这个DelegationIdentity的组成：

![图 - 23](https://storage.googleapis.com/papyrus_images/1504c0c449ba7e28cbce9614a963f62d71763c80be08c5a560feabd2d47b103f.png)

图 - 23

请注意这两个存入local Storage的东西，这个delegationChain里面包含着 II 的过期时间，当你刷新页面的时候，会重新调用一次 AuthClient.create()方法，其会尝试从local Storage里面直接取出Ed25519KeyIdentity和DelegationChain，并且检查是否过期，通过这两个东西直接构建出DelegationIdentity，这样就不需要重新登陆了。

这里想多说一下这个signature，回到图-19，可以看到签名是对seed，session\_key（Ed25519KeyIdentity），expiration（过期时间）签名，其内部其实是对这些东西组成的一个结构体的hash签名，这是canister signature，后来验证是子网来验证。用 II 的Actor调用后端的函数本身就是用户认证过的，所以将Ed25519KeyIdentity的公钥设置为委托，并且对其进行签名，都是用户同意的，所以生成的这个delegationChain是可以代表用户的，即这个最终的DelegationIdentity是认证过的identity，用它来调用后端的函数也是可以代表用户的。

Agent.call()：
-------------

![图 - 24](https://storage.googleapis.com/papyrus_images/5319c2a73d742af48a65adba132dfb43987f6108f148e2ae8f8bd78a6e582761.png)

图 - 24

这里会先看看identity是否存在，然后拿到sender，对参数进行一些转化赋值给submit，然后构建一个request结构体。

![图 - 25](https://storage.googleapis.com/papyrus_images/b58f601ec95e8979ee10709fe645021d68651ed16491dd980808b41fbbd4b1ab.png)

图 - 25

然后将这个结构体传给delegationIdentity里面的transformRequest方法：

![图 - 26](https://storage.googleapis.com/papyrus_images/dcefb28754e5fda339c1c5d9c9c1648e103db5659a4a8f323411b745b61e3ce0.png)

图 - 26

可以看到，在和后端交互的时候，会用identity里面的Ed25519KeyIdentity对这个requestId进行一次签名，然后伴着Identity里面的delegationChain中的delegations和pubKey一起传给后端，这里的sender\_sig可以利用delegation里面的publicKey来验证，然后delegation里面的签名由子网验证，这样就可以做到信息的可靠性。sender\_pubKey可用来生成principal，call函数的最后一段就是将请求发给后端，然后等待http response。

结语：
---

Internet computer 的身份系统奇妙而伟大，值得所有ic开发者去学习，这篇文章只是从表层去阐述了一下整个流程，其设计思想还需要读者自己去摸索，我也建议在阅读这篇文章前后都去熟悉一下agentjs和II的源码结构，了解各个数据结构的构造。其中有部分内容为我所推断，并没得到有效的证实，如有不同意见可随时与我交流。另外，由于DelegationChain的create方法可以被多次调用，理论上是可以在不同Dapp中使用同一个DelegationIdentity，只需要将delegation添加进delegationChain的delegations数组里，但我并没有实现过，如果有此经验的小伙伴欢迎与我交流，感谢🙏

---

*Originally published on [Kjsghuh](https://paragraph.com/@kjsghuh/ii)*
