65.9K
CodeProject 正在变化。 阅读更多。
Home

为何选择 HTML5?为何不选?UI/UX 与存储挑战(N 系列第 3 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2022 年 1 月 6 日

CPOL

19分钟阅读

viewsIcon

8638

downloadIcon

132

包括完整的C# Web API,允许您通过简单的方法发布应用程序数据。

阅读本系列之前的文章

引言

我做了相当多的研究,试图找到一个能满足我们这个小应用需求的服務,但我真的空手而归。是的,也许外面有解决方案,但

  1. 它们太贵了——我是个极度节俭的人。我看到一个服务,最低要求购买 50TB(是的,太字节)存储。每月超过 3,000 美元。
  2. 它们太难理解了——我只是想把数据发布到一个 URL (web api),然后让数据为我保存。
  3. 如果我理解了服务的工作原理,我也不明白它会花我多少钱。我以前注册过 AWS 服务,它失控了,在我注意到之前就产生了 100 美元的费用。我真的不想那样。
  4. 我真正想要的是 SaaS(软件即服务)数据存储——为什么我不能拥有它?为什么不能便宜点?为什么它如此困难?

最后一个问题有点天真,不是吗?但有时,你希望保持天真。

让远程数据存储变得简单会面临一些挑战(安全、身份/认证、保护服务等),但如果我们忽略这些挑战,也许我们能有所进展。

让我们试试!

项目约束

主要的项目约束之一是,我们必须能够用……等等……

这个限制仅仅是因为在客户端,由于我们正在使用 HTML5 技术构建这个应用程序,这是我们可用的语言。

我知道 JavaScript 对很多开发者来说就像是肚子上的一拳。这意味着很多开发者甚至不会阅读这篇文章,但再次强调,我正在尝试实现“一次编写,随处运行”的白日梦,所以请大家耐心等待。我用 C/C++ 学习编程,然后在大约 2000 年转向 C#,所以我理解对 JavaScript 的鄙视,但这些年来发生了很多变化。

此外,我喜欢为工作选择合适的工具,在这种情况下,JavaScript 似乎就是我们需要的工具。

进入 .NET 和 C#

当然,我们在这篇文章中要解决的问题是能够在服务器端保存数据(在使用客户端/JavaScript 发布数据之后)。

在我看来,构建服务器端 WebAPI 最简单的方法是使用 .NET Core 并用 C# 编写。

WebAPI 的主要目的

在本文中,我们将构建一个简单的 WebAPI,它将允许开发人员

  1. 将其用户的数据发布到 WebAPI 端点
  2. 将其存储(在服务器上)
  3. 以便她以后可以再次为用户检索数据

警钟敲响!

阅读了这些基本要求后,你脑海中可能警钟大作。

你可能会对这篇文章大喊,例如:

“用户数据将被泄露!”

“你不能那样明文保存数据!你太荒谬了!”

我将在本文中提供一个解决方案,希望你会觉得它很有趣。

剧透警告

这算是剧透,但我们会这样做:AES256 加密

所有发布到服务器的数据都将通过 AES256 加密在客户端进行加密。

更多细节我们稍后会讲。

设置 WebAPI 项目

请记住,我正在向您展示 WebAPI 将如何设计和构建,但最终用户开发人员无需了解这些细节。

目标:JavaScript 开发者

我们正在尝试找到一个解决方案,任何 JavaScript 开发者都可以使用它安全地发布他们的应用程序数据,而只需了解客户端开发。我们希望让 WebAPI 尽可能简单(但不能过于简单),以便 JavaScript 开发者只需在特定端点(URL)上调用 fetch API 来保存用户数据,然后在另一个端点上调用 fetch 来检索数据。就是这样。

我的工作方式

几年前我转向了 Linux,所以我所有的开发都是使用 Visual Studio Code (VSC) 完成的。

但是,您会看到我们将创建一个完整的 Visual Studio 解决方案,所以如果您正在使用完整版的 Visual Studio,您应该能轻松跟随。

当然,您也可以直接下载已完成的解决方案,并在 Visual Studio 或 VSC 中打开它,所以一切都会很好。

创建带有单元测试的项目

我还想在项目中包含单元测试,因为它能让我更容易地测试 WebAPI 方法和我的领域模型。没什么花哨的,只是能够运行一个方法而无需做大量的设置工作,而是让 Xunit 运行器为我完成。

因为我想包含单元测试,我发现创建一个良好、干净的 .NET WebAPI 项目有点麻烦,但我已经解决了细节问题,以便我们能获得一个漂亮的、干净的目录结构,如下所示:

我所有的项目都在一个名为 dotnet 的外部文件夹中。

项目的目标文件夹名为 `MainProject`,并将包含两个主要的子文件夹

  1. MainProject -- 包含 WebAPI 代码
  2. MainProject.Tests -- 包含项目的单元测试。
  3. MainProject.sln 将引用这两个项目。

这使得创建引用我们 WebAPI 中的模型和控制器,并将所有内容封装在一个名为 MainProject 的外部文件夹中的单元测试变得非常容易。

我的项目命名:LibreStore

现在,让我们看看这在我的 WebAPI 项目中将如何运作,我将其命名为 LibreStore
我将其命名为 LibreStore,因为 Libre 在西班牙语中意为“自由”,而这个项目是尝试创造存储自由。我还有网站域名 NewLibre.com,所以最终它将成为该域名下的一个不错的项目。

实现项目结构

为了实现该项目结构,我们将运行以下 dotnet 命令

请注意:我假设如果您想跟着做,您的机器上已安装 .NET Core 6.x 并已设置好。更多信息请参阅 官方 .NET Core 安装文档

以下是创建我们初始项目并拥有之前所示结构的命令:

command 解释
$ mkdir LibreStore 创建外部文件夹
$ cd LibreStore 进入文件夹
$ dotnet new mvc -o LibreStore 创建 MVC 项目(带 MVC 控制器的 WebAPI)
$ dotnet new sln 创建新的解决方案文件
$ dotnet sln add LibreStore 将主项目添加到 SLN 文件。
$ dotnet new xunit -o LibreStore.Tests 创建新的 XUnit 测试项目及所有依赖项
$ dotnet sln add LibreStore.Tests 将单元测试项目添加到 SLN 文件。
$ dotnet add LibreStore.Tests reference LibreStore 将主项目 (LibreStore.dll) 的引用添加到 XUnit 测试项目,以便可以使用它。
$ cd LibreStore.Tests 进入测试项目文件夹
$ dotnet test 运行单元测试

如果您遵循了所有这些步骤并且一切顺利,那么您应该会看到类似以下的内容,显示 1 个测试已运行并成功。

如果我们决定运行主要目标(我们的 LibreStore MVC / WebAPI),那么我们将进入 LibreStore 项目文件夹并运行以下命令

$ donet run

这会构建 WebAPI 项目并在特定端口(您的可能不同)启动 Web 服务器。

启动后,您可以使用网络浏览器导航到该站点。

您的浏览器可能会发出警告,因为 Web 服务器使用的证书未签名。

如果一切顺利,您应该会看到

那是 HomeController 将 Home View 返回给浏览器。

收集我们最基本的需求

这些都很好,但我们只是想达到这样的程度:

  1. 发布数据
  2. 数据保存在服务器上
  3. 检索数据

这引出了一些要求

  1. 需要一个接受发布数据的方法——一个 URL
  2. 需要一个“桶”来存储发布的数据
  3. 需要一种方法来标识开发人员用户正在将数据保存到的“桶”

思考了一会儿

我正在努力让这篇文章简短,所以您可能会发现一些细节缺失了。我考虑了我的下一步,下一步是创建一个类,它将帮助我们保存数据。我将这个类命名为 MainToken

MainToken 类

我之所以这样称呼它,是因为您只需要这个基于字符串的键就可以将数据存储在远程数据存储中。

这是这个类

public class MainToken{
    public int Id{get;set;}
    public int OwnerId{get;set;}
    public String Key{get; set;}
    public DateTime Created {get;set;}
    public bool Active{get;set;}

    public MainToken(String key, int ownerId=0)
    {
        OwnerId = ownerId;
        Key = key;
        Created = DateTime.Now;
        Active = true;
    }
}

它只是一个带有几个属性和构造函数的类。

为了刻意保持简单,我只要求用户提供一个 MainToken.Key 值,该值将作为其 GUID,用于将其数据存储在远程存储中。

此字段必须是唯一的。稍后,我还会要求它至少有 10 个字符长——旨在确保它至少在某种程度上难以猜测。

安全并非源于模糊性

然而,LibreStore 的安全性不会来自模糊性。相反,它将始终确保数据是密码加密的 AES256 数据。

然而,如果有人猜到了用户的 Key,他们就能检索到她的数据。当然,数据将经过 AES256 加密,所以它_应该_无关紧要。除非有某种未知的破解方法击败了 AES256——在撰写本文时尚未发现。

数据可能被他人添加

此外,如果 Key 被猜到,那么其他人可能会恶意地使用该键插入数据,而该键的真正所有者可能会拥有他们不想要的数据。

重点:创建强大的密钥

创建一个强大的 GUID Key 在这里非常重要,这样你就可以确保只有你能向你的 LibreStore 发布数据。

这是一个新颖的想法

我知道在这一点上,许多读者可能会对这个想法感到犹豫。这个想法是,你所需要的就是

  1. GUID MainToken.Key
  2. 加密数据
  3. LibreStore 端点 (URL)

然后,你就能发布任何人都可以检索的数据。

当你发布这些数据时,它会将你的 GUID Key 作为数据表中的数据键进行存储。

如果用户向数据表发布的数据中包含一个从未创建过的 MainToken.Key,那么该键将在那时创建。

数据将如何存储?

我们还没有讨论实际数据将如何存储?它可以存储在服务器上的一个或多个文本文件中,也可以存储在数据库中。在我们的案例中,我们将从介于这两个选项之间的方式开始。

SQLite

我们将使用基于文本的 SQLite 数据库。

这为我们提供了一种简单的方法来创建数据库和轻松地开始插入数据。

我将把 SQLite 调用封装在某种数据访问包装器中,以便将来当我们想要将实现切换到 SQL Server 或 MySQL 数据库时,我们可以在不影响用户的情况下完成。

为什么选择 Sqlite?

Sqlite 数据库的酷炫之处 - 如果你获取代码,构建并运行它,你会发现第一次发布数据时会创建一个名为 librestore.db 的文件。这就是数据库,它是由 SqliteProvider 类中的 DDL(数据定义语言)代码创建的。这很酷,因为你不需要安装任何数据库服务器。

我简直不敢相信 Sqlite 有多棒,以及它有多容易使用。撰写这篇文章并真正深入研究 Sqlite 让我大开眼界,认识到这个小数据库是多么伟大的技术。它是一个强大而使用起来极其简单的系统。如需了解更多关于这个伟大项目(FOSS 完全开源软件)的信息,请访问:https://sqlite.ac.cn/

关于本文:我一路上兴高采烈

几天前我写了这篇文章的上一部分,然后我踏上了创建 ASP.NET Core 6.x MVC / WebAPI 的旅程,它将从 sqlite 数据库写入/读取数据。

我现在已经成功完成了任务。

已完成的 WebAPI,但我对设计有所放任

让一切按我想要的方式工作花费了相当大的努力,而设计也因我想要实现一个可用的原型而受到影响。我只是想看看它是否能工作。

与其详细描述我为创建初始 Web API 所做的所有事情,不如尽可能多地列出我遇到的挑战,并提供一些关于这些挑战细节的描述。

我还会谈谈我想要实现什么,以及这似乎如何改变了设计和代码。

数据库快照

以下是目前构成 Sqlite 数据库的三个表的快速概览。

三张表

  1. MainToken - MainToken.Key 是你将用作系统主标识符的密钥(稍后详述)。
  2. Bucket - 是的,这张表可以在 Data 字段中存储任何东西(最大 8,000 字节)。但是,你需要你的主标识符才能将它们取回。当然,你将对数据进行 AES256 加密。
  3. Usage - 此表允许我跟踪 IPAddressesActions (GetData, SaveData 等),以便我了解我的服务如何被使用以及是否受到攻击。我想将来可能不得不禁用一些 IP。不过,其中没有其他可识别的信息。

您还可以看到我在每个表中都有一个 Active 字段。此字段将允许我将 MainToken 设置为非活动状态,这样它就不能再被使用——以防有人攻击我或发生其他情况。

如何使用 Web API

使用 Web API 就是这么简单。

它托管在哪里?

https://newlibre.com/LibreStore[^]

那是我的网站,它已启用 HTTPS,因此您发布的所有内容都受到保护。

这意味着当您的 MainToken.Key 在 URL 中传递时,它不会被泄露。

两个帮助您存储应用程序数据的端点

目前只有两个主要端点可供您使用。

保存数据 (SaveData)

要保存您的数据,您只需

  1. 创建一个 (字符串) 密钥,长度至少为 10 字节且小于等于 128 字节。将其存储在一个您不会丢失的地方,因为没有它,您将无法找回您的数据。
  2. 使用以下 URL 发送您的数据(目前,我只将其设置为接受 HTTP Get 命令):https://newlibre.com/LibreStore/Data/savedata?key=<your-key-here>&data=<your-data-here>

最简单的尝试方法是使用 JavaScript fetch API (使用 Fetch - Web API | MDN[^])。

在您的浏览器开发者控制台中尝试一下

以下是您如何使用浏览器开发者控制台发送数据的方法。

  1. 打开你的浏览器开发者控制台(在大多数网络浏览器中按 F12)。
  2. 粘贴以下代码并修改以包含您的 Key 和 Data。
fetch("https://newlibre.com/LibreStore/Data/
       SaveData?key=FirstOneForTest&data=First post to data for test.")
  .then(response => response.json())
  .then(data => console.log(data));

我已使用 Key 发布,这意味着 MainToken 记录已创建。

如果您使用以前用过的 Key 发布,那只是意味着数据将绑定到该 Key

调用 SaveData 时会发生什么?

当您使用上面的 fetch 和您自己的密钥发送数据时,会发生以下情况:

  1. MainToken 表中将创建一个新的唯一条目(行)。
  2. 数据将被插入到 Bucket 表的 Data 字段中,并与创建 Key 时生成的 MainToken.ID 绑定。

至此,您的 Bucket 数据已存储。

返回了什么?

完成后,API 返回

包含两个字段的 JSON

  1. success: (truefalse)
  2. bucketId: 刚刚插入到 bucket 表中的行的 id

它看起来像下面这样

如果您将其保存到一个对象中,那么您将拥有一个具有这两个属性(successbucketId)的对象,并且您将能够重复使用这些值。

GetData

现在,当您想要检索数据时,您将发送到以下 URL:
https://newlibre.com/LibreStore/Data/GetData?key=<your-key>&bucketId=<your-bucket-id>

同样,您可以使用 Fetch API 来获取数据。

使用您保存数据时返回的 bucketId

fetch("https://newlibre.com/LibreStore/Data/GetData?key=FirstOneForTest&bucketid=2")
  .then(response => response.json())
  .then(data => console.log(data));

在浏览器控制台中,它看起来像下面这样:

使用 LibreStore 有两条主要规则

  1. 绝不能将您的 MainToken.Key 泄露出去 - 我在上面的例子中已经违反了这一条。这意味着任何人现在都可以尝试使用我的 Key 来检索数据。当然,当我保存我的真实数据时,我会创建一个长随机 Key 来存储我的数据。
  2. 切勿存储未加密的数据 - 我也打破了这条规则,以便向您展示它是如何工作的,但将来我会使用 AES256 加密所有数据。如果您正确加密数据,那么您实际上不必担心规则 #1,因为攻击者应该无法解密您的数据。

您现在可以使用 LibreStore 吗?

是的,你可以向它发布和检索数据,但我还没有告诉你如何轻松加密你的数据,所以目前你(显然)不应该发布任何真正重要的东西。

下一篇文章:通过 AES256 加密数据

既然我只是想让 Web API 和这篇文章面世,并为了使这篇文章更短,我将撰写一篇关于如何通过 JavaScript 使用 AES256 加密算法加密数据的文章。

抢先看:AES256 加密

然而,既然我已经完成了研究并弄清楚了如何通过 JavaScript 使用 AES256 加密和解密数据,如果您愿意,可以查看我在 Codepen.io 上运行的代码并试用一下:https://codepen.io/raddevus/pen/VwMXawY[^]。

尝试一下

  1. 输入您的密码
  2. 输入您的数据
  3. 点击 [加密] 按钮

您将看到一串 Base64 编码的字节出现。那是将加密的字节转换为 Base64 数据。

它不是明文字节。它是转换为 Base64 的密文字节。

如果您点击解密按钮,数据将

  1. Base64 解码
  2. 使用密码解密
  3. 明文将添加到底部的 div 中,以便您可以看到它——它将与原始文本匹配。

如果您在解密前更改密码,则无法解密字节,您将看不到任何内容。

我必须学习、研究、开发的事项清单

Sqlite

  • 如何创建数据库
  • 如何获取表中最后插入的行(SQL Server 中是 @@IDENTITY,但在 Sqlite 中您使用 SELECT last_insert_rowid()
  • 如何通过 ASP.NET Core Data Provider 连接和查询——通过 nuget 添加 $ dotnet add package sqlite
  •  

ASP.NET Core

  • 如何绕过 CORS (跨域资源共享),以便任何人都可以从任何其他站点访问 Web API(见下面的代码快照)。AllowAnyOrigin() 解决了这个问题,但这感觉很危险,不是吗?
  • 如何在我的(有限的)托管站点上构建和部署 ASP.NET Core——我无法完全控制 Web 服务器,并且要让事情正常运行可能很困难,但我已经弄清楚了在 web.config 中为主机需要做什么,以及如何启动应用程序。
  • 我还必须弄清楚如何将其构建为一个“单一可执行文件”并部署到托管网站:LibreStore.dllLibreStore.exe,它们在 Web 服务器中运行。相当大的挑战。

在您自己的网络上运行

当然,如果您获取代码并在您自己的网站上运行,那么您可以使用 CORS 限制数据的访问位置,并使其只有您的 Web 应用程序才能读取/写入您的本地数据库。

这就是我们创造简单的方式

请记住,这个完整的想法是一个新颖的想法,旨在作为一种方式

最初目的

让网页开发者轻松地在他们的应用程序中保存和检索数据

幕后所有工作(构建 Web API 来实现此功能)并非易事,但现在 Web App 开发人员确实有一种通过 JavaScript Fetch 轻松保存数据的方法。

下次再见

下次,我们将更新我们的 ImageCat 应用,将其数据存储在 LibreStore 中,以便应用运行时可以随时检索。

我能想到我们将会遇到的一些挑战,但这些只会让我们学到更多。👍🏽

添加查看使用统计信息的功能

我们还将添加查看使用统计信息的功能,以便我们可以密切关注 Web API 的使用情况,并确保其不被滥用。

稍后,我将添加 Owner 表,以便人们可以通过电子邮件地址注册他们的 MainTokens 的使用,这将防止他们的数据被删除。将来,我将不得不确保未被使用的数据(来自只是试用服务的人)将定期删除。随着我们将其转变为更真实的 SaaS(软件即服务),这方面的内容会更多。

关注点

我一直想在兴趣点中加入一些内容,但直到现在才真正有太多可补充的。

我的密钥是表情符号

在测试过程中,我发现可以使用由表情符号组成的 MainToken.Key。这非常酷。

我对开发环境运行了一个 Fetch,如下所示:

fetch("https://:7138/Data/SaveData?key=🤬🤖🤢🤫😊🥴😨🤭😁🤓&data=
       I'm trying to save emojis, but???")
  .then(response => response.json())
  .then(data => console.log(data));

它返回正常,我对本地 sqlite 数据库的 MainToken 表进行了 select 操作,看到了以下内容:

接下来,我执行了一个 Fetch 调用到 GetData,看到了以下内容:

我成功地使用表情符号作为字符串密钥,然后使用相同的密钥进行了检索。

这让我笑了。

关于代码的注意事项

  • 本文中我没有展示太多代码,因为我试图让文章更短,而且代码相当基础。
  • DataController 基本上是所有事情的开始和结束。它是一个大型控制器,只是让我把这个东西运行起来。
  • 我希望它有一个更好的设计,并且正在朝着一个更好的 DAL(数据访问层)发展,但最终,它有点丑陋。但是,它能工作。而且,它足够容易扩展,所以我对此感到满意。
  • 您会在 DataController 中找到几个我没有提到的额外方法。其中一个名为 GetAllTokens() 的方法只有在用户提供正确密码(将与 SHA256 哈希匹配)时才能运行。

历史

  • 2022 年 1 月 6 日:首次发布

注意:原始的首图是来自 Pixabay 的免版税图片(这里)。

© . All rights reserved.