AccessでGoogle認証とファイルのダウンロード

現在、VBAで業務用アプリケーションを作成していますが、アップデータの配布に於いてイントラネット内に配置ですと、外部ではアップデートが出来ないことになります。かといって、VPNの設定などをして入れるようにするというのも考えもの。ということで、アップデータの配布にG SuiteのGoogle Driveを使えないか?

ということで、VBAにてGoogle認証をし、Drive APIを叩いてファイルをダウンロードする手段を考えました。Client IDや
Client Secretがあっても今回の手法の場合、組織内のアカウントでない場合、認証が通らずダウンロードが出来ません。また、この手法の場合、ファイルへのアクセス権が全くなくとも、ファイルオーナーの自分だけはダウンロードが可能になったりします。とても便利。今回はAccess VBAにて作成しています。

今回使用するファイルや資料

新方式が登場しました

IE11の廃止に伴い、SeleniumやNode.jsやらといった手段を使わず、またPuppeteerと同様の手法(CDPを叩く)でVBAとEdge/ChromeのみでOAuth2.0認証する手段が登場しました。スクレイピングも可能になっています。以下のエントリーを参考にしてみてください。この手法は最も制限が無く、もっともすぐれた選択肢になると思います。

VBAでOAuth2.0認証 - 新方式を試してみた

IE11を使わない認証対応版

IE11廃止に伴い、本ページの認証部分については、IE11を使わない認証方法を利用する必要があります。この部分について、対応版を作成しました。以下のエントリーを参考に、Puppeteerを使った認証を利用すると、2022年6月以降も、VBAでGoogle API認証が可能になります。

VBAからGoogle APIを叩いてみる – IE11廃止対応版

事前準備

プロジェクトの作成

今回は、Google Apps Scriptは使用しません。が、Google Drive APIは必要になります。そのため、Google Cloud PlatformにてDrive APIの有効化とClient IDおよびClient Secretが必要になります。以下の手順でプロジェクトを作成します。

  1. Google Cloud Platformにアクセスする
  2. ページの上部、Cloud Platformの文字の横にある▼をクリックする
  3. 出てきたダイアログの右上、「新しいプロジェクト」をクリックする
  4. プロジェクト名を設定し、作成ボタンをクリックします。
  5. 左サイドバーのAPIとサービスをクリックします。
  6. 検索にて、Drive APIを検索し、有効にするをクリック。
  7. 左サイドバーの認証情報をクリックします。
  8. 認証情報を作成をクリックします。
  9. OAuthクライアントIDを選択します。
  10. OAuth クライアント ID を作成するには、まず同意画面でサービス名を設定する必要があります。」と出るので、同意画面を設定をクリック。
  11. OAuthの同意画面では、アプリケーションの種類を今回は「内部」にしておきます。これで組織外のユーザは認証出来ません。
  12. アプリケーション名を適当に設定し、保存をクリック。
  13. 次の画面では、アプリケーションの種類を選びます。ここでは、「その他」を選択します。名前は適当につけて、作成をクリックします。
  14. ここで、OAuthクライアントIDとシークレットが出るので控えておきます。

図:プロジェクトを作成しましょう。

図:組織内のユーザに限定しちゃいましょう。

図:重要なコードなので他人に開示したりしちゃだめです。

ダウンロードファイルの準備

事前にGoogle Driveにファイルをアップロードしておきます。ファイルは今回G Suite内のユーザ共有か?自分だけがアクセスできる状態にしておきます。前者であれば「リンクを知ってるxxxの全員が閲覧可」や後者であれば、「オフ - 特定のユーザだけがアクセス出来ます」の状態にしておきます。

また、アップロードが完了したら、ファイルのIDを控えておきます。ファイルIDはURLの中で、/d/の後ろから/viewの前までの文字列。これが今回VBAからダウンロードするファイルとなります。

例:https://drive.google.com/file/d/1pB38TVRqX7BnxU294RpTMhSsFkXBkwEx/view?usp=sharing

図:ファイルは限定公開の状態にしておきます。

ソースコード

OAuth認証の為のコード

Option Explicit
'--------------------------------------------------
'クライアントIDやシークレット等
'--------------------------------------------------
Public Const client_id As Variant = "ここにクライアントIDを入れてください。"
Public Const client_secret As Variant = "ここにクライアントシークレットを入れてください。"

'--------------------------------------------------
'認証用文字列、スコープ、リダイレクトURI等
'--------------------------------------------------
Private Const response_type As String = "code"
Private Const redirect_uri As String = "urn:ietf:wg:oauth:2.0:oob"
Private Const grant_type As String = "authorization_code"
Private Const scope As String = "profile email https://www.googleapis.com/auth/drive"  '半角スペースで区切る
Private Const oauthurl As String = "https://accounts.google.com/o/oauth2/auth?"
Private Const tokenurl As String = "https://www.googleapis.com/oauth2/v4/token"
Private Const reftokenurl As String = "https://accounts.google.com/o/oauth2/token"
Private Const tokeninfo As String = "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token="

Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal ms As LongPtr)

'認証の実行とAccess_Token等の保存
Public Function Authgoogle()
    Dim auth_code As String
    Dim tokenget As String
    Dim ret As String
   
    'serverAuthCodeを取得
    auth_code = GetAuthorizationCode()
    
    'Access_Tokenを取得
    tokenget = GetAccessToken(auth_code)
    
    '取得結果を表示
    If tokenget = True Then
      MsgBox "認証が完了しました。"
    Else
      MsgBox "認証に失敗しました。"
    End If
    
End Function

'認証用serverAuthCodeの取得
Public Function GetAuthorizationCode() As String
  '各種変数の宣言
  Dim ie As Object
  Dim url As String
  Dim iptCode As Object
  Dim auth_code As String
  
  Const READYSTATE_COMPLETE = 4
   
  '初期化
  Set ie = Nothing
  Set iptCode = Nothing
  auth_code = ""

  'OAuth認証用URLの組み立て
  url = oauthurl & _
        "client_id=" & client_id & "&" & _
        "response_type=" & response_type & "&" & _
        "redirect_uri=" & redirect_uri & "&" & _
        "scope=" & EncodeURL(scope)
 
  'IEを起動し認証URLを開く
  Set ie = CreateObject("InternetExplorer.Application")
  With ie
    .Visible = True
    .AddressBar = False
    .MenuBar = False
    .StatusBar = False
    .Toolbar = False
    .Width = 600
    .Height = 480
    .Navigate url
    
    '認証が実行されるまで待機
    While .Busy Or _
          .ReadyState <> READYSTATE_COMPLETE Or _
          InStr(LCase(.LocationURL), "https://accounts.google.com/o/oauth2/approval") < 1
      DoEvents
    Wend
    
    '10秒間sleepさせる(Windows10対策)
    Sleep 7000
    
    
    '認証が実行されたらserverAuthCodeを取得
    On Error Resume Next
    auth_code = .Document.getElementsByClassName("qBHUIf")(0).value
    
    'ログアウトさせる
    '.Navigate "https://accounts.google.com/o/logout"
    While .Busy Or .ReadyState <> READYSTATE_COMPLETE
      DoEvents
    Wend
    .Quit
  End With
  
  '取得したserverAuthCodeを返す
  GetAuthorizationCode = auth_code

End Function

'Access_Token他を取得するコード
Private Function GetAccessToken(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 SQL As String
  Dim db As DAO.Database
  Dim rs As DAO.Recordset
  Set db = CurrentDb()
  Set rs = db.OpenRecordset("setting", dbOpenDynaset)
  
  'JSONをパースする用の変数
  Dim doc, jsn
  'HTMLDocumentを取得
  Set doc = CreateObject("HtmlFile")
  'scriptタグを追加
  doc.Write "<script>document.JsonParse=function (s) {return eval('(' + s + ')');}</script>"

  'Access_Token取得用POST文字列の組み立て
  dat = "code=" & auth_code & "&" & _
        "client_id=" & client_id & "&" & _
        "client_secret=" & client_secret & "&" & _
        "redirect_uri=" & redirect_uri & "&" & _
        "grant_type=" & grant_type & "&" & _
        "&access_type=offline"
        
  'POST通信でAccess Token等をリクエスト
  With CreateObject("WinHttp.WinHttpRequest.5.1")
    .Open "POST", tokenurl, False
    .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)

        'tokenをsettingテーブルに保存
        With rs
          .FindFirst "ID=1"
          .Edit
          !accesstoken = jsn.access_token
          !refreshtoken = jsn.refresh_token
          .Update
        End With

        rs.Close: Set rs = Nothing
        db.Close: Set db = Nothing
        
        GetAccessToken = True
      Else
        GetAccessToken = False
      End If
    Else
      GetAccessToken = False
    End If
  End With
End Function

'Access_Tokenをチェックし、refresh_tokenで新しいAccess_Tokenを取得する関数
Public Function checkAccessToken() As Boolean
  Dim access_token As String
  Dim refresh_token As String
  Dim Json As String
  Dim dat As Variant
  Dim jsonstr As Object
  Dim ngflag As Boolean
  Dim xmlHttp  As Object
  Dim url As String
  Dim strRes As String
  Dim status As String
  Dim expirein As Integer
  
  Set xmlHttp = CreateObject("MSXML2.XMLHTTP")
   
  'DB接続用
  Dim SQL As String
  Dim db As DAO.Database
  Dim rs As DAO.Recordset
  Set db = CurrentDb()
  Set rs = db.OpenRecordset("setting", dbOpenDynaset)
  
  'JSONをパースする用の変数
  Dim doc, jsn
  'HTMLDocumentを取得
  Set doc = CreateObject("HtmlFile")
  'scriptタグを追加
  doc.Write "<script>document.JsonParse=function (s) {return eval('(' + s + ')');}</script>"
  
  '取得済みAccess_Token類を呼び出す
  With rs
    .FindFirst "ID=1"
    access_token = !accesstoken
    refresh_token = !refreshtoken
  End With

  'Access_Tokenを'tokeninfoでexpireしてるかチェック
  url = tokeninfo & access_token
  With xmlHttp
    .Open "GET", url, False
    .setRequestHeader "Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"
    .send
    strRes = .ResponseText
    status = .status
  End With

  If status = 200 Then
    'expireしていなければ200が返ってくる
    ngflag = False
    
    'expires_inが60以下ならrefreshしてしまう
    Set jsn = doc.JsonParse(strRes)
    expirein = CInt(jsn.expires_in)
    
    If expirein <= 60 Then
      ngflag = True
    Else
      ngflag = False
    End If
    
  Else
    'expireしてると400が返ってくる
    ngflag = True
  End If
    
  'ngflagをもとにrefresh_Tokenを使って新しいtokenを取得する
  'reftokenurlを使用する
  If ngflag = True Then
    'refresh_TokenでPOSTするパラメータを組み立て
    dat = "client_id=" & client_id & _
        "&client_secret=" & client_secret & _
        "&grant_type=refresh_token" & _
        "&refresh_token=" & refresh_token

    'POSTで新しいAccess_Tokenを取得する
    With xmlHttp
      .Open "POST", reftokenurl, False
      .setRequestHeader "Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"
      .send (dat)
      strRes = .ResponseText
      status = .status
    End With

    '新しく取得したAccess_Tokenを格納する
    If status = 200 Then
        'HTMLDocumentを取得
        Set doc = CreateObject("HtmlFile")
        'scriptタグを追加
        doc.Write "<script>document.JsonParse=function (s) {return eval('(' + s + ')');}</script>"
        
        Set jsn = doc.JsonParse(strRes)
        access_token = jsn.access_token
        
        With rs
          .FindFirst "ID=1"
          .Edit
          !accesstoken = access_token
          .Update
        End With

        ngflag = False
    Else
        ngflag = True
        MsgBox "再認証が必要です。再認証後再度実行してください。"
        Call Authgoogle
    End If

  End If

  '終了処理
  Set xmlHttp = Nothing
  rs.Close: Set rs = Nothing
  db.Close: Set db = Nothing
  
  '値を返す
  checkAccessToken = ngflag

End Function
'URLエンコードする為の関数
Public Function EncodeURL(ByVal str As String) As String
    
    #If Win64 Then
        '64bitの場合のコード
        Dim s As String, com As String, ret As String
        Dim ScriptFilePath As String, ExeFilePath As String
         
        ret = "" '初期化
         
        'スクリプト用コード設定
        s = "Option Explicit" & vbCrLf
        s = s & vbCrLf
        s = s & "Dim Args, ret" & vbCrLf
        s = s & vbCrLf
        s = s & "Set Args = WScript.Arguments" & vbCrLf
        s = s & "If Args.Count < 1 Then WScript.Quit" & vbCrLf
        s = s & "With CreateObject(""ScriptControl"")" & vbCrLf
        s = s & "  .Language = ""JScript""" & vbCrLf
        s = s & "  ret = .CodeObject.encodeURIComponent(Args(0))" & vbCrLf
        s = s & "End With" & vbCrLf
        s = s & "Set Args = Nothing" & vbCrLf
        s = s & "WScript.Echo ret"
         
        ScriptFilePath = VBA.Environ$("TEMP") & "\enc.vbs"
        With CreateObject("Scripting.FileSystemObject")
          With .CreateTextFile(ScriptFilePath, True)
            .Write s
            .Close
          End With
          If .FileExists(ScriptFilePath) Then
            ExeFilePath = .GetSpecialFolder(0).Path & "\SysWOW64\cscript.exe"
            If .FileExists(ExeFilePath) Then
              com = ExeFilePath & " //Nologo """ & ScriptFilePath & """ """ & str & """"
              ret = CreateObject("WScript.Shell").Exec(com).StdOut.ReadAll
            End If
            .DeleteFile ScriptFilePath
          End If
        End With
        EncodeURL = ret
    #Else
        '32bitの場合のコード
        Dim script As Object
        Dim js As Object
        
        Set script = CreateObject("ScriptControl")
        script.language = "JavaScript"
        Set js = script.CodeObject
        EncodeURL = js.encodeURIComponent(str)
    #End If
    
End Function
  • Authgoogleを実行することで、認証が実行されます。
  • 続けて、GetAutherizationCodeが実行されて、IEが立ち上がりログインを要求してきます。
  • sleepを入れている理由は、さっと閉じてしまうと認証結果のコードを取りそこねるのを防ぐためです。
  • checkAccessTokenは、Access Tokenがexpireしていないかチェックを行い、expireしている場合には、refresh Tokenを使って自動でAccess Tokenを取り直しています。
  • 取得したAccess TokenおよびRefresh Tokenはsettingテーブルに格納しています。
  • 2019年1月1日より、ブラウザ上で表示されるAuthrization Codeの取得の為の仕様が変更になっているため、旧来のgetElementById(“code”)でelement指定しても取れません。auth_code = .Document.getElementsByClassName(“qBHUIf”)(0).valueといった形でClassName指定で取得するようにしています。

ファイルのダウンロードの為のコード

Option Compare Database
'ファイルダウンロード関数(カレントディレクトリにダウンロード)
Public Function getFileDownload()
    'DB接続用
    Dim SQL As String
    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    Set db = CurrentDb()
    Set rs = db.OpenRecordset("setting", dbOpenDynaset)
    
    'バイナリファイル生成用
    Dim FileData() As Byte
    Dim FileNum As Long
    
    'ファイルのIDを指定し、URLを組み立てる
    Dim fileid As Variant
    fileid = "ここにファイルのIDを入れてください"
    Dim downurl As String
    downurl = "https://drive.google.com/uc?export=download&id=" & fileid

    'Access_Tokenのexpireチェック
    Dim ret As Variant
    Dim access_token As String
    ret = checkAccessToken()
    
    If ret = False Then
      '取得済みAccess_Token類を呼び出す
      With rs
        .FindFirst "ID=1"
        access_token = !accesstoken
      End With
    Else
      'エラーメッセージを出して、再認証をする
      MsgBox "再認証が必要です。"
      Call Authgoogle
      Exit Function
    End If

    With CreateObject("WinHttp.WinHttpRequest.5.1")
        .Open "GET", downurl, False
        .setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
        .setRequestHeader "Authorization", "Bearer " & access_token
        .send
        
        '返ってきた値をもとにデータを処理
        Select Case .status
            Case 200
                'レスポンスヘッダを取得
                Debug.Print .GetAllResponseHeaders()
                
                'バイナリデータを取得
                FileData = .responseBody
                
                'ファイル名を取得する
                tempheader = .GetResponseHeader("Content-Disposition")
                disposition = DEcodeURLMSHTML(Mid(tempheader, InStr(tempheader, "filename*=UTF-8''") + 17))
                
                'ファイルパス組み立て
                FilePath = CurrentProject.Path & "\" & disposition
                
                'バイナリデータを生成する
                FileNum = FreeFile()
                Open FilePath For Binary Access Write As #FileNum
                    Put #FileNum, 1, FileData
                Close #FileNum
            Case Else
                MsgBox "ダウンロード失敗したよ"
                
                '終了処理
                Set db = Nothing
                Set rs = Nothing
                
                Exit Function
        End Select
    End With
    
    '終了処理
    Set db = Nothing
    Set rs = Nothing
    
    MsgBox "カレントディレクトリにファイルダウンロードしたよ。"
End Function

'MSHTMLを使ったURLデコード関数
Function DEcodeURLMSHTML(ByVal sWord As String) As String
    Dim d As Object
    Dim elm As MSHTML.HTMLSpanElement
    Dim objD As HTMLDocument: Set objD = New MSHTML.HTMLDocument
    sWord = Replace(sWord, "\", "\\")
    sWord = Replace(sWord, "'", "\'")
    Set d = CreateObject("htmlfile")
    Set elm = objD.createElement("span")
    elm.setAttribute "id", "result"
    objD.appendChild elm
    objD.parentWindow.execScript "document.getElementById('result').innerText = decodeURIComponent('" & sWord & "');", "JScript"
    DEcodeURLMSHTML = elm.innerText
End Function
  • WinHttpRequestでリクエストするURLはDriveのファイルダウンロードURL直にアクセスさせます。
  • Box APIを叩くでも利用したコードにて、バイナリファイルを組み立てて、カレントディレクトリに保存しています。
  • DecodeURLMSHTMLという関数を利用しています。利用する為には、参照設定にて「Microsoft HTML Object Library」が必要です。
  • ファイル名などの情報は、Content-Dispositionヘッダー内に含まれていて、ここから取り出します。ヘッダの内容は以下のような感じ

実行と結果

今回のファイルはAccessで作成していますが、主要なコマンドはリボンに組み込んであります。あらかじめ、Google Cloud Platformで控えておいた「Client ID」および「Client Secret」またGoogle DriveのファイルのIDを記述しておき、以下の手順で実行します。

  1. Google認証を実行し、Googleへログインする
  2. settingテーブルにAccess TokenとRefresh Tokenが格納されます。
  3. 続けて、ドライブファイルダウンロードを実行すると、このファイルと同じフォルダ内に対象のファイルがダウンロードされます。

図:これでオンライン配信がより確実に行える!!

Accessファイルのオンラインアップデート配信では、XMLファイルおよびアップデート用のインストーラやZIPファイルをGoogle Driveに格納し、外部ユーザに限定配信(組織のユーザのみ)が可能なので、VPNといった仕組みが不要です。対象のアカウントが削除されれば、Access Tokenは無効化されます。

※つまり、バージョン確認用のXMLのアクセスに1回、ファイルのダウンロードに1回の合計2回はファイルのダウンロードを実行する必要があります。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)