VBAからGoogle APIを叩いてみる – IE11廃止対応版
Windows11では既にIE11が使えない為、VBAからのOAuth2.0認証時にIEを立ち上げて認証が出来ない為、こちらのエントリーにあるように「Puppeteerを使って認証するEXE」をNode.jsとPuppeteerを使って作り、クリアさせています。基本的には同じ仕組みなのですが、REST APIを提供するサイトによってちょっとずつ事なるので、困りものですが。
しかし、Windows10でも2022年6月には基本的にIE11が廃止となるため、このままだとGoogle APIを叩くのに認証が出来ず困ります。という事で、Google API用の認証用EXEを作成し、それをもってしてOAuth2.0を行わせ、Access Tokenを取得するものを作成しました。以下のサイトの改訂版となります。他のREST APIよりも複雑で、また以前の認証方法よりも複雑となっています。
※Googleの場合IE11対応自体を2021年10月に終了させているので、他のサイトに先んじてVBAからのIE11を使った認証も完了していました。
※index.exeを使わずに実行できる方法が出ましたので、index.exeの同梱をやめました(exeはZIPで固めてもGoogleから警告が出るので・・・)。以下のサイトを参照してみてください。
目次
今回利用するファイル等
IE11の認証と違って不安定さが無く、また64bit絡みの問題も無いので非常に軽快に動作します。また、今回のEXEで使ってるPuppeteerですが、v11.0を利用していましたが、リダイレクトする部分で「no frame for given id found」のエラーが出てしまうバグがあったので、最新版のv13を使っています。
また、今回のGoogleの認証内容変更にはよりセキュアにする為のnonceパラメータの追加などやlocalhostなどへのループバック認証の廃止、オプションが多数追加されているので、必要に応じてそれらを追加実装すると良いでしょう。
新方式が登場しました
IE11の廃止に伴い、SeleniumやNode.jsやらといった手段を使わず、またPuppeteerと同様の手法(CDPを叩く)でVBAとEdge/ChromeのみでOAuth2.0認証する手段が登場しました。スクレイピングも可能になっています。以下のエントリーを参考にしてみてください。この手法は最も制限が無く、もっともすぐれた選択肢になると思います。
事前準備
Google APIを利用するためには、Google Cloud Consoleより事前準備が必要です。また、企業内で利用する場合には、プロキシーサーバを使っているケースがあるので、そのプロキシーサーバのURLとポート番号がアクセスに必要になります。
プロキシー設定を調べる
企業内で使う場合、ウェブアクセスにプロキシーを使ってる場合には、VBAからアクセスする場合もその設定を利用する必要性があります。プロキシーを経由しなければ外に出ることができないので、プログラムが動作しません。プロキシーの設定はいろいろなパターンがありますが、一般的な設定の調べ方は以下の通り。
サーバーのアドレスとポート番号について、http://を除外して、コロンでポート番号でつなげて利用します。(例:hiroproxy.net:8080)
- コントロールパネルより「インターネットオプション」を開く
- 「接続」タブを開き、「LANの設定」を開く
- この画面でプロキシーサーバの部分にアドレスとポート名が入ってるならばこれを控えておく。
- 場合によっては、詳細設定の中の「HTTP」で指定してるサーバーアドレスとポート番号を控えておく。
- 自動構成スクリプトを使ってる場合、そこに指定されてるアドレスのファイルの中に、様々なプロキシーアドレスが入っていますので、それを一旦ダウンロードして中身をテキストエディタで開いてみる(通常はpacというファイル)
- 5.のケースの場合、pacファイル内はIF文を使ってアクセスするサイト別にプロキシーが設定されてることが多いので、もっとも一般的なサイトアクセスもしくはGoogleについてだけ定義している場合には、そのサーバーアドレスとポート番号を控えておく。
図:プロキシー設定がない場合はこの作業は不要です。
Google側の設定を行う
ここでは、クライアントIDとクライアントシークレットを取得します。また、Google Apps Script APIを有効にします。以下の手順で取得しましょう。今回使用するスプレッドシートを開いて作業を行います。
- Google Cloud Consoleの認証情報作成を開く
- 認証情報を作成をクリックする
- OAuthクライアントIDを選択する
- クライアントIDの作成では、「ウェブアプリケーション」を選択する
- 承認済みのリダイレクト URIはVBA側と同じリダイレクトURLを指定します。
- 作成ボタンを押すと、クライアントIDとクライアントシークレットが手に入るので控えておく。
- Cloud Consoleを閉じ、スクリプトエディタの画面ではOKを押して閉じる
これで必要な情報の半分が手に入りました。この2つは大切なものなので、漏れたりしないように保存しておく必要があります。また、今回のケースは内部利用目的なので、OAuth同意画面に於いては、ユーザの種類は「内部」とし、スコープでは、VBA側で指定してるものを指定しています。
※今回はOAuth同意画面の名称は「G Suite Applications」にしています
図:クライアントID、シークレット取得しておきましょう。
図:OAuth同意画面の設定
認証を実行するコード
ここまでの情報でOAuth2.0認証を実行し、Access Token他を取得する準備が整いました。今回はAccessを利用しているので、Access Token、Refresh Token、expireする時間を取得してテーブルに格納します(本来は第三者が容易にこのコードを知られないようにする仕組みを用意しましょう)。IE11で認証するケースとはちょっと異なり、今回はNode.js + Puppeteer + pkgにてWindows用のEXEを作っており、VBAから叩いてAuthcodeを取得する所まではそちらで対応しています。
ソースコード
VBA側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
'OAuth認証用 Private Const client_id As String = "ここにクライアントIDを入れる" Private Const client_secret As String = "ここにクライアントシークレットを入れる" Private Const oauthurl As String = "https://accounts.google.com/o/oauth2/v2/auth?" Private Const tokenurl As String = "https://oauth2.googleapis.com/token" Private Const grant_type As String = "authorization_code" Private Const response_type As String = "code" Private Const redirecturl As String = "ここにリダイレクトURLを入れる" Private Const scope As String = "openid profile email https://www.googleapis.com/auth/spreadsheets" '半角スペースで区切る 'プロキシURL Const proxyuri As String = "ここにプロキシーURLを入れる" 'Puppeteerで認証コードを実行する Public Function googleAuthorization() 'iniファイルからidとpassを読み込み Dim authcode As String Dim tokenflg As Variant 'WSHの用意 Dim WSH, wExec, sCmd As String, Result As String Set WSH = CreateObject("WScript.Shell") '認証用URLを構築 Dim oauthpage As String Dim param As String 'OAuth認証用URLの組み立て oauthpage = oauthurl & _ "access_type=offline" & "%26" & _ "prompt=consent" & "%26" & _ "response_type=code" & "%26" & _ "State=R91556" & "%26" & _ "redirect_uri=" & redirecturl & "%26" & _ "client_id=" & client_id & "%26" & _ "scope=" & EncodeURL(scope) 'コマンドラインの組み立てと実行 sCmd = CurrentProject.Path & "\index.exe -g " & oauthpage Set wExec = WSH.Exec("%ComSpec% /c " & sCmd) 'ステータスを見てループ Do While wExec.status = 0 DoEvents Loop '標準出力内容を取得 authcode = wExec.StdOut.ReadAll 'Access Tokenを取得する tokenflg = GetAccessToken2(authcode) '終了処理 If tokenflg = True Then 'Access Tokenを取得できた MsgBox "認証が完了しました。" Exit Function Else 'Access Token取得失敗 MsgBox "認証は失敗しましたよ!!残念!" Exit Function End If '終了処理 Set wExec = Nothing Set WSH = Nothing End Function 'Access_Token他を取得するコード Private Function GetAccessToken2(ByVal auth_code As String) As Boolean Dim access_token As String Dim Json As String Dim dat As Variant Dim jsonstr As Object Dim ret As Variant Dim SQL As String Dim db As DAO.Database Dim rs As DAO.Recordset Set db = CurrentDb() Set rs = db.OpenRecordset("tokeninfo", dbOpenDynaset) 'JSONをパースする用の変数 Dim doc, jsn 'HTMLDocumentを取得 Set doc = CreateObject("HtmlFile") 'scriptタグを追加 doc.write "<script>document.JsonParse=function (s) {return eval('(' + s + ')');}</script>" access_token = "" '初期化 'Access_Token取得用POST文字列の組み立て dat = "code=" & auth_code & "&" & _ "client_id=" & client_id & "&" & _ "client_secret=" & client_secret & "&" & _ "redirect_uri=" & redirecturl & "&" & _ "grant_type=" & grant_type 'POST通信でAccess Token等をリクエスト With CreateObject("WinHttp.WinHttpRequest.5.1") .Open "POST", tokenurl, False .setProxy 2, proxyuri .setRequestHeader "Content-Type", "application/x-www-form-urlencoded;charset=UTF-8" .send dat If .status = 200 Then '返ってきたJSON文字列を取得 Json = .responseText If Len(Trim(Json)) > 0 Then 'JSON文字列より各種値を取得・保存 'パース関数でJSONオブジェクトを取得 Set jsn = doc.JsonParse(Json) access_token = jsn.access_token Debug.Print Json 'tokenをsettingテーブルに保存 With rs .FindFirst "ID=1" .Edit !access_token = jsn.access_token !refresh_token = jsn.refresh_token !expire_in = jsn.expires_in !getdate = Now() !expiredate = DateAdd("s", jsn.expires_in, Now()) .Update End With rs.Close: Set rs = Nothing db.Close: Set db = Nothing GetAccessToken2 = True Else GetAccessToken2 = False End If Else GetAccessToken2 = False End If End With End Function |
- oauthurlおよびtokenurlがこれまでのものから変更になっていますので要注意です。
- oauthpageのURL構築内容が変更になっており、access_type=offlineがここに入っていないと「refresh_token」が返ってきません。(これまではAccess Token取得側で指定していました)
- また、同様に新規に「prompt=consent」を入れておかないと、最初の1回目以降、「refresh_token」が返ってきません(access_typeが指定されていても返ってこない)。無い場合で認証後、再度認証を行ってrefresh_tokenを取得したい場合は、Googleアカウントのアカウントにアクセスできるアプリから、OAuth同意画面で設定してる名称の「G Suite Applications」を削除しないといけません。
- getAccessToken2の関数内では、access_type=offlineは無効なのでURL構築では入れない
- 出力を受け取ったら、今回はDebug.Printで表示していますが、次の「Access Token」を取得する関数へ渡します。
Node.js側コード
今回は、command-line-argsおよびpuppeteer-coreを利用して、VBAからのコマンドライン引数で認証用URLとリダイレクトURLの2つを取得し、PuppeteerでGoogle APIの認証⇒Authenticate Codeの取得までを担当させています。それ以降はVBA単体で行えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
//使用するモジュール const puppeteer = require('puppeteer-core'); var fs = require('fs'); const path = require("path"); var shell = require('child_process').exec; var spawnSync = require('child_process').spawnSync; const commandLineArgs = require('command-line-args'); //edge/chromeのパスを取得(ユーザ権限インストール時) const userHome = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"]; var kiteipath = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"; var temppath = path.join(userHome, "AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"); var edgepath = "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"; //chrome場所判定 if (fs.existsSync(kiteipath)) { chromepath = kiteipath } else { if (fs.existsSync(temppath)) { chromepath = temppath; } else { //Chromium Edgeの場合に対応 if(fs.existsSync(edgepath)){ chromepath = edgepath; }else{ //IEを起動してChromeのインストールを促す shellexec('start "" "iexplore" "https://www.google.co.jp/chrome/"') return; } } } //コマンドライン引数を取得 const optionDefinitions = [ { name: 'geturl', alias: 'g', type: String } ]; const args = commandLineArgs(optionDefinitions); //puppeteer実行 main(); //puppeteerメイン関数 async function main() { const browser = await puppeteer.launch({ headless: false, executablePath: chromepath, ignoreDefaultArgs: ["--guest",'--disable-extensions','--start-fullscreen','--incognito',], slowMo:100, }); //URLを取得 var urlman = args.geturl; //urlを取得する var url = await decodeURIComponent(urlman); //redirecturlを取得する var redirecturl = await url.substr(url.indexOf('redirect_uri=') + 13); redirecturl = await redirecturl.split("&"); var redirect = redirecturl[0] //pageを定義 const page = await browser.newPage() const navigationPromise = page.waitForNavigation() //ログインページを開く const response = await page.goto(url) await page.setViewport({ width: 900, height: 900 }) await navigationPromise //特定要素が出てくるまでウェイト await page.waitForSelector('#identifierId'); //リダイレクトURLが含まれているかチェックし、含まれるまでループ(二段階認証対応) var rcheck = -1; var ret = ""; while(rcheck == -1){ //再度、URLを取得 var allPages = await browser.pages(); ret = allPages[1].url(); //リダイレクトURLが含まれているかチェックし、含まれるまでループ rcheck = await ret.indexOf(redirect); //rcheckが-1の場合はウェイトを入れる if(rcheck == -1){ await sleep(5000); }else{ await navigationPromise } } //code=以降の文字列を取得する var cutman = await ret.substr(ret.indexOf('code=') + 5); cutarr = await cutman.split("&"); //Authenticate Codeを出力する console.log(cutarr[0]); //ブラウザを閉じる await browser.close() } //スリープ用関数 function sleep(milliSeconds) { return new Promise((resolve, reject) => { setTimeout(resolve, milliSeconds); }); } |
- 今回はコマンドライン引数は、1つ取得させています。
- リダイレクトURLは1つ目の引数から分解して取得しています。
- 受け取った引数のうち、%26の文字は「&」に変換します。理由はそのままVBA側から&で渡すと引数が壊れる為。同じく%20は半角スペースに変換するために、一括でdecodeURIComponentにてURLデコード処理をしています。
- 成功するとリダイレクト先のURLにcode=でAuthenticate Codeが付与されている。このURLは2個目のタブにあるURLなので、allPages[1].url()で取得させています。response.request().redirectChain()などでは取れないので、要注意(responseの中にはそれっぽいURLがいるのだけれど)
- code=以下を取得させていますが、ここは配列になっているので、ここからcode=の部分だけを取得させています。
- 取り出したら、console.logで出力するとVBA側で拾ってくれる
- ウェイトはsleep関数だけで行っています。
- commandLineArgsというモジュール、非常に癖というかバグが強めで、2つ目の引数にリダイレクトURLを今回渡したにも関わらず、きちんとVBAからは受け取れていなかったり(BoxやGraphでは問題なく受け取れてるのだが)
単一実行ファイルを作成する
Node.js 18よりSingle executable applicationsという機能が装備され、標準で単独実行ファイルが作成できるようになりました。結果pkgはプロジェクト終了となっています。よって、以下のエントリーの単一実行ファイルを作成するを参考に、Node18以降はexeファイルを作成することが可能です。
pkgコマンドの注意点
前述のNode.jsとPuppeteer-coreで作ったアプリをEXE化するものとして、pkgを利用しています。類似のものにnexeがあります。最新版pkgでは、Node.js v14に対応しているものの、別の環境でビルドをしようとしたらGitやらpatch.exeやらNASMやらが入っていないとエラー?になった為、ちょっと癖があるプログラムです。
よって、今回はpkg@5.3.3およびNode.js 14.17.5でビルドしています。ビルドする時は以下のコマンドでビルドしないと、evaluate関数あたりでエラーが出てしまうので、注意。
1 |
pkg index.js -t win --public |
作成してできたindex.exeをindex-win.exeにリネームして今回のVBAからは呼び出させています。
nexeコマンドの注意点
同じくEXEを作るnexeですが、keytarなどのネイティブモジュールをパックしてくれなくなってしまったのと、インストール自体がかなり面倒な事になっていたので、改めてここにインストール方法とビルドの注意点を。
注意点としてv3.3.3の時点では
- nexeは自身が用意してる版のNode.jsでしか対応していないので、ビルドする場合にはNode.jsのバージョンを合わせる必要があります(現時点では、v14.15.3まで対応)
- コマンドラインは、nexe index.jsでビルドが出来ます。
- プロキシがある場合には、実行する為にnexeがプロキシを透過できるようにする必要があります以下のコマンドを入れて、npmにプロキシ通過の設定をしておくこと
12npm config set https-proxy https://hogehoge.com:8080npm config set proxy https://hogehoge.com:8080 - keytarなどのネイティブモジュールはEXEにパックされないので、node_modulesのフォルダ毎作成したexeの配布が必要。但し、node_modulesの中はkeytarのフォルダのみあればOK
また、command-line-argsなどにコマンドラインパラメータを渡すようなケースでは、VBAで使う場合はメアド、URLなどの文字列は必ず、Chr(34)などで対象の文字列を括ってからでないと、Account is Requiredというエラーが出て止まります(keytarが原因)。
認証を実行する
ここまでで、OAuth2.0認証のAuthenticate Codeの取得までが装備出来ているので、VBA側で「googleAuthorization」を実行します。今回のは認証までのコードのみしか入っていませんが、ソレ以降のGoogle APIを叩くコードやrefresh tokenでAccess Tokenを交換するコードはこれまでと同じです。
実行すると
- 用意しておいた認証用URLとリダイレクトURLをindex.exeに引き渡す
- Puppeteerにてインストール済みのChromeもしくはChromium Edgeが起動し認証ページが開かれる
- ログインをして、承認を実行する
- リダイレクトURL先にcode=付きのURLで飛ばされて、Authenticate Codeを取り出す
という作業が行われます
図:承認画面
こんにちは
GoogleAPIにとても困っていたので、欲しいコードの発見に喜んでおります。
しかし
Google APIを装備したAccessファイル(認証用EXE入り)
↑のリンク先が「このアイテムは Google の利用規約に違反しているため、アクセスできません。」との表示になり、肝心のINDEX.exeファイルが入手できません。
エラーとならないよう、何か修正をかけていただけたら助かります。
よろしくお願いいたします
exeファイルはZIPで固めても警告が出るようになったので、accdbファイルのみに差し替えました。
また、index.exeを使わないで認証する方法が登場したので、以下のページを参照して実装してみてください)
https://officeforest.org/wp/2024/01/11/vba%e3%81%a7oauth2-0%e8%aa%8d%e8%a8%bc-%e6%96%b0%e6%96%b9%e5%bc%8f%e3%82%92%e8%a9%a6%e3%81%97%e3%81%a6%e3%81%bf%e3%81%9f/