如何使用C#中的服务账户登录Google API - 凭据无效。

36

我正在尝试使用C#登录Google API和Google Analytics的简单服务账户,但却让自己疲惫不堪。我的公司已经将数据导入Analytics,并且我可以使用他们的Query Explorer查询信息,但在.Net上的入门并没有取得任何进展。根据文档,我正在使用由Google生成的带有PKI的json文件,因为这样的服务账户是计算机之间通信的正确方式。以下是代码片段:

public static GoogleCredential _cred;
public static string _exePath;

static void Main(string[] args) {
    _exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase).Replace(@"file:\", "");
    var t = Task.Run(() => Run());
    t.Wait();
}

private static async Task Run() {
    try {
        // Get active credential
        using (var stream = new FileStream(_exePath + "\\Default-GASvcAcct-508d097b0bff.json", FileMode.Open, FileAccess.Read)) {
            _cred = GoogleCredential.FromStream(stream);
        }
        if (_cred.IsCreateScopedRequired) {
        _cred.CreateScoped(new string[] { AnalyticsService.Scope.Analytics });
        }
        // Create the service
        AnalyticsService service = new AnalyticsService(
            new BaseClientService.Initializer() {
                HttpClientInitializer = _cred,
            });
        var act1 = service.Management.Accounts.List().Execute(); // blows-up here

所有代码都能通过编译,但是当执行Execute()语句时,会抛出一个GoogleApiException错误:

[无效凭据] 地址[Authorization - header] 原因[authError] 域[全球]

我错过了什么?

6个回答

42

看起来GoogleAnalytics不能接受一个通用的GoogleCredential并将其解释为ServiceAccountCredential(即使内部承认它实际上是该类型)。因此,您必须以困难的方式创建一个ServiceAccountCredential。同时GoogleCredential也不会公开凭据的各种属性,所以我不得不自己构建。

我使用http://jsonclassgenerator.codeplex.com/上的JSON C#类生成器,使用Google API自动包含的JSON库(Newtonsoft.Json)构建了一个"个人"ServiceAccountCredential对象,并检索服务账户下载的JSON文件的关键部分,使用其电子邮件和私钥属性构造所需的凭据。将真正的ServiceAccountCredential传递给GoogleAnalytics服务构造函数,将导致成功登录并访问该帐户允许的资源。

下面是可行代码示例:

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Analytics.v3;
using Newtonsoft.Json;
    .
    .
    .
try
{
    // Get active credential
    string credPath = _exePath + @"\Private-67917519b23f.json";

    var json = File.ReadAllText(credPath);
    var cr = JsonConvert.DeserializeObject<PersonalServiceAccountCred>(json); // "personal" service account credential

    // Create an explicit ServiceAccountCredential credential
    var xCred = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(cr.ClientEmail)
    {
        Scopes = new[] {
            AnalyticsService.Scope.AnalyticsManageUsersReadonly,
            AnalyticsService.Scope.AnalyticsReadonly
        }
    }.FromPrivateKey(cr.PrivateKey));

    // Create the service
    AnalyticsService service = new AnalyticsService(
        new BaseClientService.Initializer()
        {
            HttpClientInitializer = xCred,
        }
    );

    // some calls to Google API
    var act1 = service.Management.Accounts.List().Execute();

    var actSum = service.Management.AccountSummaries.List().Execute();

    var resp1 = service.Management.Profiles.List(actSum.Items[0].Id, actSum.Items[0].WebProperties[0].Id).Execute();

有些人可能想知道由Google生成的带PKI(私钥)的服务账号凭据是什么样子。从Google APIs Manager(IAM & Admin)页面上的https://console.developers.google.com/iam-admin/projects,选择相应的项目(您至少有一个)。现在从左侧导航链接中选择服务账号,然后在屏幕顶部选择创建服务账号。填写名称,设置提供新的私钥复选框,然后单击创建。谷歌会自动下载一个JSON文件,看起来像这样:

{
  "type": "service_account",
  "project_id": "atomic-acrobat-135",
  "private_key_id": "508d097b0bff9e90b8d545f984888b0ef31",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...o/0=\n-----END PRIVATE KEY-----\n",
  "client_email": "google-analytics@atomic-acrobat-135.iam.gserviceaccount.com",
  "client_id": "1123573016559832",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-analytics%40atomic-acrobat-135923.iam.gserviceaccount.com"
}

看起来有人使用了你的代码并在他的博客上使用了它。因为你在2016年回答了这个问题,而他的博客是最近才发布的。我的问题是,我能否使用这个获取访问令牌,以便我可以在嵌入式API中使用该访问令牌。因为我得到了一个空的访问令牌。 - CyberNinja
3
我太懒了,不想使用你的类生成器工具。所以我用了这个方法:Newtonsoft.Json.Linq.JObject cr = (Newtonsoft.Json.Linq.JObject) JsonConvert.DeserializeObject(json); string s = (string) cr.GetValue("private_key"); 谢谢你的回答——它帮了我很多。 - robertpb
1
感谢dlumpp的回答,我已经确认只要正确设置范围,使用GoogleCredential.FromStream(或FromFile)是可以正常工作的。 https://stackoverflow.com/users/6753705/dlumpp - Mark Adamson
这样做会给我一个"MailKit.Security.AuthenticationException: '334: eyJzdGF0dXMiOiI0M...",但如果我使用证书就可以了!太好了... - Murphybro2
我花了一些时间来玩这个东西,突然我的密钥起作用了。出于兴趣,我改变了一些字符来看会发生什么,根据我改变的字符不同,它仍然可以工作 o_O - Murphybro2
有什么想法,我如何在BigQueryClient中使用同样的方法? - Jay Magwadiya

8
无效证书错误是因为您指定的范围没有随您的凭据一起发送。我犯了同样的错误,在CreateScoped调用后仍然看到凭据上的0个范围。 GoogleCredential是不可变的,所以CreateScoped会创建一个新实例,并设置指定的范围。
像这样重新分配带有作用域结果的凭据变量,它应该可以正常工作:
  if (_cred.IsCreateScopedRequired) {
    _cred = _cred.CreateScoped(AnalyticsService.Scope.Analytics);
  }

接受的答案是有效的,因为它以一种更加复杂的方式实现了相同的目标。

2
这个对我很有帮助,因为在 Python 的等价代码中我根本不需要设置范围。 - Mark Adamson

7
在2020年,您不需要执行所有这些操作,GoogleCredential的功能良好。问题中的代码看起来正确,除了一行代码:
credentials.CreateScoped(new string[] { DriveService.Scope.Drive });
CreateScoped 方法返回凭据的副本。如果你将其重新分配给自身,则它可以正常工作。
为了完整起见,这是我完美运行的测试代码:
using (var stream =
            new FileStream("drive-credentials.json", FileMode.Open, FileAccess.Read))
        {                
            var credentials = GoogleCredential.FromStream(stream);
            if (credentials.IsCreateScopedRequired)
            {
                credentials = credentials.CreateScoped(new string[] { DriveService.Scope.Drive });
            }


            var service = new DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credentials,
                ApplicationName = "application name",                    
            });

            FilesResource.ListRequest listRequest = service.Files.List();
            listRequest.PageSize = 10;
            listRequest.Fields = "nextPageToken, files(id, name)";

            // List files.
            IList<Google.Apis.Drive.v3.Data.File> files = listRequest.Execute()
                .Files;
        }

这个答案应该被用在Google文档中,因为他们的文档仍然加载pl12证书。即使我自己编写了解析类,我也花了一段时间才找到这个答案...现在它可以正常工作了。谢谢。 - Piotr Kula

7

对于2020年,调用方式如下:

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Google.Apis.Services;
using Google.Apis.Auth.OAuth2;
using System.IO;
using Google.Apis.Sheets.v4;
using Google.Apis.Sheets.v4.Data;

namespace SistemasInfinitos.Controllers.Google.Apis.Sample.MVC4
{
    public class SpreadsheetseController : Controller
    { 
        public ActionResult IndexAPI()
        {
            //accede a las credenciales
            var stream = new FileStream(Server.MapPath("~/quickstart2-9aaf.json"),
                FileMode.Open
               // FileAccess.Read//SOLO LECTURA
                );
            //abre las credenciales
            var credentials = GoogleCredential.FromStream(stream);

            //virifica las credenciales
            if (credentials.IsCreateScopedRequired)
            {
                credentials = credentials.CreateScoped(new string[] { SheetsService.Scope.Spreadsheets });
            }
            ///inicializa la api
        var service = new SheetsService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credentials,
                ApplicationName = "SistemasInfinitos",
            });

            // Define los parametros.  
            String spreadsheetId = "1MKxeqXV5UEMXU2yBe_xi0nwjooLhNN6Vk";
            String range = "Sheet1";
            SpreadsheetsResource.ValuesResource.GetRequest request =service.Spreadsheets.Values.Get(spreadsheetId, range);
            // imprime   
            ValueRange response = request.Execute();
            IList<IList<Object>> values = response.Values;
            ViewBag.List = values;
            return View();
        }
    }
}

视图

@{
    ViewBag.Title = "IndexAPI";
}

<div class="col-md-6">
    <h3>Read Data From Google Live sheet</h3>
    <table class="table" id="customers">
        <thead>
            <tr>
                <th>
                    id
                </th>
                <th>
                    Name
                </th>
            </tr>
        </thead>
        <tbody>
            @{
                foreach (var item in ViewBag.List)
                {
                    <tr>
                        <td>@item[0]</td>
                        <td>@item[1]</td>
                    </tr>

                }
            }
        </tbody>

    </table>
</div>

1

另一种选择是使用GoogleCredential.GetApplicationDefault()。我相信这是目前(2018年10月)推荐的方法。下面是一些F#代码,但在语法上与C#基本相同:

let projectId = "<your Google Cloud project ID...>"
let creds =
  GoogleCredential.GetApplicationDefault()
    .CreateScoped(["https://www.googleapis.com/auth/cloud-platform"])
use service =
  new CloudBuildService(
    BaseClientService.Initializer(HttpClientInitializer=creds))
let foo = service.Projects.Builds.List(projectId).Execute()

现在,请确保将GOOGLE_APPLICATION_CREDENTIALS设置为指向凭据JSON文件的文件,例如:GOOGLE_APPLICATION_CREDENTIALS=creds.json dotnet run

0

如果您在尝试确定如何创建ServiceAccountCredential而不直接使用密钥文件的情况下到达此处,您可能会对以下方法感兴趣(有时会起作用):

GoogleCredential credential = GoogleCredential.GetApplicationDefault();

ServiceAccountCredential serviceAccountCredential = 
    credential.UnderlyingCredential as ServiceAccountCredential;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接