0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

サービスアカウントだけでSearch Consoleの所有権確認からsitemap送信までAPIで完結させる

0
Posted at

この記事は Zenn に公開した記事の再掲です(原文・最新版: https://zenn.dev/acs_developer/articles/search-console-service-account-site-verification )。

結論

新しく立ち上げた静的サイトについて、Search Console への登録を サービスアカウントだけで API 経由で完結 させました。所有権確認から sitemap 送信までが対象です。

人手で行った操作は、GCP コンソールで Site Verification API を「有効にする」1タップだけです。管理画面をブラウザで自動操作する方法は使わず、API の結果を正としました。

呼び出した順番と、各段で記録した結果は次のとおりです。

順 処理 API 実測
1 FILE方式のトークン取得 Site Verification POST /token (確認ファイル名を取得)
2 確認ファイルを公開ディレクトリへ置いてデプロイ — —
3 所有権確認 POST /webResource?verificationMethod=FILE HTTP 200
4 人間の Google アカウントを owners に追加 PUT /webResource/{id} —
5 プロパティ追加 Webmasters v3 PUT sites/{siteUrl} HTTP 204
6 sitemap 送信 PUT sites/{siteUrl}/sitemaps/{feedpath} HTTP 204
7 一覧で確認 GET sites permissionLevel: siteOwner

本稿では、この呼び出し順と、サービスアカウントが所有者になる仕組みを扱います。あわせて、owners に人間を足す理由と、URL エンコードで注意する点をコード付きで整理します。

前提・環境

  • 対象: 静的サイト(本稿では https://example.com/ と表記)
  • 認証: サービスアカウントの JSON 鍵
  • ライブラリ: google-auth(service_account.Credentials と AuthorizedSession)
  • 有効化した API: Site Verification API(Search Console 側の API は、以下のコードで webmasters/v3 エンドポイントとして呼んでいます)
  • スコープ: 次の2つ
    • https://www.googleapis.com/auth/siteverification
    • https://www.googleapis.com/auth/webmasters

1本のスクリプトをサブコマンドで切り替え、1段ずつ手で実行して結果を確かめながら進めました。

スクリプト全体

実際に使ったスクリプトから、鍵のパスとドメインだけを置き換えています(全44行)。

# -*- coding: utf-8 -*-
"""Search Console をサービスアカウントで登録・所有権確認する。
使い方: python3 gsc_api.py token   → FILE 方式のトークン(ファイル名)を表示
        python3 gsc_api.py verify  → 所有権確認(トークンファイルが本番にある前提)
        python3 gsc_api.py owner <email> → 確認済みサイトの所有者に Google アカウントを追加
        python3 gsc_api.py add    → webmasters sites.add+sitemap 送信+一覧
"""
import sys, json
from google.oauth2 import service_account
from google.auth.transport.requests import AuthorizedSession

KEY = 'path/to/service-account.json'  # 実パスは非公開
SITE = 'https://example.com/'
SCOPES = ['https://www.googleapis.com/auth/siteverification',
          'https://www.googleapis.com/auth/webmasters']
creds = service_account.Credentials.from_service_account_file(KEY, scopes=SCOPES)
s = AuthorizedSession(creds)
SV = 'https://www.googleapis.com/siteVerification/v1'
WM = 'https://www.googleapis.com/webmasters/v3'

def show(r):
    try: print(r.status_code, json.dumps(r.json(), ensure_ascii=False)[:800])
    except Exception: print(r.status_code, r.text[:300])

cmd = sys.argv[1]
if cmd == 'token':
    r = s.post(f'{SV}/token', json={'site': {'identifier': SITE, 'type': 'SITE'}, 'verificationMethod': 'FILE'})
    show(r)
elif cmd == 'verify':
    r = s.post(f'{SV}/webResource?verificationMethod=FILE', json={'site': {'identifier': SITE, 'type': 'SITE'}})
    show(r)
elif cmd == 'owner':
    email = sys.argv[2]
    r = s.get(f'{SV}/webResource'); show(r)
    items = r.json().get('items', [])
    me = [i for i in items if i['site']['identifier'].rstrip('/') == SITE.rstrip('/')]
    if not me: print('not verified yet'); sys.exit(1)
    item = me[0]; owners = item.get('owners', [])
    if email not in owners: owners.append(email)
    r = s.put(f"{SV}/webResource/{item['id']}", json={'id': item['id'], 'site': item['site'], 'owners': owners}); show(r)
elif cmd == 'add':
    r = s.put(f'{WM}/sites/{SITE.replace("/", "%2F").replace(":", "%3A")}'); print('sites.add', r.status_code, r.text[:200])
    r = s.put(f'{WM}/sites/{SITE.replace("/", "%2F").replace(":", "%3A")}/sitemaps/{(SITE + "sitemap.xml").replace("/", "%2F").replace(":", "%3A")}'); print('sitemap', r.status_code, r.text[:200])
    r = s.get(f'{WM}/sites'); show(r)

各段の中身

1. token: FILE方式の確認ファイル名をもらう

POST /token に site.type = 'SITE' と verificationMethod = 'FILE' を渡すと、確認用のトークンが返ります。FILE 方式の場合、このトークンが確認ファイルの名前(google<トークン>.html の形)になります。

このファイルを静的サイトの公開ディレクトリに置き、デプロイしました。トークンの実値は本稿には載せていません。

2. verify: 確認ファイルが公開されてから呼ぶ

POST /webResource?verificationMethod=FILE で所有権確認を行い、HTTP 200 が返りました。

ここで重要なのは、確認を実行したのがサービスアカウント自身だという点です。この API では、確認を実行した主体が、その Web リソースの所有者として登録されます。言い換えると、この時点で所有者になっているのはサービスアカウントであり、人間のアカウントではありません。

verify は Google 側が確認ファイルを取りに来る処理です。そのため、デプロイが反映される前に呼ぶ順番にはしないでください。docstring に「トークンファイルが本番にある前提」と書いているのはこのためです。

3. owner: 人間の Google アカウントを owners に足す

サービスアカウントだけが所有者のままでは、普段使う Google アカウントで Search Console を開いても、このサイトを扱えません。そこで webResource の owners に人間のアカウントを追加しました。

コードで気をつけた点は2つです。

  • 既存の owners を取得してから追記し、配列ごと PUT する。 PUT はリソースを置き換える操作なので、追加したいアカウントだけを送るのではなく、取得した既存リストに追記したものを送っています。
  • サイトの照合では末尾スラッシュを揃える。 GET /webResource の一覧から、identifier.rstrip('/') で比較して対象を選んでいます。

一覧に対象が無い場合は、not verified yet を出して止めます。verify より前に誤って実行しても、何も書き換わりません。

4. add: sites.add と sitemap 送信

Search Console 側には、PUT sites/{siteUrl} でプロパティを追加しました。結果は HTTP 204 です。続けて PUT sites/{siteUrl}/sitemaps/{feedpath} で sitemap.xml を送信し、こちらも HTTP 204 でした。どちらもレスポンスボディは無いので、成否はステータスコードで判断しています。

最後に GET sites を実行し、permissionLevel が siteOwner になっていることを確認して完了としました。

URL エンコードの要点

Webmasters v3 では、siteUrl と feedpath を URL パスの一部として 埋め込みます。どちらも https://example.com/ のように : と / を含むため、そのまま連結すると、パスの区切りとして解釈されてしまいます。

今回のコードでは、この2文字を手で置き換えています。

enc = lambda u: u.replace("/", "%2F").replace(":", "%3A")
# https://example.com/            → https%3A%2F%2Fexample.com%2F
# https://example.com/sitemap.xml → https%3A%2F%2Fexample.com%2Fsitemap.xml

注意点は、feedpath を 相対パス sitemap.xml ではなく、サイト URL を含む完全な URL にしてからエンコードしていることです。

今回のドメインは : と / 以外の記号を含まないため、この置換だけで足りました。クエリ文字列などを含む URL を扱う場合は、urllib.parse.quote(url, safe='') のように、汎用のエンコード関数を使うほうが安全だと考えられます(今回はこちらでは検証していません)。

なお、Site Verification 側の identifier は JSON ボディで渡すため、エンコードは不要です。

まとめ

  • 人手で行う必要があったのは、GCP で Site Verification API を有効化する1タップだけ でした。それ以外は、サービスアカウントの鍵とスコープ2つで完結します。
  • 呼び出し順は token → 確認ファイルのデプロイ → verify(200) → owners 追加 → sites.add(204) → sitemap 送信(204) → sites.list です。verify はデプロイの反映後に呼びます。
  • verify を実行した サービスアカウントが所有者になる ため、人間が管理画面で扱えるよう owners に追記します。PUT は置き換えなので、既存リストを取得してから追記します。
  • Webmasters v3 の siteUrl と feedpath はパスに埋め込むため、: と / をエンコードします。feedpath はサイト URL を含めた完全な形で渡します。
  • 確認は管理画面ではなく、sites.list の permissionLevel: siteOwner で行うと、状態をスクリプト内で閉じて確かめられます。

検証の続きや関連ツールは ACS Developer で公開しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?