发布于 2026-01-06 2 阅读
0

使用 .NET 6、C# 10 和 Azure Active Directory 构建安全且精简的 API

使用 .NET 6、C# 10 和 Azure Active Directory 构建安全且精简的 API

2021 年微软 Build 大会精彩纷呈,新品发布和功能展示层出不穷。我们还提前了解了来年即将推出的新功能。我尤其喜欢关于 .NET 和 Azure PaaS/FaaS 的环节!您可以在这里点播观看所有内容

在其中一场研讨会上,我看到 Maria Nagagga 和 Stephen Halter 展示了一个用 .NET 6 和 C# 10 构建的极简 API。新版 .NET 带来了一些令人兴奋的新功能和诸多语言改进。我必须承认,新的 .NET 6(目前是 Preview 4 版本)看起来很像一个精简版的 Node.js API,这非常棒,因为我们开发者只需编写更少的代码就能实现所需功能。代码越少,bug 就越少!

我曾尝试使用FeatherHTTP框架实现一些极简 API,但如果 .NET 框架本身就能内置这种功能,而无需引入外部依赖项,那就更好了 :)

受 Maria 和 Stephen 的启发,我不仅创建了自己的极简 API 版本,还添加了使用 Azure AD 的身份验证功能。让我们来试用一下。

先决条件

您需要安装以下软件:

  • 最新版 .NET 6(预览版 4)可在此处下载。
  • 最新 Visual Studio 2019 预览版

我用的是 VS Code,所以没有向导之类的东西。好在 .NET CLI 有一个模板,可以支持最少的 API :) CLI 优先!

最后,这段代码中的许多部分仍然在使用 nightly 版本,因此您需要创建一个nuget.config文件并添加以下 XML 代码,以确保能够拉取正确的 NuGet 包。冒险也是一种乐趣……

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
    <add key="dotnet6" value="https://dnceng.pkgs.visualstudio.com/public/_packaging/dotnet6/nuget/v3/index.json" />
    <add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
  </packageSources>
</configuration>
Enter fullscreen mode Exit fullscreen mode

你想开发一个API吗?(或者堆个雪人?)

打开你常用的命令提示符,然后输入以下内容

dotnet new web -n minimalapi
Enter fullscreen mode Exit fullscreen mode

最终结果应该类似这样:

替代文字

这就是最简洁的默认设置了!打开*.csproj文件并将其更新为以下内容:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <LangVersion>Preview</LangVersion>
    <UserSecretsId>2bd37d96-2487-4c58-a5f3-ddd2524920ea</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Identity.Web" Version="1.11.0" />
    <PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.0.0-2.21275.18">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <Compile Include=".usings" />
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

这使我们能够针对预览语言功能,拉取编译器 NuGet 包来运行和调试应用程序,以及Microsoft.Identity.Web用于身份验证的 NuGet 包。

接下来,我们将添加一个.usings文件,用来存放(或者说声明)我们所有的 using 语句。简洁明了。我的文件看起来是这样的:

global using System;
global using System.Collections.Generic;
global using System.Globalization;
global using System.Linq;
global using System.Net;
global using System.Security.Claims;
global using System.Net.Http;
global using System.Net.Http.Json;
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Cors;
global using Microsoft.AspNetCore.Http;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Identity.Web;
global using Microsoft.AspNetCore.Authorization;
global using MinimalWeather;
Enter fullscreen mode Exit fullscreen mode

与传统的 ASP.NET API 不同,最小版本不包含 `.` 文件Startup.cs。相反,我们所有的初始化代码、中间件以及端点都放在 `.`Program.cs文件中。打开该文件并添加以下内容:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => options.AddPolicy("allowAny", o => o.AllowAnyOrigin()));
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddAuthorization();

var app = builder.Build();

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

app.MapGet("/secure", [EnableCors("allowAny")] (HttpContext context) => 
{
    AuthHelper.UserHasAnyAcceptedScopes(context, new string[] {"access_as_user"});
    return "hello from secure";
}).RequireAuthorization();


app.MapGet("/insecure", [EnableCors("allowAny")] () =>
{
    return "hello from insecure";
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

注意这里完全没有逗号usingsnamespaces句号等等。这真美

这段代码创建了一个 Web 应用程序,然后我们仅用三行代码就添加了 CORS 和 MIW 身份验证。接下来,我们需要在中间件中启用身份验证和授权,最后定义两个端点:

  • /secure(需要有效的访问令牌)
  • /insecure(无需事先身份验证即可访问)

遗憾的是,由于目前还处于早期阶段,与 Microsoft.Identity.Web 的集成尚未完全实现。我无法使用该RequiredScope属性强制检查传入的 JWT 作用域。因此,我从 Microsoft.Identity.Web 代码库中复制了代码(链接在此),并创建了一个辅助方法来检查 HTTP 请求是否包含正确的作用域。代码如下所示:

namespace MinimalWeather
{
    public static class AuthHelper
    {
        public static void UserHasAnyAcceptedScopes(HttpContext context, string[] acceptedScopes)
        {
            if (acceptedScopes == null)
            {
                throw new ArgumentNullException(nameof(acceptedScopes));
            }

            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            IEnumerable<Claim> userClaims;
            ClaimsPrincipal user;

            // Need to lock due to https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?#do-not-access-httpcontext-from-multiple-threads
            lock (context)
            {
                user = context.User;
                userClaims = user.Claims;
            }

            if (user == null || userClaims == null || !userClaims.Any())
            {
                lock (context)
                {
                    context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                }

                throw new UnauthorizedAccessException("IDW10204: The user is unauthenticated. The HttpContext does not contain any claims.");
            }
            else
            {
                // Attempt with Scp claim
                Claim? scopeClaim = user.FindFirst(ClaimConstants.Scp);

                // Fallback to Scope claim name
                if (scopeClaim == null)
                {
                    scopeClaim = user.FindFirst(ClaimConstants.Scope);
                }

                if (scopeClaim == null || !scopeClaim.Value.Split(' ').Intersect(acceptedScopes).Any())
                {
                    string message = string.Format(CultureInfo.InvariantCulture, "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. ", string.Join(",", acceptedScopes));

                    lock (context)
                    {
                        context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                        context.Response.WriteAsync(message);
                        context.Response.CompleteAsync();
                    }

                    throw new UnauthorizedAccessException(message);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

在 Azure Active Directory 中创建应用注册

我们将使用 .NET Interactive 来配置 Azure AD 并注册两个应用程序,而不是逐一展示步骤。.NET Notebook 已附加到存储库中,您只需按照说明运行即可配置 AAD 身份验证。
在本示例中,我选择了 Web API 和 Web 应用程序配置。Web 应用程序用于安全地调用 API。

把代码给我看看!

您可以在 425Show 的 GitHub 仓库中找到可行的解决方案。

概括

我非常期待使用 .NET 6 和 C# 10 构建更多有趣的项目。这个框架比以前更快、更简洁、更强大。而且别忘了,使用 .NET,你可以构建适用于所有平台和所有类型的应用程序!

如有任何问题,请随时联系我,祝您编程愉快 :)

文章来源:https://dev.to/425show/secure-and-minimal-apis-using-net-6-c-10-and-azure-active-directory-197i