ASP.NET Core 6 Web API调用MS Graph API遇类型加载错误及解决
我的项目包含两个应用:
- 前端:基于Angular开发,使用msal-angular实现Azure AD用户认证
- 后端:ASP.NET Core 6 Web API
用户可通过该应用设置新PIN码,用于访问公司职场。
如果用户1知晓用户2的employeeId,就能获取并修改用户2的PIN码,存在越权操作风险。
通过后端调用MS Graph API,在后端层面获取当前请求用户的真实employeeId,将请求中传入的employeeId与真实ID进行比对,确保用户只能操作自己的PIN码。
program.cs文件
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Identity.Web; var builder = WebApplication.CreateBuilder(args); var MyConfig = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); // Add services to the container. builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd") .EnableTokenAcquisitionToCallDownstreamApi() .AddMicrosoftGraph(graphBaseUrl: "https://graph.microsoft.com", defaultScopes: "User.Read") // 此处修改 .AddInMemoryTokenCaches(); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); app.UseCors(builder => builder.WithOrigins( "http://localhost:4200", "https://app-gws-pinguin.azurewebsites.net") .AllowAnyOrigin().AllowAnyHeader().WithMethods("GET", "DELETE", "POST", "PUT")); app.UseSwagger(); app.UseSwaggerUI( c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "PINguin API"); c.RoutePrefix = string.Empty; } ); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseRouting(); app.UseAuthorization(); app.MapControllers(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); app.Run();
KeyVaultController.cs文件
using Azure.Identity; using Azure.Security.KeyVault.Secrets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Graph; using Microsoft.Identity.Web.Resource; using pinguin_api.Models; using System.Security.Claims; namespace pinguin_api.Controllers { [Authorize] [RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")] [Route("api/KeyVault")] [ApiController] public class KeyVaultController : Controller { private readonly SecretClient _secretClient; private readonly GraphServiceClient _graphServiceClient; public KeyVaultController(IConfiguration configuration, GraphServiceClient graphServiceClient) { var keyVaultUrl = configuration.GetValue<string>("KeyVaultUrl"); _secretClient = new SecretClient(new Uri(keyVaultUrl), new ManagedIdentityCredential()); _graphServiceClient = graphServiceClient; } [HttpGet("getSecret")] public async Task<IActionResult> GetSecret([FromQuery] string secretName) { try { // 从Access Token中提取用户标识 var userId = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrWhiteSpace(userId)) { return Unauthorized("无法验证用户身份。"); } // 从Azure AD获取用户工号 var user = await _graphServiceClient.Users[userId].GetAsync(); var employeeId = user.OnPremisesExtensionAttributes.ExtensionAttribute1; var userEmployeeNumber = user.AdditionalData["extensionAttribute1"].ToString(); // 验证工号是否匹配 if (secretName != userEmployeeNumber) { return Unauthorized("工号与当前认证用户不匹配。"); } // 从KeyVault获取密钥 KeyVaultSecret secret = await _secretClient.GetSecretAsync(secretName); if (secret != null) { return Ok(secret.Value); } return NotFound(); } catch (ServiceException e) when (e.InnerException is UnauthorizedAccessException) { return Unauthorized($"访问用户数据出错:{e.Message}"); } catch (ArgumentException) { return BadRequest(); // 400 请求错误 } catch (System.Security.SecurityException) { return StatusCode(403); // 403 禁止访问 } } [HttpPost("setSecret")] public async Task<ActionResult> SetSecret(SecretModel secretModel) { if (secretModel == null || string.IsNullOrWhiteSpace(secretModel.SecretName) || string.IsNullOrWhiteSpace(secretModel.SecretValue)) { return BadRequest("必须提供密钥名称和值。"); } try { KeyVaultSecret secret = new KeyVaultSecret(secretModel.SecretName, secretModel.SecretValue); secret.Properties.Tags.Add("EmployeeName", secretModel.SecretTag); await _secretClient.SetSecretAsync(secret); return Ok(); } catch (Azure.RequestFailedException e) { return BadRequest($"设置密钥出错:{e.Message}"); } } } }
.csproj文件
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <UserSecretsId>aspnet-pinguin_api-922cbc86-144d-48ee-a4a5-05baafcbc41b</UserSecretsId> <RootNamespace>pinguin_api</RootNamespace> </PropertyGroup> <ItemGroup> <PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.5.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.12" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.12" /> <PackageReference Include="Microsoft.Graph" Version="5.36.0" /> <PackageReference Include="Microsoft.Graph.Core" Version="3.1.3" /> <PackageReference Include="Microsoft.Identity.Web" Version="2.16.0" /> <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="2.16.0" /> <PackageReference Include="Microsoft.Identity.Web.UI" Version="2.16.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> </ItemGroup>
- 参考Web API调用下游API的官方配置文档
- 参考Web API调用Microsoft Graph的代码配置指南
System.TypeLoadException: 无法从程序集'Microsoft.Graph.Core, Version=3.1.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'中加载类型'Microsoft.Graph.IAuthenticationProviderOption'。
at Microsoft.Identity.Web.GraphServiceCollectionExtensions.<>c.b__1_0(IServiceProvider serviceProvider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.b__0(ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
at lambda_method11(Closure, IServiceProvider, Object[])
at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass7_0.b__0(ControllerContext controllerContext)
at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass6_0.g__CreateController|0(ControllerContext controllerContext)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
2023年12月7日更新
移除KeyVaultController中所有Graph相关代码
在KeyVaultController.cs中移除所有与GraphServiceClient相关的代码后,再次请求GetSecret端点,未出现错误。
仅保留Graph的声明与初始化
仅保留KeyVaultController.cs中GraphServiceClient的声明和构造函数初始化逻辑时,请求GetSecret端点仍出现完全相同的错误。
结论:问题出在以下代码片段:
private readonly SecretClient _secretClient; private readonly GraphServiceClient _graphServiceClient; public KeyVaultController(IConfiguration configuration, GraphServiceClient graphServiceClient) { var keyVaultUrl = configuration.GetValue<string>("KeyVaultUrl"); _secretClient = new SecretClient(new Uri(keyVaultUrl), new ManagedIdentityCredential()); _graphServiceClient = graphServiceClient; }
2023年12月14日更新:问题已解决
未修复原错误,改用HttpClient实现需求。以下代码可用于获取Azure AD用户的自定义extensionAttribute,也适用于其他MS Graph请求:
private readonly SecretClient _secretClient; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; public KeyVaultController(IConfiguration configuration, IHttpClientFactory httpClientFactory) { _configuration = configuration; _httpClientFactory = httpClientFactory; var keyVaultUrl = configuration.GetValue<string>("KeyVaultUrl"); _secretClient = new SecretClient(new Uri(keyVaultUrl), new ManagedIdentityCredential()); } private async Task<string> GetAccessTokenAsync() { string authority = $"{_configuration["AzureAd:Instance"]}{_configuration["AzureAd:TenantId"]}"; var cca = ConfidentialClientApplicationBuilder.Create(_configuration["AzureAd:ClientId"]) .WithClientSecret(_configuration["AzureAd:ClientSecret"]) .WithAuthority(new Uri(authority)) .Build(); var result = await cca.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }).ExecuteAsync(); return result.AccessToken; } private async Task<JsonElement> GetUserFromGraphAsync(string userEmail, string accessToken) { var httpClient = _httpClientFactory.CreateClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var url = $"https://graph.microsoft.com/v1.0/users?$filter=startswith(userPrincipalName,'{userEmail}')&$select=displayName,onPremisesExtensionAttributes"; var response = await httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); var jsonResponse = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(jsonResponse); var userElement = doc.RootElement.GetProperty("value")[0].Clone(); // 克隆元素 return userElement; }
内容的提问来源于stack exchange,提问作者Maido47

