区块链 + .NET MAUI ❤️ 使用身份验证的体验


近年来,区块链技术通过提供去中心化、安全和透明的解决方案,彻底改变了各个行业。随着企业越来越多地寻求利用区块链的潜力,将其与.NET MAUI等现代开发框架集成变得至关重要。但是,.NET MAUI 与以太坊生态系统的集成仍处于起步阶段,使用这两种技术的经验很少。本文探讨了我们在.NET MAUI 应用程序中使用区块链进行身份验证的经验,重点是 “使用以太坊登录” (SIWE) 协议的集成。我们深入研究了使用WalletConnect将MAUI应用程序与以太坊钱包连接的过程,重点介绍了这种方法如何通过允许用户在不依赖传统凭据的情况下进行身份验证来增强安全性和用户体验。

区块链是一项创建于2008年的技术,是第一种加密货币比特币的基础。本质上,它是一个仅限追加的数据库,这意味着可以添加数据,但不能删除或修改数据,从而确保其不变性。这种分布式账本系统通过去中心化节点网络进行维护,无需中介机构,降低了操纵或审查的风险。此外,区块链提供了一定程度的假名性,因为交易和数据与加密地址而不是个人身份相关联,在保持系统透明度的同时保护用户隐私。
最初,区块链旨在支持比特币等加密货币,为进行金融交易提供安全和分散的系统。但是,随着时间的推移,区块链技术已经发展并扩大了其范围,在金融以外的各个领域都有应用,例如供应链管理、健康、艺术和数字身份。如今,区块链还用于身份验证,允许个人以去中心化的方式安全地验证自己的身份,从而消除了依赖传统中介机构的需要,增强了对个人数据的隐私和控制。
区块链的身份验证领域非常广泛,包括匿名凭证等高级方法。这些凭证允许使用诸如零知识证明之类的加密技术在不泄露个人信息的情况下进行身份验证。这确保了安全和私密的交易,消除了对中介机构的需求,并允许用户在分散的数字环境中保持对个人数据的控制。例如,如上所述,这些凭证可用于克服互操作许可和未经许可的区块链中的挑战 在这篇文章中 我和其他同事合着了。

在本文中,我们将使用以太坊区块链在我们的.NET MAUI 应用程序中使用用户的钱包对用户进行身份验证。这个概念基于 ERC-4361:使用以太坊登录。SIWE 是一种协议,允许用户使用其以太坊地址登录 Web 应用程序,从而无需使用传统的用户名和密码。通过使用私钥签署消息,用户可以在不暴露私钥的情况下证明对以太坊地址的所有权。这提供了一种安全和分散的身份验证方法,通过避免依赖集中式和易受攻击的身份验证系统来增强用户的隐私和安全性。与OpenID Connect或OAuth2等传统方法相比,这种机制特别适合web3应用程序。
市场上有成千上万的钱包,实现每个SDK来连接我们的应用程序的所有可能性是不可行的。因此,有两个选项:1)只选择最常用的钱包,例如TrustWallet、Metamask等,以及2)使用WalletConnect(我们将在本文中关注的选项)。
WalletConnect是一种开源协议,可将去中心化应用程序(dApps)与钱包安全地连接起来,允许用户在不泄露私钥的情况下通过扫描二维码或使用深度链接来授权交易。在这种情况下,我们将使用 WalletConnectSharp,它是 C# 协议的实现。你可以看看它的 GitHub 这里

我们将使用.NET MAUI 构建一个示例应用程序,允许用户使用他们的以太坊钱包登录。该应用程序将具有一个登录按钮,用于实现以太坊登录(SIWE)协议。第一步是访问 WalletConnect 云 网站,登录并创建一个帐户(如果没有)。登录后,在平台上创建一个新项目并确保保存项目 ID,因为稍后将需要它。
接下来,创建一个继承自的新类 按钮 然后添加一个命令来处理点击事件,如下所示:
public WalletConnectButton()
{
Text = "Sign in with WalletConnect";
Command = new Command(OnClicked);
}
这个 onClicked 函数包含执行登录过程的高级逻辑。它包括将应用程序连接到钱包,创建登录消息,请求钱包对该消息进行签名,最后验证签名是否有效。我们将逐步完成这个过程。
private async void OnClicked()
{
//Connects the app with the wallet.
var connected = await ConnectWallet();
if (!connected)
{
return;
}
//User's wallet address
var address = _dappClient.AddressProvider.DefaultSession.CurrentAddress(CHAIN_ID).Address;
//Return the siweMessage and the signature
var (signature, siweMessage) = await SignInWithEthereum(address);
if (signature == null || siweMessage == null)
{
var errorMsg = "Error: An error occurred signing in with ethereum";
Debug.WriteLine(errorMsg);
ErrorCommand?.Execute(errorMsg);
return;
}
//Verifies that the address that signs the SIWE message is the user's address.
var isValid = VerifyPersonalSignSignature(siweMessage.ToString(), signature, address);
if (isValid)
{
Debug.WriteLine("Success: User authenticated with Ethereum");
SignedInCommand?.Execute(new AutenticatedUserData
{
AuthenticationNonce = siweMessage.Nonce,
UserAddress = address,
WalletName = _dappClient.AddressProvider.DefaultSession.Peer.Metadata.Name,
UserPublicKey = _dappClient.AddressProvider.DefaultSession.Peer.PublicKey
});
return;
}
ErrorCommand?.Execute("The wallet signature is not valid");
Debug.WriteLine("Error: The wallet signature is not valid");
}
让我们仔细看看 ConnectWallet 函数。此函数构造两个对象: SignClientOptions 和 连接选项。可以修改这些对象以自定义钱包显示的消息并调整一些连接设置。
private async Task<bool> ConnectWallet()
{
//Modify this function to customize the client options.
var dappOptions = CreateSignClientOptions();
//Modify this function to customize the connection options.
var dappConnectOptions = CreateConnectionOptions();
//Initiates the client
_dappClient = await WalletConnectSignClient.Init(dappOptions);
var connectData = await _dappClient.Connect(dappConnectOptions);
//Launch the wallet app with the connection params
//On android the OS will let you choose the wallet app but on iOS it won't
var uri = new Uri(connectData.Uri);
await Launcher.Default.OpenAsync(uri);
try
{
//Wait until the user approves the connection
//If the connection is not approved or it times out an exception will be thrown
await connectData.Approval;
Debug.WriteLine("Approved");
return true;
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
ErrorCommand?.Execute(e.Message);
}
return false;
}
在 SignClientOptions 对象,你必须设置 projectID 使用从中获得的 ID 钱包连接 网站。您也可以取消对 “存储” 属性的注释以启用永久存储。元数据对象允许您自定义钱包消息的外观。
您可以通过以下方式进行设置 SignClientOptions 对象:
return new SignClientOptions()
{
ProjectId = WALLET_CONNECT_PROJECT_ID,
Metadata = new Metadata()
{
Description = "MAUI WalletConnect and SIWE example",
Icons = new[]
{
"https://cdn.prod.website-files.com/630fae9a46ee72ef23481b76/643471939ca472a0f0fa96b6_favicon.png"
},
Name = "MAUI SIWE Example",
Url = "https://uxdivers.com/"
},
Storage = new InMemoryStorage()
};
以下是使用这些自定义设置后消息的外观示例:

这个 连接选项 对象指定连接的到期时间和所需的命名空间,钱包必须支持这些原语才能建立连接。尽管 个人签名 方法是满足我们需求的唯一必备方法,我们在此示例中加入了其他方法,以更全面地说明潜在功能。
private ConnectOptions CreateConnectionOptions()
{
return new ConnectOptions()
{
Expiry = (long)new TimeSpan(0,10,0).TotalSeconds,
RequiredNamespaces = new RequiredNamespaces()
{
{
"eip155", new ProposedNamespace()
{
Methods = new[]
{
"eth_sendTransaction", "eth_signTransaction", "eth_sign", "personal_sign", "eth_signTypedData",
},
Chains = new[]
{
CHAIN_ID
},
Events = new[]
{
"chainChanged", "accountsChanged", "connect", "disconnect"
}
}
}
}
};
}
一旦我们创建了 SignClientOptions 和 连接选项 对象,我们可以初始化客户端,一个实例 WalletConnectSign 客户端。此对象管理应用程序和钱包之间的通信。初始化后,我们调用 Connect 方法来获取 连接数据 对象。在这种情况下,我们使用由提供的深度链接 连接数据 在设备上打开钱包应用程序。或者,你可以在屏幕上显示二维码,供用户用手机扫描。当用户在终端上进行交易并将钱包放在不同的设备上时,第二种方法更可取。在此示例中,我们使用深度链接通过以下方式打开应用程序 发射台,由 MAUI 提供的用于启动其他应用程序的课程。
最后, ConnectWallet 方法会等到钱包批准连接。如果用户不批准连接或连接超时,则会引发异常。
现在我们已经与钱包建立了连接,是时候提出登录请求了。这个 使用以太坊登录 函数负责此任务。它首先创建了一个实例 SIWE 消息 班级。
private async Task<(string, SiweMessage)> SignInWithEthereum(string address)
{
//Creates the SIWE message that will be signed by the user's wallet
var siweMessage = CreateSiweMessage();
//Create the request following the RPC format for 'personal_sign'
var request = new EthPersonalSign(siweMessage.ToString(), address);
//Launches the wallet app
Application.Current.Dispatcher.Dispatch(async () => await Launcher.Default.OpenAsync($"wc:"));
string signature;
try
{
//Send the sign request to the user's wallet with the siwe message
signature = await _dappClient.Request<EthPersonalSign, string>(_dappClient.AddressProvider.DefaultSession.Topic, request, CHAIN_ID, siweMessage.Expiry);
}
catch (Exception e)
{
signature = null;
siweMessage = null;
}
return (signature, siweMessage);
}
这个 SIWE 消息 类代表 “使用以太坊登录” 消息,该消息用于通过验证用户对特定以太坊地址的控制来对用户进行身份验证。该类包括各种属性,例如 域, 地址, 声明, 澳元 (观众), ChainID, 随机数, 到期, 版本,以及 Iat (当时发布)。这个 随机数 和 到期 参数通过防止重放攻击和确保签名消息仅在有限的时间内有效,在安全性中起着至关重要的作用。可以通过纳入额外的安全措施来改进该协议,例如指定签名有效性的允许时间窗口,并对签名进行更严格的验证 域 和 澳元 用于防止网络钓鱼攻击的字段。
public class SiweMessage
{
public string Domain { get; set; }
public string Address { get; set; }
public string Statement { get; set; }
public string Aud { get; set; }
public string ChainId { get; set; }
public string Nonce { get; set; }
public long? Expiry { get; set; }
public string Version => "1";
public string Iat { get; } = DateTime.Now.ToISOString();
}
下一步是定义请求。在这种情况下,我们将使用 'personal_sign' 方法。在 WalletConnect 文档中 以太坊 | WalletConnect 文档 这里总结了以太坊 RPC 方法以及如何发出请求以及每种方法的响应。使用 个人签名 由于其用户友好的格式和增强的安全性,“使用以太坊登录”(SIWE)消息的签名方法是业界的标准方法。这种方法允许用户签署清晰、可读的消息,从而降低意外滥用的风险,并明确表示他们没有签署交易。它在以太坊钱包中的广泛支持确保了兼容性和一致的用户体验。此外,添加的前缀通过明确区分签名消息和交易来帮助防止重放攻击。
如文档中所述,我们定义了 以太坊个人签名 学员要遵循的 RPC 个人签名 方法。该类在其构造函数中接收消息和用户地址作为参数。由于 JSON 转换器需要无参数的构造函数,因此添加了一个额外的构造函数来满足这一要求。
[RpcMethod("personal_sign"), RpcRequestOptions(Clock.ONE_MINUTE, 99998)]
public class EthPersonalSign : List<string>
{
public EthPersonalSign(string message, string address) : base(new List<string> { message, address })
{
}
public EthPersonalSign()
{
}
}
最后,该方法使用以下方法重新打开钱包 发射台 类并将请求发送到钱包。响应包含用户对消息的签名,该函数随后将其与原始 SIWE 消息成对返回。此信息将由 验证个人签名 用于验证签名的函数。
这个 验证个人签名 函数使用以下命令检查给定的以太坊签名是否由特定的以太坊地址创建 Nethereum https://nethereum.com/ 图书馆。它接受一条消息、一个签名和一个预期的地址作为输入。该函数使用 以太坊消息签名器 使用 UTF-8 对消息进行编码,并使用椭圆曲线恢复功能从签名中恢复签名的以太坊地址。然后,它以不区分大小写的方式将此恢复的地址与预期的地址进行比较。如果它们匹配,则该函数返回 真的,确认签名有效且由预期的地址所有者生成;否则,它将返回 假的,表示签名无效或来自其他地址。
public bool VerifyPersonalSignSignature(string message, string signature, string expectedAddress)
{
// Recover the address from the signature
var signer = new EthereumMessageSigner();
var recoveredAddress = signer.EncodeUTF8AndEcRecover(message, signature);
// Compare the recovered address with the expected address
return string.Equals(recoveredAddress, expectedAddress, StringComparison.OrdinalIgnoreCase);
}
验证签名后, onClicked 函数调用 SignedInCommand。此命令由用户通过可绑定属性定义,并接收的实例 经过身份验证的用户数据 类作为其命令参数。这个 经过身份验证的用户数据 类是自定义的,可以包含特定用例所需的任何信息。
//Verifies that the address that signs the SIWE message is the user's address.
var isValid = VerifyPersonalSignSignature(siweMessage.ToString(), signature, address);
if (isValid)
{
Debug.WriteLine("Success: User authenticated with Ethereum");
SignedInCommand?.Execute(new AutenticatedUserData
{
AuthenticationNonce = siweMessage.Nonce,
UserAddress = address,
WalletName = _dappClient.AddressProvider.DefaultSession.Peer.Metadata.Name,
UserPublicKey = _dappClient.AddressProvider.DefaultSession.Peer.PublicKey
});
return;
}
使用.NET MAUI 创建 SIWE 示例具有挑战性,因为可用文档有限 WalletConnectSharp 图书馆。此外,虽然有一个名为的包 WalletConnectsharp.auth 专为钱包身份验证而设计,大多数钱包都与该协议不兼容。结果,我们选择使用 个人签名 方法,钱包广泛支持该方法。但是,这种方法需要打开两次钱包应用程序,尽管这在 web3 应用程序中很常见,但对于不熟悉生态系统的用户来说可能会不方便。
该解决方案的行为也因平台而异。在 Android 上,当 Launcher 类打开深度链接时,操作系统会显示一个对话框,允许用户从可用钱包中进行选择。但是,在iOS上,此功能不存在,因此操作系统将打开默认的钱包应用程序,而无需询问用户他们更喜欢哪一个。

有一个名为 AppKit 的新 WalletConnect SDK (概述 | WalletConnect 文档),它提供了诸如仅通过一次交互即可请求钱包身份验证的功能。此 SDK 的文档全面且易于理解。但是,该软件开发工具包不适用于.NET MAUI,但适用于 iOS 和安卓系统。可以从这些平台特定的包中为 MAUI 创建库绑定。
未来的另一项潜在增强功能是通过创建更复杂的解决方案来扩展此解决方案 SIWE 消息 这提高了安全性。此外,nonce 行可用于将消息链接到会话并使用 JWT 令牌。有一个 SIWE 使用 Blazor 的示例,它集成了 JWT 和 Nethereum 库,可以在 GitHub 上找到 这里
我们成功地在.NET MAUI 中创建了一个示例应用程序,该应用程序仅使用 C# 库来管理与钱包应用程序的连接,不依赖任何 JavaScript 依赖关系。此应用程序允许用户使用SIWE协议使用钱包登录。与协议和连接相关的所有逻辑都封装在一个控件中,使其易于在其他解决方案中重复使用,您可以检查所有代码 这里
区块链是一项强大的技术,可以通过提供安全性、透明度和信任来增强软件解决方案。尽管区块链最近被人工智能的兴起所掩盖,但它仍然是一种有价值的工具。随着当前系统越来越多地与人工智能整合,也应考虑将区块链整合,因为它可以成为任何垂直业务的强大差异化因素。
在 UXDivers,我们致力于为所有.NET 技术提供卓越的用户界面/用户体验。整合区块链等创新工具只是我们帮助客户保持领先地位、实现安全、透明和面向未来的技术互动的一种方式。在我们继续探索尖端技术的同时,我们很高兴能够突破界限,提供既能满足当今需求又能开启未来可能性的解决方案。敬请关注,我们将深入研究新兴趋势,发现为您的项目带来更多价值的新方法!🚀