You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 13:45:57