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

.NET Framework 4.8部署Gmail API时GoogleWebAuthorizationBroker授权失败

.NET Framework 4.8应用部署服务器后Gmail OAuth授权失败

问题描述

基于.NET Framework 4.8开发的应用,需关联用户Gmail账户并访问邮件内容。本地环境使用GoogleWebAuthorizationBroker配合桌面OAuth凭证可正常运行,但部署到服务器后出现授权失败错误,提示无法启动浏览器打开OAuth授权链接且返回“Access is denied”。已在Google开发者控制台创建Web和桌面应用的OAuth 2.0凭证,了解到Google.Apis.Auth.AspNetCore3适用于.NET Core而非.NET Framework,寻求解决办法。

错误信息

System.AggregateException: One or more errors occurred. ---> System.NotSupportedException: Failed to launch browser with "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&code_challenge=tDq__xCgypmYKgfwjD0nAcV7mMU6rWMS2Hm_UqPqbTY&code_challenge_method=S256&response_type=code&client_id=180884391537-6lvtccf5rot6a124v9qb4coilhmtv3fe.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A64176%2Fauthorize%2F&scope=https%3A%2F%2Fmail.google.com%2F%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.modify%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.send%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email" for authorization. See inner exception for details. ---> System.ComponentModel.Win32Exception: Access is denied
   at System.Diagnostics.Process.StartWithShellExecuteEx(ProcessStartInfo startInfo)
   at System.Diagnostics.Process.Start()
   at System.Diagnostics.Process.Start(ProcessStartInfo startInfo)
   at Google.Apis.Auth.OAuth2.LocalServerCodeReceiver.OpenBrowser(String url)
   at Google.Apis.Auth.OAuth2.LocalServerCodeReceiver.<ReceiveCodeAsync>d__13.MoveNext()
   --- End of inner exception stack trace ---
   at Google.Apis.Auth.OAuth2.LocalServerCodeReceiver.<ReceiveCodeAsync>d__13.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.AuthorizationCodeInstalledApp.<AuthorizeAsync>d__8.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.<AuthorizeAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.<AuthorizeAsync>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.<AuthorizeAsync>d__1.MoveNext()
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()

相关代码

private static readonly string credentialsPathDesktop = HttpContext.Current.Server.MapPath("~/App_Data/IntegracionesCRM/Credentials/Google/client_secrets.json");
private static readonly string credentialsPathWebApp = HttpContext.Current.Server.MapPath("~/App_Data/IntegracionesCRM/Credentials/Google/client_secret_webApp.json");
private static readonly string[] Scopes = { Scope.MailGoogleCom, Scope.GmailModify, Scope.GmailSend, "https://www.googleapis.com/auth/userinfo.email" };

// Authorizing a service account with workspace
public LoadServiceGoogle LoadGoogleCredentials(int gdtnegociotipo)
{
    context.Configuration.LazyLoadingEnabled = false;
    var tipo = context.gdtnegociotipoes.SingleOrDefault(x => x.gdtnegociotipoid == gdtnegociotipo);

    using (var stream = new FileStream(credentialsPathDesktop, FileMode.Open, FileAccess.Read))
    {
        var clientSecrets = GoogleClientSecrets.FromStream(stream).Secrets; // Accede a la propiedad Secrets del objeto GoogleClientSecrets

        try
        {
            // Genering a random user id to avoid conflicts
            string userIdPrefix = "user_";
            string userId = userIdPrefix + Guid.NewGuid().ToString();
            string tokensPath = HttpContext.Current.Server.MapPath(string.Format("~/App_Data/IntegracionesCRM/Tokens/TiposID/{0}", tipo.gdtnegociotipoid));

            UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                                   clientSecrets,
                                   Scopes,
                                   userId,
                                   CancellationToken.None,
                                   new FileDataStore(tokensPath)).Result;

            return new LoadServiceGoogle { credential = credential, tokensFolderPath = tokensPath, credentialsPath = credentialsPathDesktop, userId = userId };
        }
        catch (Exception ex)
        {
            throw new SecurityAccessDeniedException("Error al obtener las credenciales de Google", ex);
        }
    }
}

解决方案

核心原因

GoogleWebAuthorizationBroker是专为桌面应用设计的授权组件,它会尝试在本地启动浏览器完成OAuth授权流程。但服务器通常是无桌面交互的环境,且运行应用的系统账户没有权限启动浏览器,因此触发“Access is denied”错误。

具体解决步骤

  1. 切换为Web应用OAuth凭证

    • 替换代码中读取的凭证文件为Web应用的client_secret_webApp.json,而非桌面版凭证。
    • 在Google开发者控制台中,为Web应用凭证配置授权重定向URI,填写服务器域名下的回调地址(例如https://你的域名.com/google-auth-callback)。
  2. 手动实现Web应用OAuth授权流程
    对于.NET Framework的Web应用,需手动完成OAuth 2.0授权码流程,分为三步:

    • 引导用户到Google授权页面
      构造授权URL并将用户重定向到该地址,获取授权码:
      var clientSecrets = GoogleClientSecrets.FromStream(new FileStream(credentialsPathWebApp, FileMode.Open, FileAccess.Read)).Secrets;
      var authorizationUrl = $"https://accounts.google.com/o/oauth2/v2/auth?client_id={clientSecrets.ClientId}&redirect_uri={Uri.EscapeDataString("https://你的域名.com/google-auth-callback")}&scope={Uri.EscapeDataString(string.Join(" ", Scopes))}&access_type=offline&response_type=code";
      HttpContext.Current.Response.Redirect(authorizationUrl);
      
    • 处理回调交换令牌
      在回调页面中,从请求参数获取code,调用Google令牌端点交换access_token和refresh_token:
      var code = HttpContext.Current.Request.QueryString["code"];
      var tokenRequestData = new Dictionary<string, string>
      {
          {"code", code},
          {"client_id", clientSecrets.ClientId},
          {"client_secret", clientSecrets.ClientSecret},
          {"redirect_uri", "https://你的域名.com/google-auth-callback"},
          {"grant_type", "authorization_code"}
      };
      
      using (var httpClient = new HttpClient())
      {
          var response = await httpClient.PostAsync("https://oauth2.googleapis.com/token", new FormUrlEncodedContent(tokenRequestData));
          var tokenContent = await response.Content.ReadAsStringAsync();
          // 解析tokenContent,提取access_token、refresh_token和过期时间
          // 示例解析:可使用Newtonsoft.Json反序列化为TokenResponse对象
          var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(tokenContent);
      }
      
    • 初始化Gmail服务
      用获取到的令牌创建UserCredential,进而初始化Gmail服务:
      var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
      {
          ClientSecrets = clientSecrets,
          Scopes = Scopes
      });
      
      var credential = new UserCredential(flow, userId, tokenResponse);
      var gmailService = new GmailService(new BaseClientService.Initializer
      {
          HttpClientInitializer = credential,
          ApplicationName = "你的应用名称"
      });
      
  3. 持久化令牌
    将refresh_token和用户关联信息存储到数据库或安全的文件系统中,后续可通过refresh_token自动刷新access_token,无需用户重复授权。

  4. 配置服务器权限
    确保服务器上运行应用的账户拥有令牌存储目录的读写权限,避免令牌保存或读取时出现权限异常。

内容的提问来源于stack exchange,提问作者Daniel Mauricio Valdes Cabrera

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 19:24:52