65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ASP.NET Core Web API 之间共享身份持有者令牌

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2024年5月11日

CPOL

3分钟阅读

viewsIcon

5415

在分布式 ASP.NET Core Web API 之间共享身份持有者令牌

引言

本文是“解耦 ASP.NET Core 身份、身份验证和数据库引擎”的后续文章。在这部续集中,我们深入研究了促进解耦的软件工程实践,使它们有利于测试驱动开发 (TDD) 和企业应用程序的快速开发,而不是 Visual Studio 项目模板的脚手架代码隐式推广的“上帝”程序集。

背景

早在 .NET Framework 1 和 2 时代,微软就引入了优雅的架构设计,用于保护各种程序宿主(例如 WinForms、WPF、Windows 服务和 ASP.NET (Core))中的应用程序代码。作为 .NET 程序员,您可以简单地使用来自各种命名空间的 AuthorizeAttribute 修饰相关函数,具体取决于宿主类型。然后,.NET 运行时将无缝处理身份验证和授权,利用来自应用程序代码或配置文件中的适当配置。

通过拥抱 .NET 组件设计并最大限度地减少主业务逻辑与宿主之间的耦合,您可以保持当前宿主的代码简洁。此外,这种方法确保了将来向新宿主类型的更平滑迁移。

本文介绍的安全架构早在 ASP.NET Core 之前就已经存在于 ASP.NET 中,区别在于 ASP.NET Core 具有更好的 DI/IoC。

使用代码

与之前的文章相比,本文主要使用“PetWebApi”作为示例。PetController 是通过基于“PetStore.yaml”的一些 Swagger 代码生成生成的,而“PetStoreClientApi”是使用 OpenApiClientGen 生成的。

在这里,我们只需要关注一些已实现的 Web API 函数。

使用 AuthorizeAttribute 修饰 Controller 或 Function

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public partial class PetController : ControllerBase
{
    public PetController()
    {
    }

    /// <summary>Add a new pet to the store<. If you give header transaction-id, it will give back the same/summary>
    /// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
    /// <param name="cookieParam">Some cookie</param>
    [HttpPost, Route("pet")]
    public async Task<Pet> AddPet([FromBody] Pet body)//, [FromHeader(Name = "Accept-Language")] string accept_Language, long cookieParam)
    {
        long key = PetData.Instance.GetCurrentMax();
        body.Id = key;
        PetData.Instance.Dic.TryAdd(key, body);
        Response.Headers.Add("transaction-id", Request.Headers["transaction-id"]);
        return body;
    }

    /// <summary>Update an existing pet</summary>
    /// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
    /// <param name="cookieParam">Some cookie</param>
    [HttpPut, Route("pet")]
    public async Task<IActionResult> UpdatePet([FromBody] object body, [FromHeader(Name = "Accept-Language")] string accept_Language, long cookieParam)
    {
        throw new NotImplementedException();
    }

    /// <summary>Find pet by ID</summary>
    /// <param name="petId">ID of pet to return</param>
    /// <returns>successful operation</returns>
    [HttpGet, Route("pet/{petId}")]
    public async Task<ActionResult<Pet>> GetPetById(long petId)
    {
        if (PetData.Instance.Dic.TryGetValue(petId, out Pet p))
        {
            return p;
        }
        else
        {
            return NotFound();
        }
    }

    /// <summary>Updates a pet in the store with form data</summary>
    /// <param name="petId">ID of pet that needs to be updated</param>
    [HttpPost, Route("pet/{petId}")]
    public async Task<IActionResult> UpdatePetWithForm(long petId, Microsoft.AspNetCore.Http.IFormFile body)
    {
        throw new NotImplementedException();
    }

    /// <summary>Deletes a pet</summary>
    /// <param name="petId">Pet id to delete</param>
    [HttpDelete, Route("pet/{petId}")]
    public async Task<IActionResult> DeletePet(long petId)
    {
        if (PetData.Instance.Dic.TryGetValue(petId, out _)) //not to TryRemove for testing
        {
            return Ok();
        }
        else
        {
            return NotFound("NoSuchPet");
        }
    }

配置宿主程序

现在我们配置宿主程序以检查正确的持有者令牌。

请注意,PetWebApi 不了解 ASP.NET Core 身份及其数据库,它只会信任“Core3WebApi”产生的持有者令牌。

 

builder.Services.AddAuthentication(
    options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; //Bearer
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }
).AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidAudience = authSettings.Audience,
        ValidIssuer = authSettings.Issuer,
        IssuerSigningKey = issuerSigningKey,
#if DEBUG
        ClockSkew = TimeSpan.FromSeconds(2), //Default is 300 seconds. This is for testing the correctness of the auth protocol implementation between C/S.
#endif
    }; 
});

PetWebApi 信任 Core3WebApi 产生的持有者令牌

客户端需要在与 localhost:6000 上的 PetWebApi 通信之前,从 localhost:5000 获取一个合适的持有者令牌

public class PetsFixture : DefaultHttpClientWithUsername
{
    public PetsFixture()
    {
        Uri baseUri = new("https://:6000");

        httpClient = new System.Net.Http.HttpClient
        {
            BaseAddress = baseUri,
        };

        httpClient.DefaultRequestHeaders.Authorization = AuthorizedClient.DefaultRequestHeaders.Authorization;

        Api = new PetClient(httpClient, new Newtonsoft.Json.JsonSerializerSettings()
        {
            NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore
        });
    }

    public PetClient Api { get; private set; }

    readonly System.Net.Http.HttpClient httpClient;
...
}

public partial class PetsApiIntegration : IClassFixture<PetsFixture>
{
    public PetsApiIntegration(PetsFixture fixture)
    {
        api = fixture.Api;
    }

    readonly PetClient api;

    [Fact]
    public async Task TestGetPet()
    {
        Pet d = await api.GetPetByIdAsync(12);
        Assert.Equal("Narco", d.Name);
    }

    [Fact]
    public async Task TestAddPet()
    {
        await api.AddPetAsync(new Pet()
        {
            //Id=339,
            Name = "KKK", //required
            PhotoUrls = new string[] { "http://somewhere.com/mydog.jpg" }, //required,
            Tags = new Tag[] { //not required. However, when presented, it must contain at least one item.
                new Tag()
                {
                    //Id=3,
                    Name="Hey"
                }
            },
        });
    }

    [Fact]
    public async Task TestPetsDelete()
    {
        WebApiRequestException ex = await Assert.ThrowsAsync<WebApiRequestException>(() => api.DeletePetAsync(9));
        Assert.Equal("NoSuchPet", ex.Response);
    }

在运行测试套件之前,通过 "StartCoreWebApi.ps1" 启动 Core3WebApi,并通过 "StartPetStoreapi.ps1" 启动 PetWebApi。

现在您可以看到 JWT 是无状态的。

如果令牌已过期,客户端将获得未授权状态代码。

[Fact]
public async Task TestFindPetsTokenExpiresThrows()
{
    Pet[] aa = await api.FindPetsByStatusAsync(PetStatus.sold);
    Assert.Equal(3, aa.Length);
    Thread.Sleep(7050);
    var ex = await Assert.ThrowsAsync<WebApiRequestException>(() => api.FindPetsByStatusAsync(PetStatus.sold));
    Assert.Equal(System.Net.HttpStatusCode.Unauthorized, ex.StatusCode);
}

共享密钥

为了使这种“分布式”身份验证能够工作,当然应该在 Web API 各方之间存在一些共享密钥,而主要密钥是“IssuerSigningkey”。

关于 ValidateIssuerSigningKey

根据 Microsoft Learn:

令牌可能包含检查签名的公钥。例如,X509Data 可以被水合到 X509Certificate 中,后者可用于验证签名。在这些情况下,验证用于验证签名的 SigningKey 非常重要。此布尔值仅适用于默认的签名密钥验证。如果设置了 IssuerSigningKeyValidator,则无论此属性为 true 还是 false,都会调用它。默认值为 false。

但是,至少对于持有者令牌,即使将此属性设置为 false,无效或不同的 IssuerSigningKey 也会导致未授权错误。这显然应该是正确的方式,因为密钥是主要的共享密钥。

Windows 和 Azure 云以及其他云提供商提供了一些存储共享密钥的工具。讨论如何为生产存储此类密钥超出了本文的范围,但有很多好的参考资料

关注点

在“ASP.NET Core 上的身份介绍”中,微软建议

ASP.NET Core 身份将用户界面 (UI) 登录功能添加到 ASP.NET Core Web 应用程序。要保护 Web API 和 SPA,请使用以下其中之一

微软实际上提供了一篇关于将身份与 SPA 结合使用的文章

此类功能长期以来一直存在于 .NET Framework 上的 ASP.NET Identity 中。

 

© . All rights reserved.