Kintoneの基本機能だけで通知メールをちょっぴりカスタマイズする方法
Kintoneはなかなか便利なサービスです。
Kintoneはプログラミングの知識がなくても業務アプリを作れる便利なサービスです。
交通費申請や経費申請などの業務フローも[プロセス管理]の設定で細かく設定できます。
参考 : プロセス管理でできること - kintone ヘルプ
[プロセス管理]のなかで[ステータス]が更新されるとメール通知が送信されます。
ただ、メール通知は基本的に件名と本文が決まっています。
Q メール通知の本文を変更したいです。レコードのすべての情報を表示したいです。
A メール通知の本文を変更する機能は未搭載です。(省略)
よくあるご質問 | メール通知の本文を変更したいです。レコードのすべての情報を表示したいです。 - aq.cybozu.info
メール通知の件名は、メール通知の件名 - kintone ヘルプで確認できます。
メール通知に埋め込まれる[レコードのタイトル]だけは設定できます。
メール通知の件名と本文には、[レコードのタイトル]というものが埋め込まれます。 この[レコードのタイトル]に設定でるものにはいくつかあります。
レコードタイトルには、次の種類のフィールドを指定できます。
- レコード番号
- 文字列(1行)
- 文字列(複数行)
- リッチエディター
- 数値
- 計算
- ルックアップ
[レコードのタイトル]に設定できる フィールドは1つ なのですが、とても便利なのでこの[レコードのタイトル]を利用してメール通知をちょっぴりカスタマイズしたいと思います。
通知メールに複数のフィールド情報を設定します。
Kintoneテンプレートにある[交通費申請]アプリをちょっぴりいじってやってみました。
「タイトル」が設定されている場合の通知メール
テンプレートにある[交通費申請]アプリでは、[レコードのタイトル]に「タイトル」が設定されていました。
設定方法
今回は、メールに「社員番号」と「申請者」両方の情報があったらいいのに・・・という要望があったという想定です。
1. [レコードのタイトル]に設定するフィールドを作成します。
メール通知の件名に埋め込まれる文字になるので[文字列(1行)]フィールドでシンプルに作成します。 あまり長い文字列だとメールの件名が見にくくなっちゃいますからね。
ポイントは追加するフィールドで「メール通知に入れたい情報を文字列連携させる」ということです(というよりこれが全てです)。 文字列の連結方法は、[&演算子]文字や数値の結合 - kintone ヘルプに説明があるので参考にします。
ちなみに「社員番号」と「申請者」の設定は以下のようにしていました。
フィールドの種類 | フィールド名 | フィールドコード |
---|---|---|
数値 | 社員番号 | fc_社員番号 |
作成者 | 申請者 | fc_申請者 |
2. レコードのタイトルに作ったフィールドを設定します。
参考 : レコードタイトルを設定する - kintone ヘルプ
3. 作ったフィールドを見えないように権限設定します。
作ったフィールドは、メール通知で使いたいだけなので入力する時に見る必要はないので、見えないように設定します。
参考 : フィールドにアクセス権を設定する - kintone ヘルプ
4. アプリを更新します。
アプリを更新する前に設定した一部の内容は[アプリの動作テスト]から動かして確認することができます。 しかし、メール通知は送信されないので注意してください。
動作テスト環境でできないこと
テスト環境では、次の設定や操作はできません。(省略)
・通知は送信されません。
このやり方は完璧ではありません。
今回ご紹介した方法では、メールの件名と本文を完全に任意の内容に変えられるわけではありません。 また、フォームに設定されたフィールドを使用するので通知を行う各アプリでそれぞれ設定しなければなりません。
そして、Kintoneのメールをカスタマイズする方法はいくつかあります。
- 有料のプラグインやサービスを使う
- 自分で他のサービスと連携させる
しかし、メール通知を受信する人がメール通知を受信しないように設定できます。
どんなにお金と時間と労力をかけても「メール通知を受信しないように設定」してたら・・・。
たとえ、外部サービスなどを利用してメール通知の設定に関わらずメールを送信できたとしても「受信しないように設定したのにメール送らないでよ!」と思われたら悲しいですよね。
そんな時は、今回ご紹介した完璧ではないけど簡単にできるやり方もカスタマイズの候補として考えてみてもいいかもしれません。
web.xmlメモ用紙
内容を覚えられない・・・本に書いてある通りに書きたくてもクラス名が長くて書けない・・・ググるのが面倒くさい・・・だからメモしちゃう。 書き溜めておけばいつか役に立つって期待してる。
web-app
<?xml version="1.0" encoding="UTF-8"?> <web-app {ここの書き方のメモ}> </web-app>
Servlet 3.0
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
Servlet 4.0
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
servlet
<web-app ...> <servlet> {ここの書き方のメモ} </servlet> <servlet-mapping> {ここの書き方のメモ} </servlet-mapping> </web-app>
Spring MVCのフロントコントローラを利用するための設定
<!-- DispatcherServletクラスをサービレットコンテナに登録する --> <servlet> <servlet-name>api</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <!-- サーブレットのcontextClassパラメータにAnnotationConfigWebApplicationContextクラスを指定する --> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </init-param> <init-param> <!-- サーブレットのcontextConfigLocationパラメータに作成した設定クラスを指定する --> <param-name>contextConfigLocation</param-name> <!-- AppConfigの内容は下記参照 --> <param-value>example.config.AppConfig</param-value> </init-param> </servlet> <!-- 定義したDispatcherServletを使用してリクエストをハンドリングするURLのパターンを指定する --> <servlet-mapping> <servlet-name>api</servlet-name> <!-- 全てのリクエストを定義したDispatcherServletを使用してハンドリングする --> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
package example.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc @ComponentScan("example") public class AppConfig implements WebMvcConfigurer { }
filter
<?xml version="1.0" encoding="UTF-8"?> <web-app ...> <filter> {ここの書き方のメモ} </filter> <filter-mapping> {ここの書き方のメモ} </filter-mapping> </web-app>
入力値の日本語が文字化けしないようにするための設定
<!-- CharacterEncodingFilterクラスをサーブレットコンテナに登録する(入力値の日本語の文字化け対策用) --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <!-- サーブレットのencodingパラメータにリクエストパラメータの文字エンコーディング(UTF-8)を指定する --> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <!-- サーブレットのforceEncodingパラメータにリクエストおよびレスポンスのエンコーディング上書きするかを指定する --> <param-name>forceEncoding</param-name> <!-- true:encodingへ強制的に上書きされる --> <param-value>true</param-value> </init-param> </filter> <!-- CharacterEncodingFilterを適用するリクエストのURLパターンを指定する --> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <!-- 全てのリクエストを適用対象にする --> <url-pattern>/*</url-pattern> </filter-mapping>
ISO 8859-1(ISO Latin 1)以外の文字を扱う必要がある場合は、CharacterEncodingFilterを使用して適切な文字エンコーディングの指定が必要です。 また、サーブレットフィルタを複数登録する場合は、リクエストパラメータから値を取得するサーブレットフィルタより前にフィルタ処理が適用されるように登録してください。 順番が逆転すると文字化けしてしまいます。
第4章Spring MVC - Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発
HiddenHttpMethodFilter
HiddenHttpMethodFilterを使用すると、クライアントとの物理的な通信はPOSTメソッドを使用しますが、サーブレットコンテナ内ではリクエストパラメータで送られてきた値に置き換えて処理を行うことができます。
第6章RESTful Webサービスの開発 - Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発
<!-- HiddenHttpMethodFilterクラスをサーブレットコンテナに登録する --> <filter> <filter-name>HiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter> <!-- HiddenHttpMethodFilterを適用するとサーブレットコンテナ内でリクエストパラメータで送られてきた値に置き換えて処理を行うことができる --> <filter-mapping> <filter-name>HiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
GitHubとSlackをSlackのアプリで連携させる方法
Slackのアプリを使ってGitHubを連携します。
1. GitHubにSlackアプリをインストールします。
参考 : 個人アカウントでアプリケーションをインストールする - GitHub Docs
- 連携するGitHubアカウントにログインします。
- [Marketplace] > 「slack」でアプリを検索 > 「Slack + GitHub」を選択してページを表示します。
- [Install it for free]ボタンで次のページを表示します。
- [Complete order and begin installation]ボタンでインストールします。
2. SlackにGitHubアプリをインストールします。
- Slack + GitHub のページを開いて、[Add to Slack]ボタンを押下します。
- 設定したいSlackのワークスペースになっていることを確認して[Allow]ボタンで進みます。
- Slackのメッセージが届いてワークスペースにGitHubアプリが追加されます。
3. SlackでGitHubアカウントへの連携設定をします。
- 使いたいチャネルで
/invite @GitHub
を送信してGitHubアプリをチャネルに追加します。 /github subscribe ユーザ名/リポジトリ名
を送信して、表示された[Connect GitHub account]ボタンでWeb画面を表示します。- [Authorize Slack]ボタンを押下すると認証コードが画面に表示されます。
- Slackに表示された[Enter code]ボタンで認証コードを入力します。
GitHubアカウントへの連携を解除する方法
参考 : GitHubとSlackを連携させて通知を自動化するまでの手順 - zenn.dev
# 現在、何が連携されているかを確認します ponsuke 10:31 /github subscribe list features GitHubAPP 10:31 Subscribed to the following repository ユーザ名/リポジトリ名 issues, pulls, commits, releases, deployments Learn More # unsubscribeで連携を解除します ponsuke 10:34 /github unsubscribe ユーザ名/リポジトリ名 GitHubAPP 10:34 Unsubscribed from ユーザ名/リポジトリ名
うまくいかなかったこと
Either the app isn't installed on your repository or the repository does not exist. Install it to proceed.
- 原因 : GitHub 側で Slack アプリがインストールされていないから
- 対応
Workspaces on free subscriptions can only install 10 apps and your workspace has reached the limit.
2020年3月頃にGitHubとSlackを連携させました。 ある日、GitHubと連携するアプリを使っているチャネルにこんな通知が来ました。
Action required - upgrade app for {ワークスペース名}. GitHub app is built on Slack's workspace apps which are now deprecated. The legacy GitHub app will stop working on July 15, 2021. Don't worry, we have built a new version of GitHub-Slack integration. You can just upgrade the app and get back to your work. Learn more about this upgrade here. (なんとなくの訳) GitHubアプリは非推奨になって2021-07-15に使えなくなるよ。 でも新しいアプリがあるからアップグレードすればOKよ。
というわけで対応しようと、通知にある[Upgrade App]ボタンから進んでいくと・・・。
アプリ
インストールできるサードパーティ製アプリやカスタムアプリは最大 10 個です。
有料プランから無料プランへプラン変更したワークスペースのためか、インストールされている「アプリ」と「インテグレーション」の合計数が10個をがっつり超えていました。
SlackのGitHubアプリをアップグレードしよう(2021年7月15日まで) | DevelopersIOを参考にすると通知にある[Upgrade App]ボタンからぽちぽちすればアップグレードできるようですが、今回はワークスペースにインストールするアプリの上限に達しているため一旦古いGitHubアプリを削除して再度新しいGitHubアプリで設定することにしました。
- Slackをブラウザで表示 > [Manage]
- 対象のワークスペースが右上に表示されていることを確認する
- [GitHub (Legacy)]をアンインストールする
- 他の不要なアプリやをインテグレーションをアンインストール
- 削除方法はアプリの種類などによって少し異なるので以下を参考にアンインストールする
- アプリは以下2つの画面に表示されるものの合計10個までをインストールできる(いまいち分かってなくてSlackにお問い合わせしてしまいました。)
- カスタムインテグレーション: https://my.slack.com/apps/manage/custom-integrations
- アプリ : https://my.slack.com/apps/manage ([Installed apps]タブにあるアプリ)
PostgreSQLのpsqlをインストールする
PostgreSQLの本体じゃなくてコマンドだけが欲しい時のお話です。
以前、MacにPostgreSQLをインストールして使ったことがありました。 ponsuke-tarou.hatenablog.com 今回は、自分のパソコンじゃなくてサーバにあるPostgreSQLにコマンドで接続したいです。 なので、PostgreSQL本体が欲しいのではなくコマンドだけが欲しいのです。
Windows
- 環境
- Windows 10 Pro バージョン1909
- 接続先 : PostgreSQL 9.3.10
インストールする
- Download PostgreSQL Database for Windows, Linux and MacOS & 32-bit or 64-bit Versions | EDBをブラウザで表示する
- 任意のバージョンのWindowsのところにある[Download]ボタンからexeファイルをダウンロードする
- ダウンロードしたexeファイルを起動する
パスを通す
パスは、使っているシェルや環境によって設定がちょっぴり違います。
今回は、コマンドプロンプトでpsql
コマンドを使うので、システムのプロパティから環境変数PATHに設定します。
Win + R
>sysdm.cpl
を入力 > Enterで[システムのプロパティ]ダイアログを開く- [詳細設定]タブ > [環境変数...] > [ユーザー環境変数]か[システム環境変数]の[Path]を選択 > [編集]ボタンでダイアログを開く
- [新規]ボタン >
{インストール先フォルダ}\bin
を入力 > [OK]ボタンでダイアログを閉じる
# 環境変数PATHに設定されました >echo %path% ...省略...C:\apps\PostgreSQL\10\bin;...省略... # psqlコマンドが使えるようになりました >psql -V psql (PostgreSQL) 10.16 # データベースへはこんな感じで接続できます(切断はCtrl+C) # psql -h {ホスト} -p {ポート} -U {ユーザー} -d {データベース} >psql -h example.com -p 5432 -U ponsuke -d my_db psql (10.16, server 9.3.10) Type "help" for help. my_db=#
psqlのオプション | 意味 |
---|---|
-h {ホスト} --host={ホスト} |
サーバを実行しているマシンのホスト名を指定 |
-p {ポート} --port={ポート} |
サーバが接続監視を行っているTCPポートもしくはローカルUnixドメインソケットファイルの拡張子 環境変数PGPORTの値、環境変数が設定されていない場合はコンパイル時に指定した値(通常は5432)がデフォルト値 |
-U {ユーザー} --username={ユーザー} |
接続するユーザーを指定 |
-d {データベース} --dbname={データベース} |
接続するデータベースの名前 |
-c {コマンド} --command={コマンド} |
実行するコマンド -cまたは-fが指定されると、psqlは標準入力からコマンドを読み取りません。 その代わりに、すべての-cオプションおよび-fオプションを順に処理した後、終了します。 |
-f {ファイル} --file={ファイル} |
コマンドを読み取るファイルを指定 |
上記表の出典 : psql - PostgreSQL 12.4文書
Windows以外
いつか・・・やった時に書きます。
AESは電子政府推奨暗号リストに載ってる共通鍵暗号方式
- 前回の勉強内容
- AESは、暗号化の規格です。
- 電子政府推奨暗号リストは、安全性及び実装性能が確認された暗号方式を載せたリストです。
- AESは、鍵長が長いほど暗号文は解読されにくくなります。
- 最後にAESで暗号化と復号をしてみます。
- 次回の勉強内容
前回の勉強内容
AESは、暗号化の規格です。
- 正式名称 : Advanced(高度な) Encryption(暗号化) Standard(標準)
昔アメリカで標準規格として採用されていたDESにとって変わって登場した暗号化方式です。
暗号化と復号で同じ鍵を使う共通鍵暗号方式です。
暗号方式に関する記述のうち,適切なものはどれか。
使う鍵の組み合わせはこんな感じです。
使う鍵 | 共通鍵暗号方式 | 公開鍵暗号方式 |
---|---|---|
暗号化 | 共通鍵 | 公開鍵 |
復号 | 共通鍵 | 秘密鍵 |
暗号方式のうち,共通鍵暗号方式はどれか。
データベースで管理されるデータの暗号化に用いることができ,かつ,暗号化と復号とで同じ鍵を使用する暗号化方式はどれか。
上記過去問の選択肢を分類するとこんな感じです。
問題の選択肢 | 共通鍵 暗号方式 |
公開鍵 暗号方式 |
暗号方式じゃない |
---|---|---|---|
AES | o | ||
ElGamal暗号 | o | ||
RSA | o | ||
楕円曲線暗号 | o | ||
PKI | PKIは、暗号化技術と電子署名で世の中の安全を守る仕組みです。 | ||
SHA-256 | ハッシュ関数です。 |
データを決まった長さに区切って暗号化するブロック暗号です。
ブロック暗号は、元のデータを決まった長さに区切って、その区切った単位ごとに暗号化します。
この区切ったデータをブロックと言います。
AESでは、このブロックの長さ(ブロック長)を128bitで分けています。
もう一つストリーム暗号という方式があります。
ストリーム暗号は、元のデータを端から1bitまたは1byte単位で次々に暗号化します。
端からバンバン暗号化するのでデータを全部受信していなくてもさくさく暗号化できちゃいます。
だから、早い!
が、ブロック暗号の方が研究が進んでいて安全だそうです。
研究が進んでいるおかげかAESは、早いそうです。
ブロック暗号 | ストリーム暗号 | |
---|---|---|
暗号化の単位 | ブロック | 1bitまたは1byte |
逐次生 | o | |
安全性 | o | |
暗号方式 | DES, AES | RC4 |
電子政府推奨暗号リストは、安全性及び実装性能が確認された暗号方式を載せたリストです。
暗号化技術の安全性を評価したりしている、CRYPTRECというプロジェクトがあります。
- 正式名称 : Cryptography(暗号技術) Research(調査) and Evaluation(評価) Committees(委員会)
CRYPTREC とはCryptography Research and Evaluation Committees の略であり、電子政府推奨暗号の安全性を評価・監視し、暗号技術の適切な実装法・運用法を調査・検討するプロジェクトである。
CRYPTREC | CRYPTRECとは
このCRYPTRECから「電子政府推奨暗号リスト」というものが出されています。
電子政府推奨暗号リストとは,CRYPTRECによって安全性及び実装性能が確認された暗号技術のうち,市場における利用実績が十分であるか今後の普及が見込まれると判断され,当該技術の利用を推奨するもののリストである。
平成27年春期問8 CRYPTREC 暗号リスト|情報処理安全確保支援士.com
AESは、安全性及び実装性能が確認された暗号方式です。
CRYPTREC | CRYPTREC暗号リスト(電子政府推奨暗号リスト)にあるPDFで「電子政府推奨暗号リスト」を確認すると(2021-04-13時点)
「共通鍵暗号」の「128ビットブロック暗号」のところにAESが載っています。
ということで、「CRYPTRECにより安全性及び実装性能が確認された暗号方式」ということですね。
AESは、鍵長が長いほど暗号文は解読されにくくなります。
鍵長は、そのままの意味で鍵の長さ(単位はbit)です。
AESでは、暗号化で使う共通鍵の鍵長が「128bit」「192bit」「256bit」から選べます。
悪い人が共通鍵を総当たりで割り出そうとした時に「0」「1」の組み合わせといえど長くなれば長くなるほど大変になるわけですね。
AES-256で暗号化されていることが分かっている暗号文が与えられているとき,ブルートフォース攻撃で鍵と解読した平文を得るまでに必要な試行回数の最大値はどれか。
- 256
ブルートフォース攻撃は、根性で不正ログインを頑張る総当たり攻撃です。
ブルートフォース攻撃をする悪い人は、復号する共通鍵をどんどん変えて復号を頑張ります。
鍵長が「256bit」の「0」「1」の組み合わせパターンを頑張って鍵を見つけるために最大回鍵を変えて復号を試し続けることになるのです。
頑張りますね。
AESの特徴は、「鍵長によって段数が決まる」です。
AESの特徴はどれか。
- 鍵長によって,段数が決まる。
- 段数は,6回以内の範囲で選択できる。
- データの暗号化,復号,暗号化の順に3回繰り返す。
- 同一の公開鍵を用いて暗号化を3回繰り返す。
暗号化では、ラウンド関数という関数を繰り返し処理します。
その関数を処理する回数を段数とかラウンド数といい、この数が多いほど強度の高い暗号文になります。
鍵長と段数の組み合わせ
鍵長 | 段数 |
---|---|
128bit | 10 |
192bit | 12 |
256bit | 14 |
というわけで、「使う共通鍵の鍵長が大きい」->「暗号化の段数が多い」->「解読されにくい暗号文ができる」ということになります。
最後にAESで暗号化と復号をしてみます。
参考 : コマンドラインで簡単にAES暗号化、または Java での AES 暗号化 - 理系学生日記
# Macに入っているopensslコマンドを使います。 $ openssl version LibreSSL 2.8.3 # 使えるAESの暗号方式をみてみます。 $ openssl list-cipher-commands | grep aes aes-128-cbc aes-128-ecb aes-192-cbc aes-192-ecb aes-256-cbc aes-256-ecb # 鍵長256bitの共通鍵を使ったAESで暗号化します。 $ echo "ponsuke" | openssl aes-256-cbc -e -base64 -p -pass pass:password salt=1CD83F097F07009C key=8E71364EB09F97B6BACAC68B1EB944FC416CE8EBBF81E8B2681667AEE7991D63 # << 共通鍵 iv =68F150FA9F95D8590F8FE50002A1571E U2FsdGVkX18c2D8JfwcAnEg69cNx1g30btqEdInz7TQ= # << 暗号化された暗号文 # 暗号文を復号します。 $ echo "U2FsdGVkX18c2D8JfwcAnEg69cNx1g30btqEdInz7TQ=" | openssl aes-256-cbc -d -base64 -pass pass:password ponsuke
次回の勉強内容
いろんなMacがある
すぐ忘れちゃう。 同じようなアルファベットの並びがあるとなんだかわからなくなる。 フルスペルなんて1mmも覚えられない。そんな自分への記録です。
MacintoshのMac
Appleが作ってるりんごマークのついたパソコンです。
Media Access ControlのMACアドレス
ネットワーク上でパソコンとかスマホとか機械を識別するための番号です。 機械の個人番号のようなもので、基本的には世界で1つの番号です。
# コマンドだとこんな感じで確認できます。 $ ifconfig en0 ether en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 options=400<CHANNEL_IO> ether 12:34:56:ab:cd:ef #<<< ここがMACアドレス
このMACアドレスがあるので広い広いネットワークの中でもどの器械(パソコンとか)にどの器械から通信しているかわかるようになります。
Message Authentication CodeのMAC
メッセージ認証をする時にデータと一緒に送られてくる検証用のデータです。
メッセージ(message)を認証(authentication)するコード(code)なので、日本語で「メッセージ認証コード」、英語で「Message Authentication Code」、略して「MAC」です。 悪い人が送ったデータじゃないことを確かめるために使うデータです。
例えば、GitHubのWebhookを使うと送られてくる情報にこんな感じでくっついてきます。
{ ...省略... "headers": { ...省略... "x-hub-signature": "sha1=sha1のHMAC値", "x-hub-signature-256": "sha256=sha256のHMAC値" }, ...省略... "body": "データの本体", ...省略... }
LambdaのPythonでGitHubのWebhookから送られてくるHAMC値をチェックする
LambdaのPythonでGitHubのWebhookから送られてくるHAMC値をチェックするコード
# -*- coding: utf-8 -*- from __future__ import print_function import json, hmac, hashlib, base64, urllib def checkHmac(event) -> bool: """GitHubのWebhookから送られてくるHAMC値をチェックする""" # 送信者と受信者で秘密情報であるSecretの値 secret = 'GitHubのWebhookで設定したSecretの値(直書きしないで環境変数とかSecrets Managerから取得すべし)' # GitHubから送られてきた情報にあったHMAC値 sent_hmac = event['headers']['x-hub-signature-256'] print('SHA-256ハッシュ関数用のHMAC値:' + sent_hmac) # Secretの値をbytes型に変換する secret_bytes = bytes(secret, 'utf-8') if (event['isBase64Encoded']): # bodyをbase64でデコードしてbytes型にする body_bytes = base64.b64decode(event['body']) else: # bodyをbytes型に変換する body_bytes = bytes(event['body'], 'utf-8') # 「Secret + メッセージ + ハッシュ関数」でHMAC値を作る created_hmac = 'sha256=' + hmac.new(secret_bytes, body_bytes, hashlib.sha256).hexdigest() print('自分で作ったHMAC値:' + created_hmac) # 2つのHMAC値が同じであったら改竄されていないと判断する return hmac.compare_digest(created_hmac, sent_hmac) def lambda_handler(event, context): if checkHmac(event): print('HMAC値で認証できた!') # base64デコードした上で文字列にする b64decd = base64.b64decode(event['body']).decode() # URLデコードして「payload=」を削除する urldecd = urllib.parse.unquote(b64decd).lstrip('payload=') # 辞書型にする body = json.loads(urldecd) # そして処理で使う print(body) else: print('へんなHMAC値がGitHubから送られてきた!')
ことの発端は試験勉強
- じみに続けている情報処理試験の勉強でメッセージ認証を勉強していた。
- そういえば以前Backlogの課題にGitHubのコミットを連携するをやった時にGitHubのWebhookでHMAC値を使ったのを思い出して、今一度やってみようと思った。
- 手頃にGitHubでWebhookを設定
- AWSでAPI GatewayとLambdaをノリで作ってみた。
- WebhookからきたHMAC値と自作したHMAC値が一致しない!
GitHubのWebhookが送ってくるbodyがへんだ
print(event)
してCloudWatch Logsをみたら・・・bodyが・・・肉眼で読めない・・・。
{ "version": "2.0", ... "body": "cGF5bG9hZD0lN0IlMjJ...=", "isBase64Encoded": true }
アルファベットと数字と最後にある=
からBase64エンコードされてるっぽい・・・前にやった時は普通に読めたのに・・・・。
ペイロードがBase 64でエンコードされている
じっと見つめると "isBase64Encoded": true
と最後に書いてある・・・なにこれ?
Lambda プロキシ統合の場合、API Gateway は、次のようにクライアントリクエスト全体をバックエンド Lambda 関数の入力 event パラメータにマッピングします。
...省略...
"isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encoded"
訳すと「適用可能なリクエスト・ペイロードがBase 64でエンコードされているかどうかを示す真偽フラグ」。
HMAC値を作るにはbodyをBase64デコードします。
# 「cGF5bG9hZD0lN0IlMjJ...=」なbodyをBase64デコードしてbytes型にしてあげる body_bytes = base64.b64decode(event['body']) # bytes型にしたbodyを使ってHMAC値を作ろう created_hmac = 'sha256=' + hmac.new(secret_bytes, b64decd, hashlib.sha256).hexdigest()
WebhookからきたHMAC値は X-Hub-Signature
ではなくX-Hub-Signature-256
を使うのがお勧めです。
{ ...省略... "headers": { ...省略... "x-hub-signature": "sha1=sha1のHMAC値", "x-hub-signature-256": "sha256=sha256のHMAC値" }, ...省略... }
ヘッダ | 説明 |
---|---|
X-Hub-Signature | このヘッダは、webhook が secret で設定されている場合に送信されます。 これはリクエスト本文の HMAC hex digest であり、SHA-1 ハッシュ関数と HMAC key としての secret を使用して生成されます。 X-Hub-Signature は、既存の統合との互換性のために提供されているため、より安全な X-Hub-Signature-256 を代わりに使用することをお勧めします。 |
X-Hub-Signature-256 | このヘッダは、webhook が secret で設定されている場合に送信されます。 これはリクエスト本文の HMAC hex digest であり、SHA-256 ハッシュ関数と HMAC key としての secret を使用して生成されます。 |
表の出典 : webhook イベントとペイロード - GitHub Docs
isBase64EncodedがFalseの時も考えておきます。
isBase64Encoded
がFalse
だとBase64エンコードはされなさそうなので処理の分岐を作っておきます。
if (event['isBase64Encoded']): # bodyをbase64でデコードしてbytes型にする body_bytes = base64.b64decode(event['body']) else: # bodyをbytes型に変換する body_bytes = bytes(event['body'], 'utf-8')
処理でBodyの内容を使う時はさらに加工して使うといいでしょう。
# base64デコードした上で文字列にする b64decd = base64.b64decode(event['body']).decode() # こんな感じになる >>> payload=%7B%22ref%22%3A%22refs%2Fheads%2Fmaster%22%2C%22before... print(b64decd) # URLデコードして「payload=」を削除する urldecd = urllib.parse.unquote(b64decd).lstrip('payload=') # こんな感じになる >>> {'ref': 'refs/heads/master', 'before... print(urldecd) # 辞書型にすると使いやすい(と思う) body = json.loads(urldecd)
付加データを付き合わせてなりすましを見破るHMAC
前回の勉強内容
勉強のきっかけになった問題
令和2年度秋(2020-10)の情報処理安全確保支援士試験午後1の問題にHMACについての問題が出題されました。
そして、大敗を喫しました・・・ので勉強します。
なりすまし対策としてデータが改竄されていないかを検証します。
「勉強のきっかけになった問題」では、「会員番号をバーコードで表示するアプリのなりすまし対策」にメッセージ認証を用いることになっています。
「他人のバーコードを会員番号から推測して表示」「他人の会員番号を盗んでバーコードを生成」しちゃってなりすしできちゃうので対策をするわけです。
メッセージ認証は、検証用のデータをメッセージに添付する方法です。
ネットワークを伝わっている間にデータが改竄されても検出できるように以下の流れで検証を行います。
- メッセージをやりとりする送信者と受信者で秘密情報(共有鍵など)を共有する
- 送信者がメッセージ本文から秘密情報などをごにょごにょ使って検証用のデータを作る
- 送信者は、メッセージ本文と検証用のデータを受信者に送る
- 受信者がメッセージ本文から秘密情報などをごにょごにょ使って検証用のデータを作る
- 受信者は、送られてきた検証用のデータと自分で作った検証用のデータを比較する
- 2つの検証用のデータが同じであったら改竄されていないと判断する
検証用のデータのことをMACといいます。
- 英語 : Message Authentication Code
- 日本語 : メッセージ認証符号
送信者Aが,受信者Bと共有している鍵を用いて,メッセージからメッセージ認証符号を生成し,そのメッセージ認証符号とメッセージを受信者Bに送信する。このとき,メッセージとメッセージ認証符号を用いて,受信者Bができることはどれか。
答. メッセージの改ざんがないことを判定できる。
HMACは、ハッシュ関数を使って作られたMACのことです。
- 英語 : Hash-based Message Authentication Code
メッセージ本文 + 共有鍵 = MAC メッセージ本文 + 共有鍵 + ハッシュ関数 = HMAC
みたいな感じです。
MAC(Message Authentication Code)は、通信内容の改ざんの有無を検証し、完全性を保証するために通信データから生成される固定長のビット列です。 MACの生成には、共通鍵暗号を用いたもの(DES-MACやAES-MAC)とハッシュ関数を用いたもの(HMAC)がありますが、設問ではブロック暗号を用いてMACを生成しているので共通鍵暗号を用いた方式であることがわかります。
GitHubのWebhookでもHMACを使います。
以前、Backlogの課題にGitHubのコミットを連携する方法 - ponsuke_tarou’s blogをやった時にHMACを使いました。
- GitHubでWebhookを登録する時に合わせて登録するSecretが「メッセージをやりとりする送信者と受信者で秘密情報」になります。
- GitHubは、情報と一緒に検証用のデータを送ってくれます。
- BODY部分のメッセージ本文とSecret(秘密情報)とハッシュ関数でHMAC値を作ります。
- 2つのHMAC値が同じであったら改竄されていないと判断する(下記Pythonのコード参照)
{ ...省略... "headers": { ...省略... "x-hub-signature": "sha1=検証データ", "x-hub-signature-256": "sha256=検証データ" }, ...省略... "body": "BODYに送ってくれた情報がある" ...省略... }
# -*- coding: utf-8 -*- from __future__ import print_function import json, hmac, hashlib def lambda_handler(event, context): # 送信者と受信者で秘密情報であるSecretの値 secret = '3m@6vC5Y_mNwhhA' # GitHubから送られてきた情報にあったHMAC値 sent_hmac = event['headers']['x-hub-signature-256'] print('SHA-256ハッシュ関数用のHMAC値:' + sent_hmac) # Secretの値をbytes型に変換する secret_bytes = bytes(secret, 'utf-8') if (event['isBase64Encoded']): # bodyをbase64でデコードしてbytes型にする body_bytes = base64.b64decode(event['body']) else: # bodyをbytes型に変換する body_bytes = bytes(event['body'], 'utf-8') # 「Secret + メッセージ + ハッシュ関数」でHMAC値を作る created_hmac = 'sha256=' + hmac.new(secret_bytes, body_bytes, hashlib.sha256).hexdigest() print('自分で作ったHMAC値:' + created_hmac) # 2つのHMAC値が同じであったら改竄されていないと判断する if created_hmac == sent_hmac: print('HMAC値で認証できた!') else: print('認証できないHMAC値がGitHubから送られてきた!')
上記は以下の投稿で紹介しているコードを基にしていますので、ご興味のある方はご参照ください。 ponsuke-tarou.hatenablog.com
次回の勉強内容
はじめてのGoogle Analytics Reporting APIをNode.jsでちょっとだけ使ってみる
以前の投稿でGoogle AnalyticsとGoogle Cloud Platformを設定してGoogle Analytics Reporting APIを使えるようにしました。
今回は、自分のQiitaのページにアナリティクスを設定、Qiitaページ用のサービスアカウントを作ったので、その情報をNode.jsを使ってAPIで取得してみたいと思います。 ちなみに、Node.jsは未経験です、ずぶのど素人です。
準備する
環境 : Windows10 Pro バージョン1909
- Node.jsをインストールする
- 参考 : Node.jsをインストールする - Qiita
- 今回インストールしたバージョンは「v10.23.1」でした
- yarnをインストールする
- 参考 : yarnをインストールする - Qiita
- 今回インストールしたバージョンは「1.22.10」
- 作業用のディレクトリを作成する(ここ以降は下記のコードを参照)
- yarnの初期化する
- GoogleAPIのNode.js用ライブラリgoogle-api-nodejs-clientをインストールする
- Google Cloud Platformで作成したサービスアカウントの鍵ファイル(json)を格納するためのディレクトリを作成する
- 鍵ファイル(json)を格納する
# 作業用のディレクトリを作成する $ mkdir google-analytics-api $ cd google-analytics-api/ # yarnの初期化する $ yarn init ...省略... Done in 93.28s. # GoogleAPIのNode.js用ライブラリgoogle-api-nodejs-clientをインストールする $ yarn add googleapis yarn add v1.22.10 info No lockfile found. [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages... success Saved lockfile. success Saved 31 new dependencies. info Direct dependencies └─ googleapis@68.0.0 info All dependencies ├─ abort-controller@3.0.0 ...省略... Done in 7.96s. # Google Cloud Platformで作成したサービスアカウントの鍵ファイル(json)を格納するためのディレクトリを作成する $ mkdir config # 鍵ファイル(json)を格納する $ ls config/ client_secrets.json
コードを書く
Google APIでの認証については、Analytics Reporting API - 承認 | アナリティクス Reporting API v4が参考になります。
analytics.js
というファイルを作って以下のコードを書きました。
リクエストの内容についてはメソッド: reports.batchGet | アナリティクス Reporting API v4 | Google Developersを見ながらしてする値を決めました。
const { google } = require('googleapis') const KEY_FILE = './config/client_secrets.json' // 鍵ファイルのパスを指定する const VIEW_ID = 'GoogleアナリティクスのViewID' /** * Auth2.0認証をしてクライアントを取得する * @returns クライアント */ async function getAnalyticsreportingClient() { const client = await google.auth.getClient({ keyFile: KEY_FILE, scopes: 'https://www.googleapis.com/auth/analytics.readonly' }) const analyticsreporting = google.analyticsreporting({ version: 'v4', auth: client }) return analyticsreporting } /** Google Analyticsから情報を取得する */ async function getReporting() { const client = await getAnalyticsreportingClient() // 取得期間をyyyy-MM-dd形式で指定 let dateRanges = [{startDate: '2021-03-19', endDate: '2021-03-30'}] const res = await client.reports.batchGet({ requestBody: { reportRequests: [ // 指定期間でのPV数TOP3のページを取得するリクエスト { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:pagePath'}], // ページ単位の情報を取得する metrics: [{expression: 'ga:pageviews'}, {expression: 'ga:avgTimeOnPage'}], // PV数と平均ページ滞在時間を取得する orderBys: {fieldName: 'ga:pageviews', sortOrder: 'DESCENDING'}, // PV数の降順でソートする pageSize: 3 // 3件分取得する }, // 直近1週間での平均ページ滞在時間TOP3のページを取得するリクエスト { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:sourceMedium'}], // ページ単位の情報を取得する metrics: [{expression: 'ga:bounceRate'}, {expression: 'ga:avgTimeOnPage'}], // 直帰率と平均ページ滞在時間を取得する orderBys: {fieldName: 'ga:avgTimeOnPage', sortOrder: 'DESCENDING'}, // 平均ページ滞在時間の降順でソートする pageSize: 3 // 3件分取得する } ] } }) // コンソールに取得内容を表示 console.log(JSON.stringify(res.data)) } getReporting()
Reporting APIを呼び出す
早速、ターミナルから実行してみます。本当はレスポンスが1行なのですが、見にくいので改行などを入れています。
$ node analytics.js {"reports":[ { "columnHeader":{"dimensions":["ga:pagePath"],"metricHeader":{"metricHeaderEntries":[{"name":"ga:pageviews","type":"INTEGER"},{"name":"ga:avgTimeOnPage","type":"TIME"}]}}, "data":{"rows":[ {"dimensions":["/ponsuke0531/items/4629626a3e84bcd9398f"],"metrics":[{"values":["1631","387.46666666666664"]}]}, {"dimensions":["/ponsuke0531/items/df51a784b5ff48c97ac7"],"metrics":[{"values":["1160","601.5056179775281"]}]}, {"dimensions":["/ponsuke0531/items/edf2eee638202aa7f61f"],"metrics":[{"values":["999","401.8253968253968"]}]} ], "totals":[{"values":["35429","382.3464974141984"]}], "rowCount":499, "minimums":[{"values":["1","0.0"]}], "maximums":[{"values":["1631","1679.0"]}]}, "nextPageToken":"3" }, { "columnHeader":{"dimensions":["ga:sourceMedium"],"metricHeader":{"metricHeaderEntries":[{"name":"ga:bounceRate","type":"PERCENT"},{"name":"ga:avgTimeOnPage","type":"TIME"}]}}, "data":{"rows":[ {"dimensions":["27.94.140.223:8080 / referral"],"metrics":[{"values":["0.0","1335.0"]}]}, {"dimensions":["nekorokkekun.hatenablog.com / referral"],"metrics":[{"values":["0.0","996.0"]}]}, {"dimensions":["zenn.dev / referral"],"metrics":[{"values":["0.0","909.0"]}]} ], "totals":[{"values":["86.22115384615384","382.3464974141984"]}], "rowCount":69, "minimums":[{"values":["0.0","0.0"]}], "maximums":[{"values":["100.0","1335.0"]}]}, "nextPageToken":"3" } ]}
はじめてのGoogle Analytics Reporting APIをPythonとCloud9でちょっとだけ使ってみる
以前の投稿でGoogle AnalyticsとGoogle Cloud Platformを設定してGoogle Analytics Reporting APIを使えるようにしました。
そのAPIを簡単に呼び出してみたいと思います。
今回は、どんなものが取得できるかをみてみたいだけなので簡易にReporting APIのドキュメントにあるクイックスタートのコードとAWSのCloud9を使います。
Pythonをそれほど知りません、が、大丈夫。なぜなら「実装」と呼べるほどのことはしません。ちょっと設定するだけです。
準備する
- はじめてのアナリティクス Reporting API v4: サービス アカウント向け Python クイックスタートからPythonのコード(HelloAnalytics.py)をダウンロードする
- Google AnalyticsでView IDを確認する
- Google Analyticsへログインする
- 管理 > 対象のアカウント選択 > 対象のプロパティ選択 > [ビューの設定]で画面を開く
- [基本設定] > [ビュー ID]に表示されたView IDをメモする
- AWSでCloud9の環境を作成する
Cloud9でコードを設定する
- Cloud9の環境を起動して表示する
- [File] > [Upload Local Files...]で以下のファイルをアップロードする
- サービスアカウントの鍵ファイル(json)
- ダウンロードしたクイックスタートのコードファイル(HelloAnalytics.py)
- HelloAnalytics.pyを開く
- 上のほうにある以下の値を書き換える
- <REPLACE_WITH_JSON_FILE> : サービスアカウントの鍵ファイル名(HelloAnalytics.pyと違う場所に配置した場合はそのパス)
- <REPLACE_WITH_VIEW_ID> : Google AnalyticsのView ID
ライブラリをインストールする
GoogleのAPIを呼び出すために必要なライブラリをインストールします。
Cloud9を使っている場合は必ず--user
オプションを使用して自分の使うPythonに対してインストールします。
忘れるとModuleNotFoundError: No module named 'apiclient'となることがあるので注意してください。
# 認証に使うライブラリをインストールする $ python -m pip install --user --upgrade oauth2client # Google APIを使うためのライブラリをインストールする $ python -m pip install --user --upgrade google-api-python-client
Reporting APIを呼び出す
あとはコードを実行するだけです。
HelloAnalytics.pyを表示した状態で画面上部の[Run]ボタンを押下すればGoogle AnalyticsからAPIで取得した情報が表示されます。
これを基にメソッド: reports.batchGetを見ながらコードのパラメータを変えてどんな情報が取得できるかを試していきたいと思います。
Google Analytics Reporting APIでの発生したリクエスト数を確認する
Google APIには、リクエスト数の制限があります。
Google アナリティクスは数多くのサイトで使用されています。Google では、処理能力を超える負荷がシステムにかからないようにすること、システム リソースが均等に配分されるようにすることを目的に、API リクエストに制限と割り当てを設けています。この制限と割り当ては変更されることがあります。
Analytics Reporting APIもタダで使えますが、リクエスト数に制限があるので制限を超えるとエラーコードが返却されるようになります。
Google Cloud Platformのプロジェクトでは「割り当て(1 日あたりの API リクエスト数など)」というものが設定できます。 割り当ての詳細は、割り当ての操作 | ドキュメント | Google Cloudを参照してください。
Analytics Reporting APIのリクエスト数制限
API リクエストの制限と割り当て - Google Developersと上記割り当て(割り当ては初期値のまま)ではリクエスト数の単位がバラバラしていてわかりにくかったのでそれぞれの情報を表にして集めてみました。
単位 | リクエスト数上限 | 参照元 |
---|---|---|
プロジェクト | 50,000件/日 | [APIとサービス]の割り当て & API リクエストの制限と割り当て |
プロジェクト | 1,200件/分 | [APIとサービス]の割り当て |
プロジェクト | 2,000件/100秒 | API リクエストの制限と割り当て |
ユーザー 1 人につきプロジェクトごとで | 100件/100秒(1,000 件まで引き上げ可能) 10件/秒 |
API リクエストの制限と割り当て |
ユーザー | 600件/分 | [APIとサービス]の割り当て |
ビュー | 10,000件/日(引き下げ不可) | API リクエストの制限と割り当て |
プロジェクトごとのリクエストの失敗(プロファイルあたり) | 10件/時 かつ 50件/日 | Reporting API のリクエスト エラーについて - API リクエストの制限と割り当て |
表を見ていて気が付いたのは「1日単位の制限」を守っても「1秒や100秒単位の制限」を守らないとエラーになってしまうということでした。特定の時間にどっとリクエストがくるとエラーになる可能性があるのですね。
Google Analytics Reporting APIでの発生したリクエスト数を確認します。
リクエスト数に制限があるので、APIを使うアプリケーションを作る場合にどのくらいリクエスト数が発生するかを確認したいと思いました。
Google APIのリクエスト数は[APIとサービス]の[ダッシュボード]で確認できます。
API 指標を表示する最も簡単な方法は、Google Cloud Console の API ダッシュボードを使用することです。すべての API の使用状況の概要を表示することも、特定の API の使用状況を詳細に調べることもできます。
- Google Cloud Platformにログインする
- 左上メニュー > [APIとサービス] > [ダッシュボード]で画面遷移する
各APIのリクエスト数を[割り当て]で詳しく確認します。
[ダッシュボード]ではAPI毎の「任意期間での合計リクエスト数」を確認できましたが、使っている特定APIのリクエスト数をもっと詳しく確認したい、そのんな時のことです。 例えば、今回はGoogle Analytics Reporting APIについて詳しく見てみます。
- [ダッシュボード]画面一覧表から[Analytics Reporting API]リンクで画面遷移する
- 左メニュー > [割り当て]で画面遷移する
- [Requests]の右にある「v」で詳細を表示 > プルダウンで[Requests1分あたり]を選択 > 表示期間を選択
- 表示される「平均」の期間は「表示期間」によって変わるようなので細かく見る場合は注意する
各ユーザーのリクエスト数を[指標]で確認します。
[割り当て]画面の下には制限数の一覧表があります。
「Requests 1 分あたり /ユーザー」という行がとても気になります。
そこで探し回っていると、[割り当て]と同じくAnalytics Reporting APIの[指標]をから各ユーザーのリクエスト数が確認できました。
- [ダッシュボード]画面一覧表から[Analytics Reporting API]リンクで画面遷移する
- 左メニュー > [指標]で画面遷移する
- [Credentials]プルダウンで見たいユーザーを選択 > [OK]ボタン > 表示期間を選択
1回のAPI呼出しにリクエストを複数指定してもリクエストは1回?
Analytics Reporting APIでは、1回のAPI呼出しにリクエストを5つまで含めることができます。
リクエストは最大で 5 つ送信できます。すべてのリクエストには、同じ dateRanges、viewId、segments、samplingLevel、および cohortGroup が含まれている必要があります。
メソッド: reports.batchGet | アナリティクス Reporting API v4 | Google Developers
1回のAPI呼出しにリクエストを複数指定するとカウントアップされるリクエスト回数は何回なのでしょうか?
let dateRanges = [{startDate: '2021-03-29', endDate: '2021-03-30'}] const res = await client.reports.batchGet({ requestBody: { reportRequests: [ { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:pagePath'}], metrics: [{expression: 'ga:pageviews'}, {expression: 'ga:avgTimeOnPage'}], }, { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:sourceMedium'}], metrics: [{expression: 'ga:bounceRate'}, {expression: 'ga:avgTimeOnPage'}], }, { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:source'}], metrics: [{expression: 'ga:pageviews'}], }, { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:browser'}], metrics: [{expression: 'ga:pageviews'}], }, { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:country'}], metrics: [{expression: 'ga:pageviews'}], } ] } })
なんと1件しかカウントアップされませんでした。同じ期間などを条件にするのであればまとめてAPI呼出ししたほうがお得なのかもしれません。
とはいえGoogleのドキュメントにある以下の記載は気になるところです。
★注: バッチ処理で複数の Reporting API リクエストを 1 回のリクエストにまとめても、定められた割り当て量を超えるリクエストを実行することはできません。
「バッチ処理」のリンク先がGoogle アナリティクス Management APIのGoogle アナリティクス API リクエストのバッチ処理になっているのでManagement APIを使った時のことなのかもしれませんが、実装の時には気をつけるポイントになりそうです。(Google アナリティクス Management APIを知らないのですが・・・)
はじめてのGoogle Analytics Reporting APIをちょっとだけ使ってみる
Google AnalyticsからAPIで情報を取得することになりました。
しかし、Google AnalyticsもGoogle APIも使ったことがないので全く分かりません。
ここでは、調べながらはてなブログの情報をGoogle Analytics Reporting APIで取得するまでをやってみます。
Google Analyticsの設定をする
まずは、情報の基となるGoogle Analyticsを設定します。 今回は、このponsuke_taro's blogをGoogle Analyticsに設定して情報を蓄積できるようにします。
はてなブログ用のデータ解析をユニバーサル アナリティクスプロパティで作成する
参考 : Google Analyticsを導入する - はてなブログ ヘルプ
ユニバーサル アナリティクスは前世代の Google アナリティクスです。ユニバーサル アナリティクス プロパティと Google アナリティクス 4 プロパティとでは、使用できるレポートに違いがあります。
- (ない場合)Google アカウントの作成でアカウントを作成する
- お客様のビジネスに適した分析ツールとソリューション - Google アナリティクス を表示する
- [無料で利用する]ボタンで次の画面へ遷移し、[測定を開始]ボタンで設定画面を表示する
- 「アカウント名」に何のアクセス解析かわかる名前を設定して[次へ]ボタンで進む
- 「プロパティ名」などの入力項目を設定して登録する
- レポートのタイムゾーン」「通貨」 : 日本に設定する
- [プロパティの設定]の下にある[詳細オプションを表示]リンクでユニバーサル アナリティクスプロパティの設定欄を表示して以下を設定して[次へ]ボタンで進む
- ユニバーサル アナリティクス プロパティの作成 : ON
- ウェブサイトの URL : 登録するはてなブログのURL
- ユニバーサル アナリティクスのプロパティのみを作成する : ON
- [ビジネス情報]部分は任意で入力して[作成]ボタンで作成する
- [プロパティ]で作成したプロパティを選択 > [トラッキング情報] > [トラッキングコード] > [トラッキングID]をメモしておく
はてなブログにトラッキングIDを設定する
- はてなブログにログインしてダッシュボード管理画面を表示する
- [設定] > [詳細設定] > [解析ツール] > [Google Analytics 埋め込み]にメモしたGoogle AnalyticsのトラッキングIDを入力する
- ページ下の[変更する]ボタンで変更を確定する
Google Cloud PlatformでAPI使えるようにする
作成したはてなブログ用のデータ解析に溜まった情報をGoogle Cloud Platform(以降GCP)のAPIで取得できるように設定していきます。
Google Analytics Reporting APIを有効化する
まずは、Google Analytics Reporting APIを使えるように設定します。
- Google Cloud Platformにログインする
- (ない場合)プロジェクトを作成する
- GCP画面上部でプロジェクトを選択 > [APIとサービス] > [ライブラリ]
- 「Google Analytics API」を検索して、「Google Analytics Reporting API」を選択する
- [有効にする]ボタンでAPIを有効化する
3つ候補に出てきたAPIの違いがいまいちわからないので頑張った概要の和訳を記録として書いておきます。
API名 | 概要の頑張った和訳 |
---|---|
Google Analytics API | Analyticsの設定およびレポートデータへのアクセスを提供します。 |
Google Analytics Reporting API | Google Analyticsでレポートデータにアクセスするための最も高度なプログラム的方法です。Google AnalyticsレポートAPIを使用すると、カスタムダッシュボードを構築してGoogle Analyticsデータを表示したり、複雑なレポートタスクを自動化して時間を節約したり、Google Analyticsデータを他のビジネスアプリケーションと統合したりすることができます。 |
Google Analytics Data API | Google Analyticsのレポートデータにアクセスします。 |
サービスアカウントを作成する
APIを利用するサービスアカウントを作成します。
サービス アカウントは IAM によって管理されるもので、人間のユーザー以外のものを指しています。App Engine アプリの実行や Compute Engine インスタンスとのやり取りなど、アプリケーション自体がリソースにアクセスする場合や、アクションを独自に実行する必要がある場合を対象としています。
- GCP画面上部でプロジェクトを選択 > サイドメニュー[APIとサービス] > [認証情報]
- [認証情報を作成]ボタン > [サービス アカウント]
- 「サービス アカウント名」「サービス アカウント ID」に任意の値を入力して[完了]ボタンでアカウントを作成する
- 一覧にある作成したアカウントの[操作] > [詳細を管理]で詳細画面を表示
- [キー]タブ > [鍵を追加] > [新しい鍵を作成]
- [キーのタイプ]で「JSON」を選択 > [作成]ボタンで作成して鍵ファイルをダウンロードする
- ここでダウンロードする鍵ファイルは「再作成できない」「鍵があればサービスを使えてしまう」ので超大切に保管する
Google Analyticsで作成したサービスアカウントに権限を設定する
作成したアカウントがGoogle Analyticsにあるはてなブログの情報を取得できるように権限を設定します。
- Google Analyticsにログインする
- 管理 > 対象のアカウント選択 > [アカウントユーザーの管理]
- 右上の[+]ボタン > [ユーザーを追加]
- 以下を設定して[追加]ボタンで追加する
- メールアドレス : 作成したサービスアカウントのサービスアカウントID(
@
以降も入力する) - 権限 : 表示と分析
- メールアドレス : 作成したサービスアカウントのサービスアカウントID(
Google Analytics Reporting APIを呼び出す
早速、Google Analytics Reporting APIを呼び出してみます。
PythonとCloud9でちょっとだけ使ってみる
メトリクスとディメンションって何?
レポートの作成 | アナリティクス Reporting API v4 | Google Developersで基本の使い方を見ていくとまずはmetrics
とdimensions
なるものに引っかかりました。
Web分析をやっている人には疑問に思わないことなのでしょうが、Google AnalyticsはおろかWeb分析的なことをやったことがないので「メトリクス」「ディメンション」が何なのかわかりません。
アナリティクスのレポートは、すべてディメンションと指標の組み合わせに基づいて構成されます。
ディメンションはデータの属性です。たとえば、ディメンション「市区町村」はセッションの性質を表し、「横浜」、「川崎」などセッションが発生した市区町村を指定します。ディメンション「ページ」は、閲覧されたページの URL を表します。
指標はデータを定量化したものです。指標「セッション」はセッションの合計数です。指標「ページ/セッション」は、セッションあたりの平均閲覧ページ数です。
「メトリクス=量」「ディメンション=量の単位」みたいな感じです(正確には違うけど)。
「日単位のPV数を取得したい」だと「日」がディメンションで「PV数」がメトリクス・・・的な。
HelloAnalytics.pyのget_report
メソッドにあるパラメータをこんな感じに変えると・・・
※. ここのコードははじめてのGoogle Analytics Reporting APIをPythonとCloud9でちょっとだけ使ってみる - ponsuke_tarou’s blogで使ったものを基にしています。
# ...省略... 'reportRequests': [ { 'viewId': VIEW_ID, 'dateRanges': [{'startDate': '7daysAgo', 'endDate': 'today'}], # メトリクスにPVを指定 'metrics': [{'expression': 'ga:pageviews'}], # ディメンションに日にちを指定 'dimensions': [{'name': 'ga:date'}] }] # ...省略...
こんな感じで「日単位のPV数」が取得できました。
ga:date: 20210316 Date range: 0 ga:pageviews: 411 ga:date: 20210317 Date range: 0 ga:pageviews: 845 ga:date: 20210318 Date range: 0 ga:pageviews: 787 ga:date: 20210319 Date range: 0 ga:pageviews: 498
メトリクスとディメンションで指定する値の意味が分からなくなりそうなので使ったものはメモしていきます。
dimensions | 意味 | レスポンス例 |
---|---|---|
ga:date | 日付 | 20210322 |
ga:pagePath | ページ | "/ponsuke0531/items/edf2eee638202aa7f61f" |
ga:sourceMedium | 参照元 | "zenn.dev / referral" |
ga:dimension{インデックス番号} | カスタム ディメンション |
どこかに日本語で一覧があったらいいのに・・・。
metrics | 意味 | type | レスポンス例 |
---|---|---|---|
ga:pageviews | ページビュー数 | INTEGER | 1631 |
ga:avgTimeOnPage | 平均ページ滞在時間(秒) | TIME | "387.46666666666664" |
ga:bounceRate | 直帰率 | PERCENT | "0.0" |
Google APIには、リクエスト数の制限がありますので気をつけましょう。
遊びで使っているだけならいいのですが、お仕事で使う場合などにはAPIへのリクエスト数の制限がありますので気をつけましょう。
Node.jsでちょっとだけ使ってみる
1回のAPI呼出しで5こまでリクエストを送信できます
各リクエストには、別々のレスポンスが返されます。リクエストは最大で 5 つ送信できます。すべてのリクエストには、同じ dateRanges、viewId、segments、samplingLevel、および cohortGroup が含まれている必要があります。
リクエストの本文 | メソッド: reports.batchGet | アナリティクス Reporting API v4 | Google Developers
こんな感じで1回のAPI呼出しで2このリクエストを送信してみました。
※. ここのコードははじめてのGoogle Analytics Reporting APIをNode.jsでちょっとだけ使ってみる - ponsuke_tarou’s blogで使ったものを基にしています。
let dateRanges = [{startDate: '2021-03-19', endDate: '2021-03-30'}] // 1回のAPI呼出し const res = await client.reports.batchGet({ requestBody: { reportRequests: [ // 1こ目のリクエスト { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:pagePath'}], metrics: [{expression: 'ga:pageviews'}, {expression: 'ga:avgTimeOnPage'}], orderBys: {fieldName: 'ga:pageviews', sortOrder: 'DESCENDING'}, pageSize: 3 }, // 2こ目のリクエスト { viewId: VIEW_ID, dateRanges: dateRanges, dimensions: [{name: 'ga:sourceMedium'}], metrics: [{expression: 'ga:bounceRate'}, {expression: 'ga:avgTimeOnPage'}], orderBys: {fieldName: 'ga:avgTimeOnPage', sortOrder: 'DESCENDING'}, pageSize: 3 } ] } })
レスポンスも2こ分返ってきました。(レスポンスは手ごろに改行しています。)
{"reports":[ { "columnHeader":{"dimensions":["ga:pagePath"],"metricHeader":{"metricHeaderEntries":[{"name":"ga:pageviews","type":"INTEGER"},{"name":"ga:avgTimeOnPage","type":"TIME"}]}}, "data":{"rows":[ {"dimensions":["/ponsuke0531/items/4629626a3e84bcd9398f"],"metrics":[{"values":["1631","387.46666666666664"]}]}, {"dimensions":["/ponsuke0531/items/df51a784b5ff48c97ac7"],"metrics":[{"values":["1160","601.5056179775281"]}]}, {"dimensions":["/ponsuke0531/items/edf2eee638202aa7f61f"],"metrics":[{"values":["999","401.8253968253968"]}]} ], "totals":[{"values":["35429","382.3464974141984"]}], "rowCount":499, "minimums":[{"values":["1","0.0"]}], "maximums":[{"values":["1631","1679.0"]}]}, "nextPageToken":"3" }, { "columnHeader":{"dimensions":["ga:sourceMedium"],"metricHeader":{"metricHeaderEntries":[{"name":"ga:bounceRate","type":"PERCENT"},{"name":"ga:avgTimeOnPage","type":"TIME"}]}}, "data":{"rows":[ {"dimensions":["27.94.140.223:8080 / referral"],"metrics":[{"values":["0.0","1335.0"]}]}, {"dimensions":["nekorokkekun.hatenablog.com / referral"],"metrics":[{"values":["0.0","996.0"]}]}, {"dimensions":["zenn.dev / referral"],"metrics":[{"values":["0.0","909.0"]}]} ], "totals":[{"values":["86.22115384615384","382.3464974141984"]}], "rowCount":69, "minimums":[{"values":["0.0","0.0"]}], "maximums":[{"values":["100.0","1335.0"]}]}, "nextPageToken":"3" } ]}
試しにリクエストの部分をコピペして6こリクエストを送信したらちゃんと以下のエラーになりました。
$ node analytics.js (node:7580) UnhandledPromiseRejectionWarning: Error: There are too many requests in the batch request. The max allowed is 5 at Gaxios._request (C:\path\google-analytics-api\node_modules\gaxios\build\src\gaxios.js:127:23) at process._tickCallback (internal/process/next_tick.js:68:7) (node:7580) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) (node:7580) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
JavaとSpringBootでちょっとだけ使ってみる(現在奮闘中)
さて、実際に使いたい環境でもやってみようと思ったら・・・そもそもPythonは使っていない・・・本当にぽんすけですね。 ということで今度はJavaを使ってSpring Bootの環境でやってみたいと思います。
- Spring Bootのプロジェクトを用意する
- クライアント ライブラリをインストールする
SpringのRestTemplateでAPIを呼び出す時にクエリパラメータをくっつける
RestTemplateでAPIを使うのにパラメータの設定方法を試しました。
はじめてSpring BootでRestTemplateを使ってAPIを呼び出すことになりました。
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations
HTTP リクエストを実行する同期クライアント。JDK HttpURLConnection、Apache HttpComponents などの基盤となる HTTP クライアントライブラリ上でシンプルなテンプレートメソッド API を公開します。 RestTemplate は、頻度の低いケースをサポートする一般化された exchange および execute メソッドに加えて、HTTP メソッドによる一般的なシナリオのテンプレートを提供します。
クエリパラメータは、必須でない場合に設定したりしなかったりと可変になりますが、どんな方法がいいのでしょう? クエリパラメータを設定するにはどんな方法がいいのか試してみることにしました。
e-StatのAPIをサンプルに使います。
サンプルに呼び出すAPIは、政府統計の総合窓口(e-Stat)−API機能の統計表情報取得を使います。
パラメータには以下の値を設定します。
パラメータ名 | 意味 | 設定する値 |
---|---|---|
appId | アプリケーションID | e-Statのサイトで取得したアプリケーションID |
openYears | 公開年月 | 202102 |
statsField | 統計分野 | 0204(人口移動) |
limit | データ取得件数 | 1 |
ControllerをでAPIを呼び出しちゃいます。
Serviceとかで呼ぶ方がいいと思いますが、とにかくパラメータをくっつける練習としてControllerでAPIを呼び出します。
package com.example.demo.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.JsonNode; @RestController @RequestMapping("/estat") public class EstatController { private static final String URL = "http://api.e-stat.go.jp/rest/3.0/app/json"; /** アプリケーションID. */ private String appId = "02b...e-Statのサイトで取得したアプリケーションID"; /** 公開年月. */ private String openYears = "202102"; /** 統計分野. */ private String statsField = "0204"; /** データ取得件数. */ private String limit = "1"; @GetMapping("/getStatsList") public JsonNode getStatsList() { RestTemplate restTemplate = new RestTemplate(); ResponseEntity<JsonNode> response = // .....ここに呼び出しの処理を書いていきます......... JsonNode jsonNode = response.getBody(); return jsonNode; } }
クエリパラメータをくっつける
- 環境
パラメータを1つ1つ設定する
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException 指定された URL で GET を実行して、エンティティを取得します。レスポンスは変換され、ResponseEntity に保存されます。 パラメーター: url - URL responseType - 戻り値の型 uriVariables - テンプレートを展開する変数
上記のgetForEntityを使って書きました。
@GetMapping("/getStatsList") public JsonNode getStatsList() { RestTemplate restTemplate = new RestTemplate(); // 1. getForEntityでクエリパラメータ一つ一つを設定して、APIを呼び出す. ResponseEntity<JsonNode> response = restTemplate.getForEntity( URL + "/getStatsList?appId={appId}&openYears={openYears}&statsField={statsField}&limit={limit}", JsonNode.class, appId, openYears, statsField, limit); JsonNode jsonNode = response.getBody(); return jsonNode; }
UriComponentsBuilderでパラメータを設定する
public <T> ResponseEntity<T> exchange(RequestEntity<?> entity, Class<T> responseType) throws RestClientException 指定された RequestEntity で指定されたリクエストを実行し、レスポンスを ResponseEntity として返します。通常、たとえば RequestEntity の静的ビルダーメソッドと組み合わせて使用されます。 パラメーター: entity - リクエストに書き込むエンティティ responseType - 戻り値の型
UriComponentsBuilderでパラメータを設定して、上記のexchangeでAPIを呼び出しました。
queryParamでパラメータ名と値を設定する
public UriComponentsBuilder queryParam(String name, Object... values) 指定されたクエリパラメーターを追加します。パラメーター名と値の両方に、後で値から展開される URI テンプレート変数を含めることができます。値が指定されていない場合、結果の URI にはクエリパラメーター名のみが含まれます。"?foo=bar" の代わりに "?foo"。 パラメーター: name - クエリパラメーター名 values - クエリパラメーター値
UriComponentsBuilderのqueryParamを使ってクエリパラメータを設定しました。
@GetMapping("/getStatsList") public JsonNode getStatsList() throws URISyntaxException { RestTemplate restTemplate = new RestTemplate(); // 1. エンドポイントからUriComponentsBuilderを作成する. UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(URL + "/getStatsList"); // 2. クエリパラメータを設定して文字列化する. String uri = builder.queryParam("appId", appId) .queryParam("openYears", openYears) .queryParam("statsField", statsField) .queryParam("limit", limit) .toUriString(); // 3. パラメータを設定したURLからRequestEntityを作成する. RequestEntity<Void> requestEntity = RequestEntity.get(new URI(uri)).build(); // 4. exchangeでAPIを呼び出す. ResponseEntity<JsonNode> response = restTemplate.exchange(requestEntity, JsonNode.class); JsonNode jsonNode = response.getBody(); return jsonNode; }
queryParamsでMultiValueMapを使って設定する
public UriComponentsBuilder queryParams(@Nullable MultiValueMap<String, String> params) 複数のクエリパラメーターと値を追加します。 パラメーター: params - パラメーター
UriComponentsBuilderのqueryParamsを使ってクエリパラメータを設定しました。
1つのキーに複数の値を設定できるMap(org.springframework.util.MultiValueMap)
MultiValueMapはよく使うMapとはちょっと違うもののようです。
@GetMapping("/getStatsList") public JsonNode getStatsList() throws URISyntaxException { RestTemplate restTemplate = new RestTemplate(); // 1. エンドポイントからUriComponentsBuilderを作成する. UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(URL + "/getStatsList"); // 2. Mapにクエリパラメータを設定する. MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add("appId", appId); params.add("openYears", openYears); params.add("statsField", statsField); params.add("limit", limit); // 3. Mapでクエリパラメータを設定して文字列化する. String uri = builder.queryParams(params).toUriString(); // ...これ以降は「queryParamでパラメータ名と値を設定する」と同じ...
AWSのSecrets Managerに大切な情報を登録する
認証に使うAPIキーやWebhookに設定したSecretなどをLambda関数で使うことがあります。これらは大切な情報なのでSecrets Managerに登録して、Lambda関数から取得して使うようにします。
- AWSのコンソール > [Secrets Manager]
-
- シークレットの種類: APIキーや各種ユーザ情報などは「その他のシークレット」を選択
- シークレットのペア: 大切な情報の名前(シークレットキー)と大切な情報の値を設定、複数設定してもOK
- 暗号化キー: DefaultEncryptionKey
- [保存]ボタンでシークレットを作成する
AWSのSecrets Managerを使った事例
Lambdaの実行権限とAPI Gatewayを作成する
ポリシーがよくわからないのでちょっと勉強します。
IAMのポリシーを作成する
- AWSマネジメントコンソールにある[IAM] > サイドメニューの[ポリシー]
- [ポリシーの作成]ボタンで作成画面を開きます。
- [JSON]タブを開いて権限の設定内容を入力します。
- この設定は作成後でも変更できます。
- 権限の対象となる
Resource
はワイルドカード*
で広く設定すると簡単ですが、セキュリティを意識して細かく狭めて設定します。
- [次のステップ:タグ]ボタンから次の画面でタグを設定(設定しなくてもOK) > [次のステップ:確認]ボタン
- 確認画面へ遷移して[名前(必須)]と[説明(任意)]を入力します。
- [ポリシーの作成]ボタンでポリシーを作成して一覧画面に戻ります。
IAMのロールを作成する
- AWSマネジメントコンソールにある[IAM] > サイドメニューの[ロール]
- [ロールの作成]ボタンで作成画面を開きます。
- [AWSサービス] > [Lambda]を選択後に[次のステップ: アクセス権限]ボタンで次の画面を開きます。
- [Attach アクセス権限ポリシー]の[ポリシーのフィルタ]で作成したポリシーを検索して選択後に[次のステップ: タグ]ボタンで次の画面を開きます。
- [タグの追加 (オプション)]は任意なので設定せずに[次のステップ: 確認]ボタンで確認画面を開きます。
- [ロール名]を入力して[ロールの作成]ボタンでロールを作成して一覧画面に戻ります。
Lambda関数を作る
実装を後回しにして関数だけ作ります。
- [AWS マネジメントコンソール]から[Lambda]の画面を開く
- [関数の作成]ボタンで作成画面を表示する
- [一から作成]を選択して以下を設定して[関数の作成]ボタンで関数を作成する
- 関数名 : 任意の名前
- ランタイム : 使いたいもの
- 実行ロール : 既存のロールを使用する
- 既存のロール : 作成したロールを選択
コードには初期コードがあるのでそのまま。実装はAPI Gateway作成後にやります。
import json def lambda_handler(event, context): # TODO implement return { 'statusCode': 200, 'body': json.dumps('Hello from Lambda!') }
API Gatewayを作成する
- 参考
作成する種類は上記の参考リンクを読んで決めました。
HTTP APIで作成する
- [HTTP API]の[構築]ボタンで次の画面へ
- [統合を追加] > [Lambda] > [Lambda 関数]で作成したLambda関数を選択
- [API 名]に任意の名前を設定して[次へ]ボタンで[ルートを設定]画面へ
- 以下を設定して[次へ]ボタンで[ステージを定義]画面へ
- メソッド : POST
- リソースパス :
/{Lambda関数名}
- 統合ターゲット : {Lambda関数名}
- [ステージを追加]ボタンで以下を追加して[次へ]ボタンで[確認して作成]画面へ
- ステージ名 :
$default
- 自動デプロイ : ON
- ステージ名 :
- [作成]ボタンで作成する
- 「{Lambda関数名}のステージ」一覧の[URLを呼び出す]列に「呼び出しURL」が表示される
- curlコマンドを使ってAPI Gatewayを呼び出してAPI GatewayがLambda関数を呼び出せることを確認する
curlコマンドのオプション | 意味 |
---|---|
-X | HTTPメソッドを指定する |
-H | HTTPヘッダを指定する |
# Lambda関数の初期コードに書いてある「Hello from Lambda!」が返却される $ curl -X POST -H 'Content-Type:application/json' {呼び出しURL}/{リソースパス} % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed } 100 20 100 20 0 0 50 0 --:--:-- --:--:-- --:--:-- 50"Hello from Lambda!" # 失敗例) 「リソースパス」をくっつけ忘れると「Not Found」になるので注意 $ curl -X POST -H 'Content-Type:application/json' {呼び出しURL} % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 23 100 23 0 0 287 0 --:--:-- --:--:-- --:--:-- 291{"message":"Not Found"}