Sqlstone - 第二部分:每个用户都拥有数据库的个人副本:解决后端挑战(数据可用性/所有权)
Sqlstone 框架的增强功能已添加
GitHub - raddevus/sqlstone: C# .NET Core 8.x ASP.NET MVC,为每个注册用户创建一个新的 sqlite 数据库来存储他们自己的数据。为什么?为什么不呢?[^]
引言
您可以在 CP 上阅读本系列的第一篇文章:
Sqlstone:每个用户都拥有数据库的个人副本:解决后端挑战(数据可用性/所有权)[^]
本文涵盖内容:增强功能
在前一篇文章中,我承诺添加四项增强功能
- 用户删除单个日志条目 - 尚未完成
- 允许用户下载整个 Sqlite 数据库(以及用户如何在本地使用它) - 已完成
- 用户删除远程数据库和 UUID - 部分完成 - 本文将进行解释
- 添加 UUID 的 QR 码生成器,以便于在其他设备上获取 UUID 和加载数据 - 已完成
- 我还添加了一项额外的增强功能,用于验证 UUID 是否接近我期望的值。新代码要求 UUID 值 1) 包含 4 个连字符,2) 长度正好是 36 个字符(包括 4 个连字符)。我注意到有些用户保存的 UUID 值是“test1”或其他什么。这项新增强功能有助于保护文件系统。
开发主题
如果我要解释一个一致的软件开发主题,我可能会用这句话
匿名“理论上,实践和理论是相同的。但在实践中,它们并非如此。”
我可能会稍微修改一下
raddevus“在开发(本地主机)中,实践和理论是相同的。但在生产环境(公共 URL)中,它们并非如此。”
那是因为我尝试让某些功能在开发环境中工作起来很容易,但在我的生产环境中却表现不同。“我的服务器上可以运行!”
好了,我们现在就来一一介绍,但在此之前,让我先向您展示我做的最简单的增强功能,并看看它的作用。
从 UUID 生成二维码
为了让用户能够从任何地方访问她的数据,我添加了一种简单的方法,可以在不同设备之间传输 UUID。我添加了从 UUID 生成 QR 码的功能。
用户只需点击 UUID 字段旁边的条形码图标按钮,即可生成 QR 码。
是的,你可以进行黑客攻击
是的,你可以采取以下步骤写入我的 UUID 数据库
- 将你的设备摄像头对准本文中的图片
- 从 QR 码中获取 UUID
- 前往 https://newlibre.com/journal^
- 将 UUID 粘贴到 UUID 文本框中
- 点击 [Gen/Set UUID]
- 查看插入该数据库的条目
- 向该数据库副本添加新条目
- 下载此特定数据库的副本(稍后将详细介绍)
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]”按钮。
您会收到另一个弹出窗口(因为每个人都喜欢弹出窗口),告诉您操作是否成功。
查看源代码,试用一下,并告诉我您的想法。