.NET 8 中 Identity的新增功能

Amy Peng

本篇翻译于Jeremy Likness的What’s new with identity in .NET 8 – .NET Blog (microsoft.com)

.NET 8 中Identity的新增功能

我在2023 年 4 月撰写的有关 ASP.NET Core 团队致力于改进 .NET 8 中的身份验证、授权和Identity管理的文章中提出的计划包括三个关键成果:

  • 用于简化登录和Identity管理的新API,适用于像 Single Page Apps (SPA) 和Blazor WebAssembly这样的客户端程序。
  • 在NET Core Identity 中为无法使用 cookie 的客户端启用基于令牌的身份验证和授权。
  • 文档的改进。

这三个成果都将随 .NET 8 一起发布。此外,我们还为 Blazor Web 应用程序添加了新的Identity UI,并且可以与新的渲染模式(服务器和 WebAssembly )配合使用。

让我们看一下 .NET 8 中的新变化所支持的一些场景。我们将在这篇博文中介绍:

  • 确保简单 Web API 后端的安全
  • 使用新的Blazor Identity UI
  • 添加外部登录(例如 Google 或 Facebook)
  • 使用内置功能和组件保护Blazor WebAssembly 应用程序
  • 对不能使用 cookie 的客户端使用令牌

让我们看一下使用新Identity功能的最简单场景。

基础 Web API 后端

使用新授权的一种简单方法是在基础 Web API 应用程序中启用它。同样的应用程序还可以用作 Blazor WebAssembly、Angular、React 和其他单页 Web 应用程序 (SPA) 的后端。假设您从 .NET 8 中包含 OpenAPI 的 ASP.NET Core Web API 项目入手,您可以通过几个步骤添加身份验证。

Identity是“可选功能”,因此需要添加一些包:

  • 支持 EF Core 集成的包:AspNetCore.Identity.EntityFrameworkCore
  • 您要使用的数据库的包,例如 EntityFrameworkCore.SqlServer(本例中我们将使用内存数据库)

您可以使用 NuGet 包管理器或命令行添加这些包。如果您要使用命令行添加包,请导航到项目文件夹并运行以下 dotnet 命令:

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory

Identity 允许您自定义用户信息和用户数据库,以防您的需求超出 .NET Core 框架中提供的范围。对于我们的基本示例,我们将只使用默认的用户信息和数据库。为此,我们将在项目中添加一个名为 MyUser 的新类,该类继承自 IdentityUser:

class MyUser : IdentityUser {}

添加一个名为 AppDbContext 的新类,该类继承自 IdentityDbContext<MyUser>:

class AppDbContext(DbContextOptions<AppDbContext> options) :
      IdentityDbContext<MyUser>(options)
        {
        }

提供特殊的构造函数可以为不同的环境配置数据库。

如果您要设置应用程序的Identity,请打开 Program.cs 文件。配置Identity以使用基于 cookie 的身份验证,并通过在调用 WebApplication.CreateBuilder(args) 后添加以下代码来启用授权检查:

builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)    
    .AddIdentityCookies();
builder.Services.AddAuthorizationBuilder();

配置 EF Core 数据库。这里我们将使用内存数据库并将其命名为“AppDb”。 这仅用于演示,因此您可以轻松地重新启动应用程序并测试注册和登录流程(每次运行都将从新数据库开始)。更改为 SQLite 将在会话之间节省用户,但需要通过迁移正确创建数据库,如 EF Core 入门教程中所示。您可以将其他关系提供程序(例如 SQL Server)用于您的生产代码。

builder.Services.AddDbContext<AppDbContext>(
    options => options.UseInMemoryDatabase("AppDb"));

配置Identity以使用 EF Core 数据库并公开Identity端点:

builder.Services.AddIdentityCore<MyUser>()
    .AddEntityFrameworkStores<AppDbContext>()
    .AddApiEndpoints();

映射Identity端点的路由。 此代码应放置在调用 builder.Build() 之后:

app.MapIdentityApi<MyUser>();

该应用程序的身份验证和授权功能就准备好了!如果您想要保护端点,请使用定义auth路由的 .RequireAuthentication() 扩展方法。如果您使用基于控制器的解决方案,则可以给 [Authorize] controller或action添加 [Authorize]属性 。

如果您要测试应用程序,点击启动,然后导航到 Swagger UI。 展开受保护的端点,选择“try it out”,然后选择“execute”。 端点会报 404 – not found,这比报 401 – not authorized更安全,因为它没有透露端点存在。

Swagger UI with 404

现在展开 /register 并填写您的凭据。如果您输入无效的电子邮件地址或错误的密码,结果将包含验证错误。

本示例中的errors 以ProblemDetails 格式返回,以便您的客户端可以轻松解析它们并根据需要显示验证错误。我将在独立的 Blazor WebAssembly 应用程序中展示一个示例。

成功注册会有 200 – OK的返回 。您现在可以展开 /login 并输入相同的凭据。 请注意,本示例不需要其他参数,可以将其删除。 请务必将 useCookies 设置为 true。 成功登录会产有 200 – OK 的返回,返回的header中包含 cookie。

现在您可以重新运行受保护的端点,它将返回有效结果。这是因为基于 cookie 的身份验证已经安全地内置于您的浏览器中并且目前它“工作正常”。 您刚刚通过Identity保护了您的第一个端点!

默认情况下,某些 Web 客户端可能不会在header中包含 cookie。如果您使用工具来测试API,您可能需要在设置中启用cookie。 默认情况下,JavaScript fetch API 不包含 cookie。 您可以通过将凭据设置为选项中包含的值来启用它们。 同样,在 Blazor WebAssembly 应用程序中运行的 HttpClient 需要 HttpRequestMessage 包含凭据,如下所示:

request.SetBrowserRequestCredential(BrowserRequestCredentials.Include);

接下来,让我们进入 Blazor Web 应用程序。

Blazor Identity UI

我们团队的一个延伸目标远大目标是在 Blazor 中实现Identity UI,其中包括注册、登录和配置多重身份验证的选项。 当您选择“Individual accounts”选项进行身份验证时,UI 会内置到模板中。 与以前版本的Identity UI 不同的是,除非您想要自定义它,否则它是隐藏的,该模板会生成所有源代码,以便您可以根据需要对其进行修改。 新版本是使用 Razor 组件构建的,可与服务器端和 WebAssembly Blazor 应用程序配合使用。

Blazor login page

新的 Blazor Web 模型允许您配置 UI在服务器端呈现或者在 WebAssembly 中运行的客户端呈现。当您选择WebAssembly模式时,服务器仍将处理所有身份验证和授权请求。它还将为跟踪身份验证状态的 AuthenticationStateProvider 自定义实现生成代码。提供程序使用 PersistentComponentState 类来预呈现身份验证状态并将其保留到页面。客户端 WebAssembly 应用程序中的 PersistentAuthenticationStateProvider 使用该组件来同步服务器和浏览器之间的身份验证状态。 当与自动交互一起运行时,状态提供程序也可以被命名为 PersistingRevalidatingAuthenticationStateProvider 或用于服务器交互的 IdentityRevalidatingAuthenticationStateProvider。

虽然本博文中的示例侧重于简单的用户名和密码登录场景,但 ASP.NET Identity 支持基于电子邮件的交互,例如帐户确认和密码恢复。还可以配置多重身份验证。所有这些功能的组件都包含在 UI 中。

添加外部登录

我们经常被问到一个问题,如何将社交网站的外部登录与 ASP.NET Core Identity 集成。现在,从 Blazor Web 应用默认项目开始,您可以通过以下几个步骤添加外部登录。

首先,您需要在社交网站上注册您的应用程序。 例如,要添加 Twitter 登录,请转到 Twitter 开发人员门户并创建一个新应用程序。您需要提供一些基本信息才能获取您的客户凭据。 创建应用程序后,请导航至应用程序设置并单击身份验证上的“edit”。然后将应用程序类型指定为“native app”,以便流程正常工作,随后启用“request email from users”选项。您需要提供回调 URL。在本示例中,我们将使用 https://localhost:5001/signin-twitter,这是 Blazor Web 应用模板的默认回调 URL。 您可以更改此设置以匹配您应用程序的 URL(例如将您自己的端口替换为5001).另外请注意 API 密钥和密码。

接下来,将适当的身份验证包添加到您的应用程序中。有一个社区维护的 ASP.NET Core OAuth 2.0 社交身份验证提供程序列表,其中有许多选项可供选择。 您可以根据需求混合多个外部登录。 对于 Twitter,我将添加 AspNet.Security.OAuth.Twitter 包。

从服务器项目根目录中的命令提示符中,运行以下命令来存储您的 API 密钥(client ID)和secret。

dotnet user-secrets set "Twitter:ApiKey" "<your-api-key>"
dotnet user-secrets set "TWitter:ApiSecret" "<your-api-secret>"

最后,在 Program.cs 中配置登录,将下面的代码替换:

builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddIdentityCookies();

使用这个代码:

builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddTwitter(opt =>
        {
            opt.ClientId = builder.Configuration["Twitter:ApiKey"]!;
            opt.ClientSecret = builder.Configuration["Twitter:ApiSecret"]!;
        })
    .AddIdentityCookies();

Cookie 是实现 ASP.NET Core Identity 的首选方式并且是最安全的方法。如果有需要,令牌(token)也是支持的,但是需要配置 IdentityConstants.BearerScheme。 令牌是专有的,基于令牌的流程适用于简单场景,因此它不实现 OAuth 2.0 或 OIDC 标准。

这次当您运行应用程序时,登录页面将自动检测外部登录并提供一个按钮来使用它。

Blazor login page with Twitter

当您登录并授权该应用程序时,您将被重定向回来并进行身份验证。

保护 Blazor WebAssembly 应用程序的安全

添加新Identity API 的一个主要动机是让开发人员可以更轻松地保护基于浏览器的应用程序,包括 Single Page Apps (SPA) 和 Blazor WebAssembly。无论您使用内置Identity提供程序、自定义登录还是 Microsoft Entra 等基于云的服务,最终结果都是通过声明和角色进行身份验证或未经身份验证的Identity。在 Blazor 中,您可以通过向组件或托管该组件的页面添加 [Authorize] 属性来保护 razor 组件。您还可以通过在路由定义中添加

.RequireAuthorization()扩展方法来保护路由。

Blazor 示例存储库中提供了此示例的完整源代码。

AuthorizeView 标签提供了一种简单方法来处理用户有权访问的内容。您可以通过context属性访问身份验证状态。参考下面的代码:

<p>Welcome to my page!</p>
<AuthorizeView>
    <Authorizing>
        <div class="alert alert-info">We're checking your credentials...</div>
    </Authorizing>
    <Authorized>
        <div class="alert alert-success">You are authenticated @context.User.Identity?.Name</div>
    </Authorized>
    <NotAuthorized>
        <div class="alert alert-warning">You are not authenticated!</div>
    </NotAuthorized>
</AuthorizeView>

问候语将显示给所有人。对于 Blazor WebAssembly,当客户端可能需要通过 API 调用进行异步身份验证时,在查询和解析身份验证状态时显示授权内容。然后,根据您是否已通过身份验证,您将看到您的姓名或一条表明您未通过身份验证的消息。

使用AuthenticationStateProvider 可以知道您是否通过了身份验证。

App.razor 页面被封装在 CascadingAuthenticationState 提供程序中。 该提供程序负责跟踪身份验证状态并将其提供给应用程序的其余部分。AuthenticationStateProvider 被注入到提供程序中并用于跟踪状态。AuthenticationStateProvider 也被注入到 AuthorizeView 组件中。当身份验证状态发生变化时,提供者会通知 AuthorizeView 组件,内容也会相应更新。

首先,我们要确保 API 调用按预期保存凭据。为此,我创建了一个名为 CookieHandler 的处理程序。

public class CookieHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        return base.SendAsync(request, cancellationToken);
    }
}

在 Program.cs 中,我将处理程序添加到 HttpClient 并使用客户端工厂来配置特殊客户端以进行身份验证。

builder.Services.AddTransient<CookieHandler>();
builder.Services.AddHttpClient(
    "Auth",
    opt => opt.BaseAddress = new Uri(builder.Configuration["AuthUrl"]!))
    .AddHttpMessageHandler<CookieHandler>();

注意:身份验证组件是可选的,可以通过Microsoft.AspNetCore.Components.WebAssembly.Authentication 包使用。 客户端工厂和扩展方法来自 Microsoft.Extensions.Http。

AuthUrl 是公开Identity API 的 ASP.NET Core 服务器的 URL。 接下来,我创建了一个继承自 AuthenticationStateProvider 的 CookieAuthenticationStateProvider 并重写 GetAuthenticationStateAsync 方法。 主要逻辑如下:

var unauthenticated = new ClaimsPrincipal(new ClaimsIdentity());
var userResponse = await _httpClient.GetAsync("manage/info");
if (userResponse.IsSuccessStatusCode) 
{
    var userJson = await userResponse.Content.ReadAsStringAsync();
    var userInfo = JsonSerializer.Deserialize<UserInfo>(userJson, jsonSerializerOptions);
    if (userInfo != null)
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, userInfo.Email),
            new(ClaimTypes.Email, userInfo.Email)
        };
        var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
        user = new ClaimsPrincipal(id);
    }
}

return new AuthenticationState(user);

用户信息端点是安全的,因此如果用户未经身份验证,请求将失败,并且该方法将返回未经身份验证的状态。否则,它会构建适当的Identity和声明并返回经过身份验证的状态。

应用程序如何知道状态何时发生变化? 以下是使用Identity API 从 Blazor WebAssembly 进行登录的情况:

async Task<AuthenticationState> LoginAndGetAuthenticationState()
{
    var result = await _httpClient.PostAsJsonAsync(
        "login?useCookies=true", new
        {
            email,
            password
        });

        return await GetAuthenticationStateAsync();
}

NotifyAuthenticationStateChanged(LoginAndGetAuthenticationState());

登录成功后,将调用 AuthenticationStateProvider 基类上的

NotifyAuthenticationStateChanged 方法来通知提供程序状态已更改。它会传递新的身份验证状态请求的结果,以便它可以验证 cookie 是否存在。然后,提供程序将更新 AuthorizeView 组件,用户将看到经过身份验证的内容。

令牌(Tokens

在极少数情况下,如果您的客户端不支持 cookie,登录 API 会提供一个参数来请求令牌。 一个自定义令牌(ASP.NET Core Identity平台专有的令牌),可用于对后续请求进行身份验证。令牌作为承载令牌在Authorization标头中传递。其次,API还提供刷新令牌。这允许您的应用程序在旧令牌过期时请求新令牌,而无需强制用户再次登录。这些令牌不是标准 JSON Web 令牌 (JWT)。这是有意为之的,因为内置Identity主要用于简单的场景。令牌选项并不是功能齐全的Identity服务提供方或令牌服务器,而只是为无法使用 cookie 的客户端提供的替代 cookie 选项的方案。

如果您不确定是否需要令牌服务器,您可以阅读文档来帮助您选择正确的 ASP.NET Core Identity解决方案。请阅读我们的 ASP.NET Core Identity管理解决方案列表以使用更先进的Identity解决方案。

文档和示例

第三个成果是文档和示例样本。我们引入了新的文档,并将在 .NET 8 发布时添加新的文章和示例。请关注问题 #29452 – .NET 8 中的Identity文档和示例来跟踪进度。请在这个问题下面询问您正在寻找的其他文档或示例。您还可以链接到各种文档的具体问题并在那里提供反馈。

结论

.NET 8 中的新Identity功能可以更容易地保护应用程序的安全。如果您的需求很简单,您现在只需通过几行代码就可以向您的应用程序添加Identity验证和授权。新的 API 可以通过基于 cookie 的Identity验证和授权来保护您的 Web API 端点。对于不能使用 cookie 的客户端,还有一个基于令牌的选项。

您可以在 ASP.NET Core 文档中了解有关新Identity功能的更多信息。

0 comments

Leave a comment

Feedback usabilla icon