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,避免暴露客户端密钥:
- 在Azure AD中注册SPA应用,重定向URI设置为
https://localhost:7253/authentication/login-callback(根据你的端口调整) - 安装NuGet包:
Microsoft.Authentication.WebAssembly.Msal - 配置MSAL(Program.cs)
builder.Services.AddMsalAuthentication(options => { builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); options.ProviderOptions.DefaultAccessTokenScopes.Add("https://graph.microsoft.com/Mail.Send"); });
- 配置appsettings.json
{ "AzureAd": { "Authority": "https://login.microsoftonline.com/你的租户ID", "ClientId": "你的客户端ID", "ValidateAuthority": true } }
- 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
相关产品推荐
相关产品推荐

