.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”错误。
具体解决步骤
切换为Web应用OAuth凭证
- 替换代码中读取的凭证文件为Web应用的
client_secret_webApp.json,而非桌面版凭证。 - 在Google开发者控制台中,为Web应用凭证配置授权重定向URI,填写服务器域名下的回调地址(例如
https://你的域名.com/google-auth-callback)。
- 替换代码中读取的凭证文件为Web应用的
手动实现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 = "你的应用名称" });
- 引导用户到Google授权页面
持久化令牌
将refresh_token和用户关联信息存储到数据库或安全的文件系统中,后续可通过refresh_token自动刷新access_token,无需用户重复授权。配置服务器权限
确保服务器上运行应用的账户拥有令牌存储目录的读写权限,避免令牌保存或读取时出现权限异常。
内容的提问来源于stack exchange,提问作者Daniel Mauricio Valdes Cabrera
相关产品推荐
相关产品推荐

