在 ASP.NET Core Web API 之间共享身份持有者令牌
在分布式 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 中使用多个环境
- ASP.NET Core 中的 Azure Key Vault 配置提供程序
- AWS Secrets Manager
关注点
在“ASP.NET Core 上的身份介绍”中,微软建议
ASP.NET Core 身份将用户界面 (UI) 登录功能添加到 ASP.NET Core Web 应用程序。要保护 Web API 和 SPA,请使用以下其中之一
- Microsoft Entra ID
- Azure Active Directory B2C (Azure AD B2C)
- Duende Identity Server
微软实际上提供了一篇关于将身份与 SPA 结合使用的文章
此类功能长期以来一直存在于 .NET Framework 上的 ASP.NET Identity 中。