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

Blazor调用Microsoft Graph API遇CORS错误,Web API同代码正常

问题本质

Azure AD的oauth2/v2.0/token端点故意限制浏览器端跨域请求,因为浏览器环境会暴露客户端密钥等敏感信息,违反OAuth2安全设计原则。这是你从Blazor浏览器端请求时触发CORS错误的核心原因,无法直接通过修改请求模式绕过(no-cors会返回不透明响应,无法获取有效token)。

以下是针对不同Blazor类型的解决方案:


1. Blazor Server 解决方案(直接复用Web API逻辑)

Blazor Server的代码运行在服务端,和你原Web API的执行环境完全一致,不存在浏览器跨域限制,直接复用原有代码即可:

  • 将获取Token、调用Graph API的逻辑封装为服务类,注册到依赖注入容器
  • 在Blazor组件中注入该服务,调用方法执行请求

示例代码

服务类(Server项目中)

public class GraphEmailService
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _config;

    public GraphEmailService(HttpClient httpClient, IConfiguration config)
    {
        _httpClient = httpClient;
        _config = config;
    }

    public async Task<string> GetAccessTokenAsync()
    {
        var tokenRequest = new Dictionary<string, string>
        {
            {"grant_type", "client_credentials"},
            {"client_id", _config["AzureAd:ClientId"]},
            {"client_secret", _config["AzureAd:ClientSecret"]},
            {"scope", "https://graph.microsoft.com/.default"}
        };

        var response = await _httpClient.PostAsync(
            $"https://login.microsoftonline.com/{_config["AzureAd:TenantId"]}/oauth2/v2.0/token",
            new FormUrlEncodedContent(tokenRequest));
        
        response.EnsureSuccessStatusCode();
        var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
        return tokenResponse.AccessToken;
    }

    public async Task SendEmailAsync(string accessToken, EmailContent email)
    {
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var graphPayload = new
        {
            message = new
            {
                subject = email.Subject,
                body = new { contentType = "Text", content = email.Body },
                toRecipients = email.To.Select(t => new { emailAddress = new { address = t } })
            },
            saveToSentItems = true
        };

        await _httpClient.PostAsync("https://graph.microsoft.com/v1.0/sendMail", 
            JsonContent.Create(graphPayload));
    }

    // 辅助类
    public class TokenResponse { public string AccessToken { get; set; } }
    public class EmailContent { public string Subject { get; set; } public string Body { get; set; } public List<string> To { get; set; } }
}

注册服务(Program.cs)

builder.Services.AddScoped<GraphEmailService>();
builder.Services.AddHttpClient();

Blazor组件中使用

@inject GraphEmailService EmailService

<button @onclick="SendTestEmail">发送测试邮件</button>

@code {
    private async Task SendTestEmail()
    {
        var token = await EmailService.GetAccessTokenAsync();
        await EmailService.SendEmailAsync(token, new GraphEmailService.EmailContent
        {
            Subject = "Blazor Server测试邮件",
            Body = "内容测试",
            To = new List<string> { "test@example.com" }
        });
    }
}

2. Blazor WebAssembly 解决方案

WebAssembly代码运行在浏览器端,必须采用安全的认证方式,不能直接请求token端点,推荐两种方案:

方案A:使用MSAL.js获取用户授权Token(推荐)

通过Azure AD单页应用(SPA)配置,使用PKCE授权码流获取Token,避免暴露客户端密钥:

  1. 在Azure AD中注册SPA应用,重定向URI设置为https://localhost:7253/authentication/login-callback(根据你的端口调整)
  2. 安装NuGet包:Microsoft.Authentication.WebAssembly.Msal
  3. 配置MSAL(Program.cs)
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("https://graph.microsoft.com/Mail.Send");
});
  1. 配置appsettings.json
{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/你的租户ID",
    "ClientId": "你的客户端ID",
    "ValidateAuthority": true
  }
}
  1. Blazor组件中调用Graph API
@inject IAccessTokenProvider TokenProvider
@inject HttpClient Http
@inject NavigationManager Navigation

<button @onclick="SendEmail">发送邮件</button>

@code {
    private async Task SendEmail()
    {
        var tokenResult = await TokenProvider.RequestAccessToken(
            new AccessTokenRequestOptions
            {
                Scopes = new[] { "https://graph.microsoft.com/Mail.Send" }
            });

        if (tokenResult.TryGetToken(out var token))
        {
            Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Value);
            var emailPayload = new
            {
                message = new
                {
                    subject = "WASM测试邮件",
                    body = new { contentType = "Text", content = "内容测试" },
                    toRecipients = new[] { new { emailAddress = new { address = "test@example.com" } } }
                },
                saveToSentItems = true
            };

            await Http.PostAsync("https://graph.microsoft.com/v1.0/me/sendMail", 
                JsonContent.Create(emailPayload));
        }
        else
        {
            // 跳转到登录页完成认证
            Navigation.NavigateTo(tokenResult.RedirectUrl);
        }
    }
}

方案B:利用托管项目的API转发请求(无需单独建中间API)

如果必须使用客户端凭证流(无需用户登录,用应用权限发邮件),可以在Blazor WASM的托管Server项目中添加代理API,由服务端转发token和Graph请求:

托管项目中添加代理控制器

[ApiController]
[Route("api/graph-proxy")]
public class GraphProxyController : ControllerBase
{
    private readonly IConfiguration _config;
    private readonly HttpClient _httpClient;

    public GraphProxyController(IConfiguration config, HttpClient httpClient)
    {
        _config = config;
        _httpClient = httpClient;
    }

    [HttpPost("send-email")]
    public async Task<IActionResult> SendEmail([FromBody] EmailContent email)
    {
        // 服务端获取token
        var token = await GetAccessToken();
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        // 调用Graph API发送邮件
        var graphPayload = new
        {
            message = new
            {
                subject = email.Subject,
                body = new { contentType = "Text", content = email.Body },
                toRecipients = email.To.Select(t => new { emailAddress = new { address = t } })
            },
            saveToSentItems = true
        };

        var response = await _httpClient.PostAsync("https://graph.microsoft.com/v1.0/sendMail", 
            JsonContent.Create(graphPayload));
        
        response.EnsureSuccessStatusCode();
        return Ok();
    }

    private async Task<string> GetAccessToken()
    {
        var tokenRequest = new Dictionary<string, string>
        {
            {"grant_type", "client_credentials"},
            {"client_id", _config["AzureAd:ClientId"]},
            {"client_secret", _config["AzureAd:ClientSecret"]},
            {"scope", "https://graph.microsoft.com/.default"}
        };

        var response = await _httpClient.PostAsync(
            $"https://login.microsoftonline.com/{_config["AzureAd:TenantId"]}/oauth2/v2.0/token",
            new FormUrlEncodedContent(tokenRequest));
        
        var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>();
        return tokenResponse.AccessToken;
    }

    public class TokenResponse { public string AccessToken { get; set; } }
    public class EmailContent { public string Subject { get; set; } public string Body { get; set; } public List<string> To { get; set; } }
}

WASM前端调用代理API

@inject HttpClient Http

<button @onclick="SendEmail">发送邮件</button>

@code {
    private async Task SendEmail()
    {
        await Http.PostAsJsonAsync("/api/graph-proxy/send-email", new
        {
            Subject = "代理测试邮件",
            Body = "内容测试",
            To = new List<string> { "test@example.com" }
        });
    }
}

内容的提问来源于stack exchange,提问作者Robin Wilson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 07:47:37