使用 .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>
你想开发一个API吗?(或者堆个雪人?)
打开你常用的命令提示符,然后输入以下内容
dotnet new web -n minimalapi
最终结果应该类似这样:
这就是最简洁的默认设置了!打开*.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>
这使我们能够针对预览语言功能,拉取编译器 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;
与传统的 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();
注意这里完全没有逗号usings、namespaces句号等等。这真美!
这段代码创建了一个 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);
}
}
}
}
}
在 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
