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

Sqlstone - 第二部分:每个用户都拥有数据库的个人副本:解决后端挑战(数据可用性/所有权)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2024 年 6 月 24 日

CPOL

7分钟阅读

viewsIcon

9851

downloadIcon

252

Sqlstone 框架的增强功能已添加

GitHub - raddevus/sqlstone: C# .NET Core 8.x ASP.NET MVC,为每个注册用户创建一个新的 sqlite 数据库来存储他们自己的数据。为什么?为什么不呢?[^]

引言

您可以在 CP 上阅读本系列的第一篇文章:

Sqlstone:每个用户都拥有数据库的个人副本:解决后端挑战(数据可用性/所有权)[^]

本文涵盖内容:增强功能

在前一篇文章中,我承诺添加四项增强功能

  1. 用户删除单个日志条目 - 尚未完成
  2. 允许用户下载整个 Sqlite 数据库(以及用户如何在本地使用它) - 已完成
  3. 用户删除远程数据库和 UUID - 部分完成 - 本文将进行解释
  4. 添加 UUID 的 QR 码生成器,以便于在其他设备上获取 UUID 和加载数据 - 已完成
  5. 我还添加了一项额外的增强功能,用于验证 UUID 是否接近我期望的值。新代码要求 UUID 值 1) 包含 4 个连字符,2) 长度正好是 36 个字符(包括 4 个连字符)。我注意到有些用户保存的 UUID 值是“test1”或其他什么。这项新增强功能有助于保护文件系统。

开发主题

如果我要解释一个一致的软件开发主题,我可能会用这句话

 

匿名
“理论上,实践和理论是相同的。但在实践中,它们并非如此。”

 

我可能会稍微修改一下

raddevus
“在开发(本地主机)中,实践和理论是相同的。但在生产环境(公共 URL)中,它们并非如此。”

那是因为我尝试让某些功能在开发环境中工作起来很容易,但在我的生产环境中却表现不同。“我的服务器上可以运行!”

好了,我们现在就来一一介绍,但在此之前,让我先向您展示我做的最简单的增强功能,并看看它的作用。

从 UUID 生成二维码

为了让用户能够从任何地方访问她的数据,我添加了一种简单的方法,可以在不同设备之间传输 UUID。我添加了从 UUID 生成 QR 码的功能。

用户只需点击 UUID 字段旁边的条形码图标按钮,即可生成 QR 码。

是的,你可以进行黑客攻击

是的,你可以采取以下步骤写入我的 UUID 数据库

  1.  将你的设备摄像头对准本文中的图片
  2. 从 QR 码中获取 UUID
  3. 前往 https://newlibre.com/journal^
  4. 将 UUID 粘贴到 UUID 文本框中
  5. 点击 [Gen/Set UUID]
  6. 查看插入该数据库的条目
  7. 向该数据库副本添加新条目
  8. 下载此特定数据库的副本(稍后将详细介绍)

UUID:“快速登录”

由于没有人会猜测 UUID,我只需使用 UUID 登录用户。

  • 是的,如果有人攻击我的网站,他们将看到所有的 UUID 文件夹,并能够访问任何 Sqlite 数据库。
  • 是的,这并不算安全。
  • 是的,这是一个原型,我稍后将向您展示如何解决这个问题。

好的,让我们下载用户数据库。

下载用户数据库

用户只需点击页面上的链接即可下载其 Sqlite 数据库的副本。

它将下载到您的 Web 浏览器下载目录中。就是这么简单。但是,其背后的 WebAPI (C#) 和 JavaScript 并非完全简单,所以让我们来看看。

这是您可以在 `UserController` 类文件中找到的代码。

[HttpPost]
    public ActionResult DownloadSqliteDb([FromQuery] String uuid)
    {
        var userDir = Path.Combine(webRootPath,uuid);
        var journalDb = Path.Combine(userDir,templateDbFile);
        Console.WriteLine(journalDb);
        return new PhysicalFileResult(journalDb, "application/x-sqlite3");
    }

用户必须提供 UUID 来识别她试图下载哪个数据库。

然后,我们只需创建指向 sqlite 数据库的用户文件空间路径(还记得从第一篇文章模板 Sqlite 数据库名为 `sqlstone_journal.db`)。

然后我发现你必须使用 `PhysicalFileResult()` 来正确返回文件的字节。如果你尝试返回 `FileResult()`,将会发生一件非常奇怪的事情。这件事就发生在我身上,我差点咬掉自己的胳膊来修复这个问题。然后我终于发现了一些关于它的文档,并在我的博客上进行了总结^

您需要 JavaScript 来处理 PhysicalFileResult

当文件返回时,我决定想用一个包含简单时间戳的名称来保存它,这样下载的浏览器就不会只将其命名为 `file(1).db`、`file(2).db` 等。
另外,这意味着如果您有多个 UUID 日志,可以下载它们而不会覆盖它们。

这是我们在 `main.cs` 中使用的 JavaScript fetch 调用。

function downloadSqliteDb(){
    console.log("1");
    if (currentUuid != null){
        fetch(`${baseUrl}User/DownloadSqliteDb?uuid=${currentUuid}`,{
                method: 'POST',
            })
            .then(resp => {console.log(resp); return resp.blob();})
            .then(blob => {
                downloadFile(blob,`journal-${GetFileTimeFormat(new Date())}.db`);
            });
    }
    else{
        
        uuidRegisterAlert("You need to register your UUID to be able to download your sqlite data.\Please register your UUID & try again.",true);
    }

}

正如您所见,我还调用了另一个 JS 函数 `GetFileTimeFormat()`,我用它来为文件命名带有时间戳。我将让您在源代码中查看该代码。

有趣的代码是 `downloadFile()` JavaScript。它很奇怪,但它有效。

它临时添加了一个子节点 `a href` 标签,然后将其删除。

function downloadFile(blob, name = "journal.db") {
    const href = URL.createObjectURL(blob);
    const a = Object.assign(document.createElement("a"), {
      href,
      style: "display:none",
      download: name,
    });
    document.body.appendChild(a);
    a.click();
    URL.revokeObjectURL(href);
    a.remove();
}

疯狂的东西。

这里还有最后一个功能要讲。我想让用户能够销毁她的账户。这样,她就拥有了自己的数据。

销毁账户及相关 Sqlite DB

我有什么资格评判?如果您想摆脱您的日志数据,我很乐意让您这样做。

回到 `UserController.cs` 中的源代码,看看 `DestroyUserAccount()`。

 [HttpPost]
    public ActionResult DestroyUserAccount([FromQuery] String uuid){
        // Every valid UUID will have 4 hyphens & be 36 chars long
        int hyphenCounter = uuid.AsSpan().Count('-');
        if (uuid.Length != 36 || hyphenCounter != 4){
            return new JsonResult(new {result=false,message="Your uuid doesn't look like a valid value.\nCannot delete account."});
        }
        var userDir = Path.Combine(webRootPath,uuid);
        try{
            Directory.Delete(userDir,true);
        }
        catch(Exception ex){
            return new JsonResult(new {result=false,message=$"Error! Delete Failed. \nCannot delete account. {ex.Message}"});
        }
        return new JsonResult(new {result=true,message="The user account and all associated data has been destroyed."});
    }

输入 UUID,在进行一些检查以确保它看起来像一个有效的 ID 后,WebAPI 将调用 `Directory.Delete()` 并将 `true` 参数(表示它应该销毁内部的所有文件夹和文件)。

UUID 验证

我发现我需要这样做,否则一些聪明人会把他们的 UUID 设置成 `css` 或其他文件夹名,然后删除我的 Web 文件夹。哈哈!那会不会很有趣?绝对不会!

所以我只检查 UUID 是否有 4 个连字符,并且长度正好是 36 个字符。如果其中任何一个测试失败,我就会拒绝用户的请求。我拒绝你和你所拥有的一切!!😁

这是 JavaScript 中与调用 `DestroyUserAccount` 端点相关的代码。

function destroyAccount(){
    
    fetch(`${baseUrl}User/DestroyUserAccount?uuid=${currentUuid}`,{
        method: 'POST',
    })
    .then(response => response.json())
    .then(data => {
       if (data.result == false){
            alert(`Could not destroy the account. \nError -> ${data.message}`);
            return;
       }
       uuidRegisterAlert("The UUUID, User Account & all associated data was permanently deleted.");
       localStorage.removeItem("currentUuid");
       currentUuid = null;
       document.querySelector("#uuid").value = "";
       window.location.reload();
    });
    document.querySelector("#modalCloseBtn").click();
}

删除账户的问题

这一切在我的本地主机上运行得很好。删除每次都成功了。

但是当我将代码移到我的生产服务器(https://newlibre.com/journal)时,我开始收到 500 错误。

我完全不知所措,直到我在 API 调用中放入了 try/catch。一旦我这样做了,我发现删除 Sqlite DB 时出现了一个错误,因为服务认为 Sqlite DB 有一个打开的连接。由于它认为文件正在被使用,它不允许我删除 Sqlite db 并移除目录。

兔子洞:EF Core、Sqlite DB 连接等

现在我将深入兔子洞,试图找到一种方法来强制应用程序关闭连接,但这涉及大量技术,可能很难找出是谁一直占用着连接。

唉,这就是软件开发者的生活。

如何启动账户删除?

点击垃圾桶图标(下图高亮显示),会弹出一个警告对话框。
如果您想销毁账户,请点击“[Destroy Account]”按钮。
您会收到另一个弹出窗口(因为每个人都喜欢弹出窗口),告诉您操作是否成功。

查看源代码,试用一下,并告诉我您的想法。

 

 

© . All rights reserved.