如何保护Azure Cosmos DB中的用户数据

166 阅读12分钟

Cosmos DB是微软的一个云数据库产品,提供可扩展和高性能的服务。核心产品运行在一个专有的NoSQL数据库上,对于有经验的MongoDB开发者来说应该是很熟悉的。除了核心的Cosmos DB API之外,微软还提供了几个API。这些包括用于以下方面的API。

  • SQL
  • MongoDB
  • Gremlin
  • 卡桑德拉

向无服务器数据库操作的转变是迁移的最明显优势之一。Cosmos DB可以根据负载自动扩展你的吞吐量,而不是传统的提前配置吞吐量。

作为开发者,我们需要注意如何保护休息时的数据。在这篇文章中,您将了解到Cosmos DB如何帮助保护静态数据,以及您可以做什么来从您打算存储在Cosmos DB的数据中删除敏感数据,如个人身份信息(PII)。

PII的安全应用

我在开发领域的第一份工作是编写应用程序,对来自客户的个人数据执行scrubbing 。实际上,我们将收到包括个人姓名、社会安全号码和地址的数据。然后,这些数据将被发送到消费者报告公司进行验证,该公司将返回一个结果,让我们知道该地址是否是最新的,以及我们收到的信息是否正确。一旦数据返回,我们可以将结果导入我们的软件,如果刷新成功,那么该账户就是一个有效的联系人。

在本教程中,你将完成同样的过程。你将在.NET 6上建立一个ASP.NET Core网络应用程序,接受一个包含人名和社保号码的CSV文件。你将把它发送到一个假的刷卡服务。然后你将在一个Cosmos DB账户中记录结果。但你的老板说,你不能存储社会安全号码。因此,你将学习如何在存储到Cosmos DB之前从你的数据模型中删除该信息。

要继续下去,你将需要。

当然,如果你只是想看看代码,可以在Github上查看

Cosmos DB概述

像大多数平台即服务(PaaS)产品一样,Cosmos DB为安全和性能提供了许多功能。这里有太多的功能可以列出。在微软无尽的迷宫般的文档中寻找文档,已经很有挑战性。但我确实想谈一谈几个亮点。

首先,作为一个PaaS,微软对网络控制、主机基础设施和物理安全负责。他们与你分享应用程序级别的控制和身份访问,以及数据分类。端点保护是你的责任。

但需要注意的是,Azure是符合HIPPA的。归根结底,你选择存储的数据是你的责任,并受到法律和法规的约束,这不在本文的讨论范围之内。

你也应该花点时间熟悉一下Cosmos DB的资源模型。我不会在这里深入探讨,但简短的说,一个database account 拥有一个databasedatabase 包含containersContainers 可以包含itemsstored procedurestriggers ,和其他对象。在本教程中,你将在Azure门户上创建数据库账户,但你将从你的Web应用中创建数据库、容器和项目。

最后我想说的是,不同的Cosmos DB API有不同的方法来删除或加密数据。在本教程中,你将使用核心的Cosmos DB API,但我鼓励你去探索其他的API。

创建你的Okta应用程序

在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register ,以注册一个新账户。如果您已经有一个账户,运行okta login 。然后,运行okta apps create 。选择默认的应用程序名称,或根据你的需要进行更改。 选择Web,然后按回车键

选择ASP.NET Core。 然后,将重定向URI改为http://localhost:5001/authorization-code/callback ,并使用http://localhost:5001/signout/callback ,作为注销重定向URI。

请注意,TCP 5001端口必须与应用程序使用的相同。你可以在终端显示的信息中看到它,当你用以下命令启动应用程序时 dotnet run.

Okta CLI是做什么的?

Okta CLI将在您的Okta机构中创建一个OIDC网络应用。它将添加您指定的重定向URI,并授予Everyone组的访问权限。当它完成后,您会看到如下输出。

Okta application configuration has been written to: /path/to/app/.okta.env

运行cat .okta.env (或Windows上的type .okta.env ),查看你的应用程序的发行者和凭证。

export OKTA_OAUTH2_ISSUER="https://dev-133337.okta.com/oauth2/default"
export OKTA_OAUTH2_CLIENT_ID="0oab8eb55Kb9jdMIr5d6"
export OKTA_OAUTH2_CLIENT_SECRET="NEVER-SHOW-SECRETS"

您的Okta域是发行者的第一部分,在/oauth2/default

注意:你也可以使用Okta管理控制台来创建你的应用程序。更多信息请参见创建一个ASP.NET Core应用程序

创建一个Cosmos DB账户

正如我之前提到的,创建你的数据库、容器和项目的逻辑将在你的应用程序中。然而,你确实需要创建一个数据库账户来连接。

导航到Azure门户,选择创建一个资源。搜索Azure Cosmos DB ,并选择该选项。按照提示,在Azure Cosmos DB营销页面上选择创建

接下来,你会看到一个页面,询问哪个API最适合你的工作负载。找到核心(SQL)-推荐,然后按创建

创建你的网络应用程序

现在你可以把你的注意力转向创建你的网络应用程序。打开Visual Studio,按创建一个新项目。找到ASP.NET Core Web App(模型-视图-控制器)的模板,并按下一步。将你的应用程序命名为Okta_CosmosDb ,并按下一步。最后,从你的框架中选择**.NET 6.0(长期支持),然后按创建**。让Visual Studio花点时间来构建项目。

接下来,你可以在你的项目中安装你需要的两个包。

Install-Package Okta.AspNetCore -Version 4.1.0
Install-Package Microsoft.Azure.Cosmos -Version 3.26.1

Okta.AspNetCore 启用Okta,将使用Okta提供的中间件完成连接您的网络应用程序和Okta的所有繁重工作。这个包只需要使用先前设置的应用程序输出到 的值进行快速配置。.okta.env

Microsoft.Azure.Cosmos 提供了用于访问Cosmos DB API的核心库。你将使用这个包来创建你的数据库,为它添加一个容器,并在容器中插入项目。

接下来,打开appsettings.Development.json ,把那里的代码替换成以下内容。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Okta": {
    "ClientId": "{yourClientId}",
    "Domain": "{yourOktaDomain}",
    "ClientSecret": "{yourClientSecret}"
  },
  "CosmosDb": {
    "Account": "{yourCosmosDbUri}",
    "Key": "{yourCosmosDbPrimaryKey}",
    "DatabaseName": "oktacosmos",
    "ContainerName": "Results"
  }
}

你可以在.okta.env ,当你初始化你的Okta应用程序时,CLI产生的Okta值。要找到你的Cosmos值,请导航到你的Cosmos DB账户页面并打开Settings > Keys 标签。在这里你会找到URI,PRIMARY KEY, 和其他你可能需要的值。

接下来,将Program.cs 中的代码替换为以下内容。

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Okta.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
})
.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOktaMvc(new OktaMvcOptions
{
    // Replace these values with your Okta configuration
    OktaDomain = builder.Configuration.GetValue<string>("Okta:Domain"),
    ClientId = builder.Configuration.GetValue<string>("Okta:ClientId"),
    ClientSecret = builder.Configuration.GetValue<string>("Okta:ClientSecret"),
    Scope = new List<string> { "openid", "profile", "email" },
    PostLogoutRedirectUri = "/"
});

builder.Services.AddScoped<Okta_CosmosDb.Services.IScrubService, Okta_CosmosDb.Services.ScrubService>();
builder.Services.AddSingleton<Okta_CosmosDb.Services.ICosmosService>(Okta_CosmosDb.Services.CosmosService.InitializeCosmosClientInstanceAsync(builder.Configuration.GetSection("CosmosDb")).GetAwaiter().GetResult());

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

这些代码大部分是Visual Studio脚手架过程中的模板,但你要添加一些额外的项目。

首先,你需要配置你的认证以使用AddOktaMvc ,并从你的appsettings 文件中提供配置值。

接下来,你要为你即将编写的几个自定义服务设置依赖性注入。这些是IScrubService ,它将处理你的刷新过程,以及ICosmosService ,它将处理与你的Cosmos DB账户的通信。

最后,你正在调用InitializeCosmosClientInstanceAsync ,它将设置你的数据库和容器(如果它们不存在)。然后它将返回CosmosService ,作为一个单子。

创建你的应用服务

在项目根目录下创建一个新的文件夹,并将其命名为Services 。你将在这个目录中添加以下四个文件。

  • ICosmosService.cs
  • CosmosService.cs
  • IScrubService.cs
  • ScrubService.cs

从替换两个接口中的代码开始。首先,将ICosmosService.cs 中的代码替换为以下内容。

namespace Okta_CosmosDb.Services
{
    public interface ICosmosService
    {
        Task SaveResultAsync(Models.ScrubResult result);
    }
}

接下来,用下面的代码替换IScrubService.cs 中的代码。

namespace Okta_CosmosDb.Services
{
    public interface IScrubService
    {
        Task<Models.ScrubResult> ScrubAsync(Models.Person person);
    }
}

现在你可以在各自的类中实现这些接口。首先,打开CosmosService.cs ,用下面的代码更新那里的代码。

using Okta_CosmosDb.Models;
using Microsoft.Azure.Cosmos;

namespace Okta_CosmosDb.Services
{
    public class CosmosService : ICosmosService
    {
        private Container _container; 
        
        public CosmosService(
            CosmosClient dbClient,
            string databaseName,
            string containerName)
        {
            this._container = dbClient.GetContainer(databaseName, containerName);
        }

        /// <summary>
        /// Creates a Cosmos DB database and a container with the specified partition key. 
        /// </summary>
        /// <returns></returns>
        public static async Task<CosmosService> InitializeCosmosClientInstanceAsync(IConfigurationSection configurationSection)
        {
            string databaseName = configurationSection.GetSection("DatabaseName").Value;
            string containerName = configurationSection.GetSection("ContainerName").Value;
            string account = configurationSection.GetSection("Account").Value;
            string key = configurationSection.GetSection("Key").Value;

            CosmosClient client = new CosmosClient(account, key);
            CosmosService cosmosDbService = new CosmosService(client, databaseName, containerName);
            DatabaseResponse database = await client.CreateDatabaseIfNotExistsAsync(databaseName);
            await database.Database.CreateContainerIfNotExistsAsync(containerName, "/id");

            return cosmosDbService;
        }

        public async Task SaveResultAsync(ScrubResult result)
        {
            await this._container.CreateItemAsync<ScrubResult>(result, new PartitionKey(result.Id));
        }
    }
}

这个服务做了两个任务。首先,它使用SaveResultAsync 方法在Cosmos DB中存储数据。这只是调用你正在操作的容器并在该容器中创建一个项目。

这个类还包含静态的InitializeCosmosClientInstanceAsync 方法,你的Program.cs 调用它来返回服务。任何时候你试图访问一个ICosmosService ,你的应用程序将使用这个方法返回一个单子。这个方法将确保你的数据库和你的容器在将CosmosService 的实例传递给消费者使用之前已经创建。

接下来打开ScrubService.cs ,用这个实现替换那里的代码。

using Okta_CosmosDb.Models;

namespace Okta_CosmosDb.Services
{
    public class ScrubService : IScrubService
    {
        public async Task<ScrubResult> ScrubAsync(Person person)
        {
            var task = Task.Run(() => { return new ScrubResult(person, new Random().Next(2) == 0); });
            return await task;
        }
    }
}

这个类的作用是模拟一个真正的擦边球服务。这里的方法是async ,因为在真实世界的环境中会是这样的,但是由于你只是模拟服务,你可以把登录包在Task.Run ,以模拟一个async

创建你的模型

接下来,你将需要几个数据模型来促进你的视图,并在你的应用程序中传递。在你的Models 文件夹中为ScrubResult.cs 添加一个文件,并在其中添加以下代码。

using Newtonsoft.Json;

namespace Okta_CosmosDb.Models
{
    public class ScrubResult
    {
        [JsonProperty(PropertyName = "id")]
        public string Id { get; set; }

        [JsonProperty(PropertyName = "person")]
        public Person Person { get; set; }

        [JsonProperty(PropertyName = "success")]
        public bool Success { get; set; }

        public ScrubResult(Person person, bool success)
        {
            Person = person;
            Success = success;
            Id = Guid.NewGuid().ToString();
        }
    }
}

现在在你的Models 文件夹中添加Person.cs 的文件和代码。

using Newtonsoft.Json;

namespace Okta_CosmosDb.Models
{
    public class Person
    {
        public string Name { get; set; }

        [JsonIgnore]
        public string SSN { get; set; }
    }
}

这里有几件事情要做,你应该明白。首先,你将在你的Cosmos数据库中存储ScrubResult 对象。每个属性都使用JsonProperty 属性明确地给出了一个名称,然而你不需要这样做。你确实需要一个叫做id 的字段。 JsonProperty 属性有助于保持你的C#代码一贯的Pascal大小写,同时保持你的Cosmos DB属性名称的camel大小写。

这里真正的关键是,API使用Newtonsoft.Json 包将对象序列化为JSON字符串。这意味着你可以使用Newtonsoft包中的任何功能来操作你的数据。这就是你在Person 对象上所做的,你使用JsonIgnore 属性来隐藏SSN。

虽然JsonIgnore 是从这个对象中删除SSN的最简单的方法,但还有许多其他方法。你甚至可以使用Newtonsoft.Json.Serialization.DefaultContractResolver ,创建一个自定义属性,并对该字段进行散列、加密,或以其他方式转化为不太敏感的东西。

添加控制器逻辑

现在你可以在你的应用程序中添加你的控制器逻辑。

首先,在你的Controllers 目录中添加一个名为ImportController.cs 的类。将代码改为以下内容。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

namespace Okta_CosmosDb.Controllers
{
    [Authorize]
    public class ImportController : Controller
    {
        Services.IScrubService _scrubService;
        Services.ICosmosService _cosmosService;

        public ImportController(
            Services.IScrubService scrubService,
            Services.ICosmosService cosmosService
            )
        {
            _scrubService = scrubService;
            _cosmosService = cosmosService;
        }

        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Upload(IFormFile csvFile)
        {
            List<Models.Person> persons = new List<Models.Person>();

            using (var stream = csvFile.OpenReadStream())
            using (StreamReader sr = new StreamReader(stream))
            {
                while (!sr.EndOfStream)
                {
                    string[] rows = sr.ReadToEnd().Split(Environment.NewLine);          
                    
                    for(int i =0; i < rows.Length; i++)
                    {
                       if(i == 0)
                        {
                            //header row
                            continue;
                        }

                        var row = rows[i].Split(',');

                        persons.Add(new Models.Person()
                        {
                            Name = row[0],
                            SSN = row[1]
                        });
                    }
                }
            }

            List<Models.ScrubResult> results = new List<Models.ScrubResult>();

            foreach(var person in persons)
            {
                results.Add(_scrubService.Scrub(person));
            }

            foreach(var result in results)
            {
                _cosmosService.SaveResultAsync(result);
            }

            return View(results);
        }
    }
}

这个控制器将作为导入页面的一个视图。它还将接受一个CSV文件,将该文件转换为Person 对象的列表,然后针对每个人运行刷新过程。一旦刷新过程完成,它将使用你之前设置的数据库将结果保存到你的Cosmos DB账户。

接下来,你将需要一个控制器来记录用户的进入和退出。添加一个名为AccountController.cs 的控制器,并将代码改为以下内容。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication.Cookies;
using Okta.AspNetCore;

namespace Okta_CosmosDb.Controllers
{
    public class AccountController : Controller
    {
        public IActionResult SignIn()
        {
            if (!(HttpContext.User?.Identity?.IsAuthenticated ?? false))
            {
                return Challenge(OktaDefaults.MvcAuthenticationScheme);
            }

            return RedirectToAction("Index", "Import");
        }

        [HttpPost]
        public IActionResult SignOut()
        {
            return new SignOutResult(new[] { OktaDefaults.MvcAuthenticationScheme,
                                         CookieAuthenticationDefaults.AuthenticationScheme });
        }
    }
}

这个控制器包含了按照Okta的建议实现的SignInSignOut 的方法。

最后,用下面的代码替换HomeController.cs 中的代码。

using Microsoft.AspNetCore.Mvc;
using Okta_CosmosDb.Models;
using System.Diagnostics;

namespace Okta_CosmosDb.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index()
        {
            if ((User?.Identity?.IsAuthenticated ?? false))
                return RedirectToAction("Index", "Import");

            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

这里你有Index 方法,但逻辑被替换为将认证的用户重定向到导入界面。

添加和编辑你的视图

第一个要编辑的视图是你的Views/Shared 文件夹中的_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Okta With Cosmos DB</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Okta - Cosmos DB</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                    </ul>
                    @if ((User?.Identity?.IsAuthenticated ?? false))
                    {
                        <form asp-action="SignOut" asp-controller="Account" method="post">
                            <button type="submit" href="Account/Logout" class="btn btn-primary my-2 my-sm-0">Logout</button>
                        </form>
                    }
                    else
                    {
                        <a href="Account/SignIn" class="btn btn-primary my-2 my-sm-0">Login</a>
                    }

                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2022 - <a href="https://profile.fishbowlllc.com" target="_blank" rel="noreferrer">Nik Fisher</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

大部分的模板布局是好的,但它不包括LoginLogout 按钮。你把这些添加到导航条上,并根据用户的认证状态显示适当的一个。

接下来,用下面的代码替换Home\Index.cshtml

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    A small tutorial on protecting PII in Microsoft's <a href="https://azure.microsoft.com/en-us/services/cosmos-db/"> Azure Cosmos DB</a>.
    <br />Secured by <a href="https://www.okta.com/free-trial/">Okta.</a> <br />
    Written by <a href="https://github.com/nickolasfisher"> Nik Fisher.</a>
</div>

这只是一个小的主页,包含一些关于教程的信息。

最后,在你的Views 文件夹中为Import 创建一个新的文件夹(如果还没有创建)。添加一个名为Index.cshtml 的文件,代码如下。

@(ViewData["Title"] = "Import Clients")

<form method="POST" asp-controller="Import" asp-action="Upload" enctype="multipart/form-data">
    <input type="file" name="csvFile" class="btn-outline-primary btn" />
    <input type="submit" class="btn btn-primary" value="Import" />
</form>

这个简单的页面为用户提供了一个导入CSV文件并将其提交给服务器的机会。

接下来,为Upload.cshtml 添加一个文件,该文件将在结果回来时显示。

@model List<Okta_CosmosDb.Models.ScrubResult>
@(ViewData["Title"] = "Scrub Results")

<table class="table table-bordered">
    <thead>
        <tr>
            <th>Name</th>
            <th>Result</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var result in Model)
        {
            <tr>
                <td>@result.Person.Name</td>
                <td>@result.Success</td>
            </tr>
        }
    </tbody>
</table>

结论

作为开发者,我们总是需要考虑保护用户的数据。如果使用得当,PaaS和SaaS平台的兴起已经降低了风险。但是,随着这些新平台的出现,我们必须确保我们使用最佳实践,并对我们选择保留的数据保持关注。

在本教程中,你学到了如何从ASP.NET Core应用程序中存储数据到Cosmos DB。你学会了如何设置你的Cosmos DB账户,以及如何设置你的应用程序以创建数据库和容器来存储你的项目。最后,你使用JSON功能,在存储前从你的数据模型中删除敏感数据。