TORICO Tech ブログhttps://tech.torico-corp.com/blog/2024-03-28T10:46:39+00:00株式会社TORICO 技術開発チームのブログ新卒エンジニアが最初の半年に任された業務2021-09-18T12:28:58+00:002024-03-28T02:49:00+00:00清瀬遼平https://tech.torico-corp.com/blog/author/r.kiyose/https://tech.torico-corp.com/blog/%E6%96%B0%E5%8D%92%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%8C%E6%9C%80%E5%88%9D%E3%81%AE%E5%8D%8A%E5%B9%B4%E3%81%A7%E8%A1%8C%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8/<p>今年の春に入社しました、情報システム部の清瀬です。</p>
<p>私がTORICOに入社からそろそろ半年が経過しようとしています。このタイミングで、この半年どういった業務を担当してきたのかを記事にしようと思います。TORICOに興味をもっている方にTORICOのエンジニア業務がどんなものなのか、知ってもらえればと思います。</p>
<h3>社内アプリへの権限付与の自動化</h3>
<p></p>
<p>TOIRCOには <a href="https://id.torico-corp.com/">TORICO-ID</a> というログインを共通化できるアプリがあります。マンガ全巻ドットコム や マンガ展 と連携させることでログインやアカウント作成の簡略化ができます。このアプリは10近くある社内アプリにも使われています。社員全員がボタン一つで簡単にログインできるのですが、問題点が一つありました。それは、Admin サイトへの権限の問題です。</p>
<p>TORICO の社員と一般のユーザーを分けるフラグはあります。そのため、社員にのみ権限を与えることは今までもできていました。しかし、全社員に全ての社内アプリのAdmin権限を付与するわけにはいきません。そのため今までは情報システム部が手動で権限の付与をしていました。</p>
<p>そこで部署ごとに権限を付与するサイトを事前に決めておき、権限の付与の自動化を行えるようにしました。作成したモデルは具体的には</p>
<ul>
<li>Website
<ul>
<li>権限を与えるアプリのモデル</li>
<li>部署とM2Mの関係</li>
<li>django-allauth の Application と 1:N の関係</li>
</ul>
</li>
<li>ClientGroup
<ul>
<li>ログイン成功時にユーザーに与える、クライアント側の Django の Group のモデ</li>
<li>django-allauth の Application と 1:N の関係</li>
</ul>
</li>
<li>Department
<ul>
<li>部署・部門のモデル</li>
<li>Website と M2M の関係</li>
<li>ClientGroup と M2M の関係</li>
<li>User と M2M の関係</li>
</ul>
</li>
</ul>
<p>の3つです。TORICO では Mezzanin という Django の CMS を使用しているため、Mezzanine の権限も付与できるように Website を Application と 1:N の関係にしています。</p>
<p>あとは Department に Application と ClientGroup を結びつけておくことで、どの部署にどの権限を付与するのかを自動で判別できるようになりました。TORICO は現在拡大期であり毎月のように新しく入社される方がいるため、この機能は非常に役立っていると個人的には思います。また、エンジニアとしても予想外のメリットがありました。それは新しいプロジェクトのローカル環境を作成した際に、いちいち SQLクライアントツールで自分のアカウントにスタッフ権限を付与しなくて済むことです。特に情報システム部は担当するアプリが多いため、その一手間を省けるのは非常に便利です。</p>
<h3>古いシステムの Django 化</h3>
<p>長年TORICOを支えてきてくれたシステムですが、その中の幾つかを Django に移行するお手伝いもしました。元のコードを読みながら、その機能を Django で再現するという業務でした。元のコードは PHP で書かれているため、PHP と Python を比較するような作業で、個人的にはとても楽しかったです。なにより、古いシステムを新しいシステムに置き換えることで、一つ一つの処理が非常に効率が良くなり、格段にレスポンスを早くすることができました。あらためてフレームワークの凄さを目の当たりできてよかったです。TORICO にはまだ古いシステムが残っているので、来期も移行の業務を積極的にやっていきたいと思っています。</p>
<h3>MySQL 5.6 を 5.7 にバージョンアップする</h3>
<p>RDS の MySQL5.6 のサポート期限が8月末(延長されて22/3/1まで)までであるということで、MySQL のパージョンアップもさせていただきました。手順としては、</p>
<ol>
<li>dev 環境のバージョンアップを行い、全ての機能が使えるかテストする</li>
<li>1 で問題あった機能を修正する</li>
<li>スナップショットでバックアップをとる</li>
<li>修正したコードをデプロイして、本番のバージョンアップを行う</li>
</ol>
<p>今回のバージョンアップでは1の部分で問題がありました。対象のアプリでは、ログイン時に MySQL の OLD_PASSWORD というハッシュ関数を使っていたのですが、それがサポートされなくなりました。そのため、代替となる関数をアプリ側で作成しなければなりませんでした。幸い、既に再現をしてくれているコードがあったため、それを拝借するだけで問題はありませんでした。ただ、ログインというアプリの基幹機能の修正だったため、本番へのデプロイは非常に緊張しました。</p>
<p>また、本番のバージョンアップ時にオプションの設定を忘れるというミスをしてしまいました。古いものが引き継がれると勘違いしていたことが原因です。その結果タイムゾーンの設定が狂ってしまい、表示されるべきコンテンツが表示されないという状況になってしまいました。普段からお世話になっている先輩に手助けしていただいたため無事に解決できましたが、もう少しで大惨事となるところでした。今後はもっと慎重に、情報集めからテストまでを行わなければいけないなと反省しています。</p>
<h3>最後に</h3>
<p>今回は、私がこの半年で任せていただいた主要な業務をいくつか紹介させていただきました。以上に取り上げたもの以外にも、</p>
<ul>
<li>新機能の追加
<ul>
<li>5件</li>
</ul>
</li>
<li>Django 化
<ul>
<li>2件</li>
</ul>
</li>
</ul>
<ul>
<li>DB・ネットワーク
<ul>
<li>2件</li>
</ul>
</li>
<li>セキュリティ(別ブログに記載)
<ul>
<li>10件</li>
</ul>
</li>
<li>コードの軽微な修正
<ul>
<li>16件</li>
</ul>
</li>
</ul>
<p>と、様々な業務を体験できました。今回はシステムに関係する業務だけを取り上げましたが、他にも壁にドリルで穴を開けたり社内サーバーを移動させたりと、普段はできない業務も経験させていただきました。TORICO ではフルスタックであることが求められるため、業務も多岐にわたります。そこに魅力を感じた方は、ぜひ TORICO に応募してみてください。</p>サーバレスアプリケーション (HTML/JSのみ) で、Google Analytics API を使ってアクティブユーザー数を表示するダッシュボードを作る2021-05-01T02:07:01+00:002024-03-28T10:46:39+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/google-analytics-api-request-nuxt-client/<pre><span></span></pre>
<p>Google Analytics は、API経由で様々な数値を取得することができます。<br/>今回は、API経由でサイトのアクティブユーザー数を取得して、Nuxtで作ったダッシュボード風JSアプリに表示してみます。</p>
<p>Google Analytics の APIから値を表示してウェブページに表示する場合、APIとの通信をサーバサイドで PHP や Python のプログラムで取得してウェブブラウザに表示時する方法と、ウェブブラウザ自体が直接 Google Analytics API にリクエストして表示する方法と、どちらの方法でも実現できます。</p>
<p>サーバサイドで行う場合、認証情報やシステムを隠蔽でき、またHTTPリクエストが無くともバックグラウンドで値を取得し続けることができるなど、大きな利点がありますが、常時起動するサーバを用意しておかないといけないため構築が若干手間です。</p>
<p>今回は、手間をかけずに実現したかったため、ブラウザと静的リソースのみで動作するものを作ります。<br/>APIへの認証は、ブラウザにログインしている Google ユーザーが行います。</p>
<h2>認証のしくみ</h2>
<p>Googleの提供している APIは、OAuth2 の認証・認可が必要となります。</p>
<p>アプリのページを開くと、Google のログインページを表示して認証を求め、認証されるとダッシュボードアプリの指定したコールバックURL にリダイレクトされるようにします。</p>
<p>リダイレクトされると、コールバックURL末尾の # (ハッシュ, フラグメント) の後に<span>アクセストークン</span>が含まれた状態になりますので、それを nuxt 内の JSでパースし、ブラウザのメモリ(変数)に格納します。</p>
<p>注意点として、認証情報(アクセストークン)がブラウザの変数に格納されるため、XSSが発生した際にアクセストークンが漏洩するリスクがあります。これはJSで認証情報を扱う以上避けられません。</p>
<p>XSSによる認証情報漏洩を防ぐには、GoogleとのAPIリクエストをサーバサイドで行い、サーバとの認証は httpOnly属性のついたセッションクッキーで行うべきですが、今回は、静的リソースなのでXSSは通常発生しないという前提の元、リスクを受容した上でクライアントサイドJSで機能を提供するものとします。</p>
<p>コールバックURLから取得したアクセストークンを認証ヘッダに含めて、ブラウザから Google Analytics にAPIリクエストをすることで、アクティブユーザー数などを取得できます。</p>
<p>アクセストークンは、localStorage に保存すると、セッション終了後も保持されるため漏洩の危険性が上がります。そのため変数として保存するだけにとどめます。ただし、OAuth2の state パラメータは、ブラウザセッションが変更されても維持する必要があるため、localStorage に一時的に保存します。</p>
<p>アプリのページを開くたびに、OAuth2のアクセストークンをリクエストし続けるとすると、毎回 Google のアカウント選択ページが出て面倒なように感じられるかもしれませんが、実際に使ってみるとそんなことはなく、一度ブラウザと Google の認証セッションクッキーが作られればあとはリダイレクトしか発生せず、1秒程度で自動的に認証が終わりアクセストークンが取得済みの状態になりますので、使用感として悪くはありません。</p>
<h3><span> 試しにAPIにリクエストする</span></h3>
<p><a href="https://developers.google.com/analytics/devguides/reporting/realtime/v3/reference/data/realtime/get?#try-it">https://developers.google.com/analytics/devguides/reporting/realtime/v3/reference/data/realtime/get?#try-it</a></p>
<p>こちらの Google アナリティクスのページを見ると、APIの説明と共に、中央もしくは右側にAPIのテストリクエストができるフォームが表示されます。</p>
<p>ids に、 ga: を先頭につけた GoogleAnalytics のID番号を書き、metrics は rt:activeUsers を入れ、</p>
<p><img alt="" height="706" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-006.png" width="1323"/></p>
<p>下部 「Google OAuth 2.0」と「API key」にチェックを入れ、「Execute」 ボタンを押します。</p>
<p>すると、さらに下部にレスポンスデータが表示されます。<br/>この情報を、nuxt 等ウェブアプリケーションで取得できます。</p>
<p><img alt="" height="732" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-007.png" width="637"/></p>
<h2><span>作り方</span></h2>
<h3><span>Google Cloud Platform のプロジェクトを作る</span></h3>
<p>まず、Google APIを使うため、GCPのプロジェクトを作ります。</p>
<p>GCPのダッシュボードの上部ヘッダから、プロジェクトを新しく作ります。<br/><a href="https://console.cloud.google.com/home/dashboard">https://console.cloud.google.com/home/dashboard</a></p>
<p><img alt="" height="634" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-001.png" width="1128"/></p>
<h4><span>ライブラリの追加</span></h4>
<p>プロジェクトを作ったら、APIとサービスページを開きます。<br/><a href="https://console.cloud.google.com/apis/dashboard">https://console.cloud.google.com/apis/dashboard</a></p>
<p>左メニューの「ライブラリ」をクリックし、ライブラリの追加ページで、Google Analytics API と Google Analytics Reporting API を有効にします。</p>
<p><img alt="" height="577" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-002.png" width="1056"/></p>
<h4><span>認証情報の追加</span></h4>
<p>次は、認証情報ページを開きます。<br/><a href="https://console.cloud.google.com/apis/credentials">https://console.cloud.google.com/apis/credentials</a></p>
<p><img alt="" height="592" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-003.png" width="1038"/></p>
<p>上部「認証情報を作成」リンクから、「API キー」を選択し、APIキーを一つ作ります。</p>
<p>続いて、「認証情報を作成」リンクから、「OAuthクライアント ID」を選択します。</p>
<p>アプリケーションの種類 は、今回は「ウェブアプリケーション」で、名前は、適当に「nuxt client」とかにします。</p>
<p>「承認済みの JavaScript 生成元」は、クロスオリジンリクエストを許可するオリジン名です。<br/>平文 http でもOKなので、</p>
<pre><a href="http://localhost:3000">http://localhost:3000</a></pre>
<p>などを追加しておきます。</p>
<p>「承認済みのリダイレクト URI」は、OAuth2 の認証フローのリダイレクトURL(コールバックURL)です。<br/>認証完了時、このURLに、アクセストークンが URLに含まれた形でリダイレクトされます。<br/>今回は、</p>
<pre><a href="http://localhost:3000/code">http://localhost:3000/code</a></pre>
<p>としました。<br/>本番用のURLが決まっていたら、その分も追加します。後からでも追加できます。</p>
<p>スクリーンショットの例は、ローカルPCで docker + Nginx でポート80 でサービスする場合を考慮し、<br/>http://localhost も追加してあります。</p>
<p><img alt="" height="760" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-004.png" width="818"/></p>
<p>ちなみに、クライアントID とクライアントシークレット が作成されますが、ブラウザからのリクエストの場合はクライアントシークレットは使いません。</p>
<p>OAuth 同意画面は、今回は社内用途のみ考慮しているので「内部」を選択し、作成します。</p>
<p>プロジェクトの状態によっては、この選択肢は無いかもしれません。</p>
<p><img alt="" height="722" src="https://d1qjlssvz4u32r.cloudfront.net/media/uploads/site-6/analytics-api/analytics-api-005.png" width="1013"/></p>
<p>これで、GCP のプロジェクトの設定は終了です。</p>
<h3><span>アプリを書く</span></h3>
<p>今回は、nuxt + TypeScript で書きます。すべてを載せると冗長なので、要点のみ記載します。</p>
<p>流れとしては、</p>
<p>1.<br/>まずブラウザで / ページ表示時。<br/>最初はアクセストークンが無いため、<br/>store/auth.ts の navigateTokenRequestUrl がコールされる。</p>
<p>2.<br/>navigateTokenRequestUrl の中では、ステートパラメータを作ってローカルストレージに保存し、<br/>Google の認証URLに遷移。</p>
<p>3.<br/>認証が完了すると、リダイレクトURL である /code に着地します。</p>
<p>4.<br/>/code では、URLのハッシュ(フラグメント) から、アクセストークンとステートを取得し、<br/>ステートの一致を検証後、アクセストークンを保存し、/ に遷移します。</p>
<p>5.<br/>/ では、今回はアクセストークンがあるため、<br/>コンポーネント内でそのアクセストークンを使って Google API でリアルタイムユーザー数を取得して表示します。</p>
<p>6.<br/>このアクセストークンの寿命は1時間のため、1時間するとGoogle API は http 401 を返すようになります。<br/>その場合、もう一度アクセストークン取得URLに遷移することで、アクセストークンの取り直しをします。<br/><br/>この時、Google とのセッションクッキーが認証済みであれば、ユーザー操作は不要で、一瞬画面がちらつくだけでアクセストークンの更新が完了します。</p>
<h4><span>設定ファイル settings.ts</span></h4>
<pre><span></span><span>export const </span><span>GOOGLE_API</span><span> = {<br/></span><span> </span><span>authAPIEndpoint</span><span>: 'https://accounts.google.com/o/oauth2/v2/auth',<br/></span><span> </span><span>analyticsAPIEndpoint</span><span>: 'https://www.googleapis.com/analytics/v3/data/realtime',<br/></span><span> </span><span>clientId</span><span>: '<GoogleAPIの認証情報で作成されてたクライアントID>.apps.googleusercontent.com',<br/></span><span> </span><span>apyKey</span><span>: 'AIz<GoogleAPIの認証情報で作成されてたAPIキー>o04',<br/></span><span> </span><span>scope</span><span>: 'https://www.googleapis.com/auth/analytics.readonly'<br/></span><span>}<br/></span><span><br/></span><span>export const </span><span>GOOGLE_ANALYTICS_ACCOUNTS</span><span> = [<br/></span><span> {<br/></span><span> </span><span>id</span><span>: '157xxxxx',<br/></span><span> </span><span>title</span><span>: 'サイト1'<br/></span><span> },<br/></span><span> {<br/></span><span> </span><span>id</span><span>: '872xxxxx',<br/></span><span> </span><span>title</span><span>: 'サイト2'<br/></span><span> },<br/></span><span>]</span></pre>
<h4>型ファイル types/tasks.d.ts</h4>
<pre><span>interface </span><span>TaskResult</span><span> {<br/></span><span> </span><span>success</span><span>: boolean;<br/></span><span> </span><span>message</span><span>: string;<br/></span><span>}</span><span><br/></span><span></span></pre>
<h4><span>Vuex Store モジュール store/auth.ts</span></h4>
<pre><span>import { Action, Module, Mutation, VuexModule } from 'vuex-module-decorators'<br/></span><span>import { GOOGLE_API } from '~/settings'<br/></span><span><br/></span><span>// OAuth2 state パラメータの localStorage一時保存用<br/></span><span>const </span><span>LOCAL_STORAGE_AUTH_STATE_KEY</span><span> = 'authState'<br/></span><span><br/></span><span>@Module({<br/></span><span> </span><span>name</span><span>: 'auth',<br/></span><span> </span><span>stateFactory</span><span>: true,<br/></span><span> </span><span>namespaced</span><span>: true<br/></span><span>})<br/></span><span>export default class extends VuexModule {<br/></span><span> </span><span>accessToken</span><span>: string = ''<br/></span><span><br/></span><span> /**<br/></span><span> * 認証済みか?<br/></span><span> */<br/></span><span> get </span><span>authorized</span><span> () {<br/></span><span> return this.</span><span>accessToken</span><span> !== ''<br/></span><span> }<br/></span><span><br/></span><span> /**<br/></span><span> * アクセストークンを保存<br/></span><span> */<br/></span><span> @Mutation<br/></span><span> </span><span>setAccessToken</span><span> (token: string) {<br/></span><span> this.</span><span>accessToken</span><span> = token<br/></span><span> }<br/></span><span><br/></span><span> /**<br/></span><span> * OAuth2 認証URLに移動する<br/></span><span> */<br/></span><span> @Action<br/></span><span> </span><span>navigateTokenRequestUrl</span><span> () {<br/></span><span> // 乱数で state パラメータを作成<br/></span><span> const </span><span>authState</span><span> = [...</span><span>Array</span><span>(30)].</span><span>map</span><span>(<br/></span><span> () => </span><span>Math</span><span>.</span><span>random</span><span>().</span><span>toString</span><span>(36)[2]).</span><span>join</span><span>('')<br/></span><span> // state パラメータをローカルストレージに保存する<br/></span><span> </span><span>window</span><span>.</span><span>localStorage</span><span>.</span><span>setItem</span><span>(</span><span>LOCAL_STORAGE_AUTH_STATE_KEY</span><span>, </span><span>authState</span><span>)<br/></span><span> const </span><span>params</span><span> = [<br/></span><span> ['scope', GOOGLE_API.</span><span>scope</span><span>],<br/></span><span> ['include_granted_scopes', 'true'],<br/></span><span> ['response_type', 'token'],<br/></span><span> ['state', </span><span>authState</span><span>],<br/></span><span> ['redirect_uri', `${</span><span>window</span><span>.</span><span>location</span><span>.</span><span>origin</span><span>}/code`],<br/></span><span> ['client_id', GOOGLE_API.clientId]<br/></span><span> ]<br/></span><span><br/></span><span> </span><span>window</span><span>.</span><span>location</span><span>.</span><span>href</span><span> = `${GOOGLE_API.authAPIEndpoint}?` +<br/></span><span> </span><span>params</span><span>.</span><span>map</span><span>(i => `${i[0]}=${encodeURIComponent(i[1])}`).</span><span>join</span><span>('&')<br/></span><span> }<br/></span><span><br/></span><span> /**<br/></span><span> * OAuth2認証完了後のURLのハッシュ(フラグメント)を解析して、アクセストークンを保存<br/></span><span> */<br/></span><span> @Action<br/></span><span> </span><span>parseResponseParamsString</span><span> (paramsString: string) : </span><span>TaskResult</span><span> {<br/></span><span> // #key=value&key2=value2 形式の文字列を URLSearchParams にする<br/></span><span> const </span><span>usp</span><span> = new </span><span>URLSearchParams</span><span>(<br/></span><span> paramsString.</span><span>replace</span><span>(/^#/, '')) as any<br/></span><span> // URLSearchParams を辞書型(マップ型)変数に変換<br/></span><span> const </span><span>paramsDict</span><span> = [...</span><span>usp</span><span>.</span><span>entries</span><span>()].</span><span>reduce</span><span>(<br/></span><span> (dict, e) => ({ ...dict, [e[0]]: e[1] }), {})<br/></span><span> // localStorage に保存されている state と一致しているか検証<br/></span><span> if (</span><span>paramsDict</span><span>.</span><span>state</span><span> !== </span><span>window</span><span>.</span><span>localStorage</span><span>.</span><span>getItem</span><span>(<br/></span><span> </span><span>LOCAL_STORAGE_AUTH_STATE_KEY</span><span>)) {<br/></span><span> return {<br/></span><span> </span><span>success</span><span>: false,<br/></span><span> </span><span>message</span><span>: 'stateが一致していません'<br/></span><span> }<br/></span><span> }<br/></span><span> // state はもう使わないので消す<br/></span><span> </span><span>window</span><span>.</span><span>localStorage</span><span>.</span><span>removeItem</span><span>(</span><span>LOCAL_STORAGE_AUTH_STATE_KEY</span><span>)<br/></span><span> // アクセストークンがあるか検証<br/></span><span> if (!</span><span>paramsDict</span><span>.access_token) {<br/></span><span> return {<br/></span><span> </span><span>success</span><span>: false,<br/></span><span> </span><span>message</span><span>: 'アクセストークンが取得できません'<br/></span><span> }<br/></span><span> }<br/></span><span> // アクセストークンがあったので変数に保存する。認証成功。<br/></span><span> this.</span><span>setAccessToken</span><span>((</span><span>paramsDict</span><span>.access_token))<br/></span><span> return {<br/></span><span> </span><span>success</span><span>: true,<br/></span><span> </span><span>message</span><span>: ''<br/></span><span> }<br/></span><span> }<br/></span><span>}</span><br/><br/></pre>
<h4><span>pages/index.vue</span></h4>
<p><span>ユーザーが最初に表示するページ</span></p>
<pre><span><template><br/></span><span> <div><br/></span><span> <header class="d-flex text-white p-2"><br/></span><span> <div class="flex-grow-1 py-1"><br/></span><span> Developer Dashboard<br/></span><span> </div></span><span><br/></span><span> </header><br/></span><span> <div v-if="</span><span>authorized</span><span>"><br/></span><span> <div class="container-fluid"><br/></span><span> <div class="row"><br/></span><span> <div<br/></span><span> v-for="</span><span>account</span><span> in </span><span>googleAnalyticsAccounts</span><span>"<br/></span><span> :key="</span><span>account</span><span>.</span><span>id</span><span>"<br/></span><span> class="col-6 col-md-4 col-xl-3 my-3"<br/></span><span> ><br/></span><span> <RealtimePanel<br/></span><span> :analytics-id="</span><span>account</span><span>.</span><span>id</span><span>"<br/></span><span> :title="</span><span>account</span><span>.</span><span>title</span><span>"<br/></span><span> /><br/></span><span> </div><br/></span><span> </div><br/></span><span> </div><br/></span><span> </div><br/></span><span> </div><br/></span><span></template><br/></span><span><br/></span><span><script lang="ts"><br/></span><span>import { Component, Vue } from 'nuxt-property-decorator'<br/></span><span>import { authStore } from '~/store'<br/></span><span>import RealtimePanel from '~/components/RealtimePanel.vue'<br/></span><span>import { GOOGLE_ANALYTICS_ACCOUNTS } from '~/settings'<br/></span><span>@Component({<br/></span><span> </span><span>components</span><span>: {<br/></span><span> RealtimePanel<br/></span><span> }<br/></span><span>})<br/></span><span>export default class </span><span>Index</span><span> extends Vue {<br/></span><span> get </span><span>accessToken</span><span> () {<br/></span><span> return authStore.</span><span>accessToken<br/></span><span> }<br/></span><span><br/></span><span> get </span><span>authorized</span><span> () {<br/></span><span> return authStore.authorized<br/></span><span> }<br/></span><span><br/></span><span> get </span><span>googleAnalyticsAccounts</span><span> () {<br/></span><span> return GOOGLE_ANALYTICS_ACCOUNTS<br/></span><span> }<br/></span><span><br/></span><span> </span><span>requestToken</span><span> () {<br/></span><span> authStore.navigateTokenRequestUrl()<br/></span><span> }<br/></span><span><br/></span><span> </span><span>mounted</span><span> () {<br/></span><span> // 認証済みでなければトークン取得URLへ遷移<br/></span><span> if (!authStore.authorized) {<br/></span><span> this.</span><span>requestToken</span><span>()<br/></span><span> }<br/></span><span> }<br/></span><span>}<br/></span><span></script></span></pre>
<h4><span>pages/code/index.vue</span></h4>
<p><span>OAuth API 認証後のリダイレクトURL(コールバックURL)</span></p>
<pre><span><!--<br/></span><span>OAuth2 認証後にリダイレクトされるURL<br/></span><span>URLに含まれるハッシュ(フラグメント)から、アクセストークンを取得して変数に格納する。<br/></span><span>--><br/></span><span><template><br/></span><span> <div><br/></span><span> code received<br/></span><span> </div><br/></span><span></template><br/></span><span><br/></span><span><script lang="ts"><br/></span><span>import { Component, Vue } from 'nuxt-property-decorator'<br/></span><span>import { authStore } from '~/store'<br/></span><span>@Component({<br/></span><span> </span><span>components</span><span>: {<br/></span><span> }<br/></span><span>})<br/></span><span>export default class </span><span>Index</span><span> extends Vue {<br/></span><span> </span><span>getRequestToken</span><span> () {<br/></span><span> authStore.navigateTokenRequestUrl()<br/></span><span> }<br/></span><span><br/></span><span> async </span><span>mounted</span><span> () {<br/></span><span> const </span><span>taskResult</span><span> = await authStore.parseResponseParamsString(<br/></span><span> </span><span>window</span><span>.</span><span>location</span><span>.</span><span>hash</span><span>.</span><span>replace</span><span>(/^#/, ''))<br/></span><span> if (!</span><span>taskResult</span><span>.</span><span>success</span><span>) {<br/></span><span> throw new </span><span>Error</span><span>(</span><span>taskResult</span><span>.</span><span>message</span><span>)<br/></span><span> }<br/></span><span> this.$router.</span><span>push</span><span>('/')<br/></span><span> }<br/></span><span>}<br/></span><span></script></span></pre>
<h4><span>components/RealtimePanel.vue</span></h4>
<pre><span><!--<br/></span><span>GAのアカウント1つに対応<br/></span><span>一定時間ごとに、リアルタイムユーザー数を更新し続けるコンポーネント<br/></span><span>--><br/></span><span><template><br/></span><span> <div class="card"><br/></span><span> <div class="card-header h2 py-3 text-truncate"><br/></span><span> {{ title }}<br/></span><span> </div><br/></span><span> <div class="card-body"><br/></span><span> <div v-if="</span><span>errorMessage</span><span>" class="my-4"><br/></span><span> {{ errorMessage }}<br/></span><span> </div><br/></span><span> <div v-if="</span><span>responseSuccess</span><span>" class="text-center my-4"><br/></span><span> <div class="display-1 fw-bold"><br/></span><span> {{ activeUsers|addComma }}<br/></span><span> </div><br/></span><span> <div class="text-muted small"><br/></span><span> Active User<br/></span><span> </div><br/></span><span> </div><br/></span><span> </div><br/></span><span> </div><br/></span><span></template><br/></span><span><script lang="ts"><br/></span><span>import { Component, Vue, Prop } from 'nuxt-property-decorator'<br/></span><span>import { authStore } from '~/store'<br/></span><span>import { GOOGLE_API } from '~/settings'<br/></span><span><br/></span><span>@Component({<br/></span><span> </span><span>components</span><span>: {<br/></span><span> }<br/></span><span>})<br/></span><span>export default class extends Vue {<br/></span><span> </span><span>activeUsers</span><span>: number | null = 0<br/></span><span> </span><span>responseSuccess</span><span>: boolean = false<br/></span><span> </span><span>errorMessage</span><span>: string = ''<br/></span><span><br/></span><span> @Prop({<br/></span><span> </span><span>type</span><span>: </span><span>String</span><span>,<br/></span><span> </span><span>required</span><span>: true<br/></span><span> })<br/></span><span> </span><span>title</span><span>!: string<br/></span><span><br/></span><span> @Prop({<br/></span><span> </span><span>type</span><span>: </span><span>String</span><span>,<br/></span><span> </span><span>required</span><span>: true<br/></span><span> })<br/></span><span> </span><span>analyticsId</span><span>!: string<br/></span><span><br/></span><span> </span><span>mounted</span><span> () {<br/></span><span> this.</span><span>reloadPolling</span><span>()<br/></span><span> }<br/></span><span><br/></span><span> </span><span>reloadPolling</span><span> () {<br/></span><span> this.</span><span>reload</span><span>()<br/></span><span> setTimeout(() => {<br/></span><span> this.</span><span>reloadPolling</span><span>()<br/></span><span> }, 20000)<br/></span><span> }<br/></span><span><br/></span><span> async </span><span>reload</span><span> () {<br/></span><span> const </span><span>response</span><span> = await this.$axios.</span><span>get</span><span>(<br/></span><span> GOOGLE_API.analyticsAPIEndpoint, {<br/></span><span> </span><span>params</span><span>: {<br/></span><span> </span><span>key</span><span>: GOOGLE_API.apyKey,<br/></span><span> </span><span>ids</span><span>: `ga:${this.</span><span>analyticsId</span><span>}`,<br/></span><span> </span><span>metrics</span><span>: 'rt:activeUsers'<br/></span><span> },<br/></span><span> </span><span>headers</span><span>: {<br/></span><span> </span><span>Accept</span><span>: 'application/json',<br/></span><span> </span><span>Authorization</span><span>: `Bearer ${authStore.</span><span>accessToken</span><span>}`<br/></span><span> },<br/></span><span> </span><span>validateStatus</span><span>: _ => true<br/></span><span> }<br/></span><span> )<br/></span><span> // トークン期限切れ<br/></span><span> if (</span><span>response</span><span>.</span><span>status</span><span> === 401) {<br/></span><span> authStore.navigateTokenRequestUrl()<br/></span><span> return<br/></span><span> }<br/></span><span> // 403: アクセス過多など<br/></span><span> if (</span><span>response</span><span>.</span><span>status</span><span> !== 200) {<br/></span><span> this.</span><span>responseSuccess</span><span> = false<br/></span><span> this.</span><span>errorMessage</span><span> = `ERROR: ${</span><span>response</span><span>.</span><span>status</span><span>}`<br/></span><span> return<br/></span><span> }<br/></span><span> if (</span><span>response</span><span>.</span><span>data</span><span>) {<br/></span><span> this.</span><span>activeUsers</span><span> = parseInt(</span><span>response</span><span>.</span><span>data</span><span>.totalsForAllResults['rt:activeUsers'])<br/></span><span> this.</span><span>responseSuccess</span><span> = true<br/></span><span> this.</span><span>errorMessage</span><span> = ''<br/></span><span> }<br/></span><span> }<br/></span><span>}<br/></span><span></script></span></pre>Djangoプロジェクト間を OAuth2 連携する2016-06-17T05:54:05+00:002024-03-28T02:48:41+00:00四柳剛https://tech.torico-corp.com/blog/author/yotsuyanagi/https://tech.torico-corp.com/blog/django-projects-oauth2/<p></p>
<p>ローカル環境に Django プロジェクトを2つ作り、OAuth2で(ダミーの)プロフィール情報を取得するまで書きます。</p>
<p>2つの Django プロジェクトを作ります。test_provider, test_consumer です。</p>
<p>OAuth2の認証フローは、サーバ間通信のため「Authorization Code」形式で行います。</p>
<p>OAuth2についての情報は <a href="https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/709.html" target="_blank">IPAのページ</a> などにあります</p>
<p><a href="https://github.com/ytyng/django-test-oauth" target="_blank">ソースコードを github に上げました。</a></p>
<h1>Django プロジェクト 概要</h1>
<h2>test_provider</h2>
<p>起動ポート 8000<br/>ユーザーのIDとハッシュ化パスワードを保持します。<br/>OAuth2 プロバイダを提供します。<br/>ユーザーに test_provider への情報提供の認可を問います。<br/><strong>django-oauth-toolkit</strong> を使います</p>
<h2>test_consumer</h2>
<p>起動ポート 8001<br/>ユーザーが最初にアクセスする Webページ<br/>ユーザーに test_provider への認可ページへ誘導します。<br/>ユーザー認可後は、このプログラムが test_provider へリクエストを行い、情報をもらいます。<br/><strong>django-allauth</strong> を使います。</p>
<p>※なお、django-allauth と同じくらい使いやすいのが <strong>python-social-auth</strong> です。Django のライブラリも含んでいます。</p>
<p>django-social-auth というライブラリも見かけると思いますが、Python3に対応していないのでやめておきましょう。</p>
<p></p>
<h1>OAuth2プロバイダの開発</h1>
<p><a href="http://django-oauth-toolkit.readthedocs.io/en/latest/tutorial/tutorial_01.html" target="_blank">django-oauth-toolkit の素晴らしいドキュメントがここにあります。</a><br/>「Make a Provider in a Minute」という挑発的なタイトルですが、<br/>認証モデルにこだわりがなければほんとにすぐ作れます。<br/>がんばっても1分では作れないと思いますが…。</p>
<h2>Django プロジェクトの作成</h2>
<pre>$ mkdir test-oauth<br/>$ cd test-oauth<br/><br/>$ django-admin.py startproject test_provider<br/>$ pip install django-oauth-toolkit django-cors-headers<br/><br/>$ cd test_provider</pre>
<h2>test_provider/settings.py の編集</h2>
<h4>INSTALLED_APPS に追加</h4>
<pre> 'oauth2_provider',<br/> 'corsheaders',</pre>
<h4>MIDDLEWARE_CLASSES に追加</h4>
<pre>'corsheaders.middleware.CorsMiddleware',</pre>
<h4>末尾に追加</h4>
<pre>CORS_ORIGIN_ALLOW_ALL = True</pre>
<h2>test_provider/urls.py の編集</h2>
<pre>from django.conf.urls import url, include<br/>from django.contrib import admin<br/>from .views import profile_view<br/><br/>urlpatterns = [<br/> url(r'^admin/', admin.site.urls),<br/> url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')),<br/> url(r'^api/profile/$', profile_view, name='profile'),<br/> url(r'^accounts/login/', 'django.contrib.auth.views.login', name='login',<br/> kwargs={'template_name': 'admin/login.html'}),<br/>]</pre>
<h2>test_provider/views.py の作成</h2>
<pre>from django.http import JsonResponse<br/>from oauth2_provider.views.generic import ProtectedResourceView<br/><br/>class ProfileView(ProtectedResourceView):<br/> def get(self, request, **kwargs):<br/> user = request.resource_owner<br/><br/> return JsonResponse({<br/> 'user_id': user.id,<br/> 'email': user.email,<br/> 'date_joined': user.date_joined,<br/> 'secret_message': 'The quick brown fox',<br/> })<br/><br/>profile_view = ProfileView.as_view()</pre>
<h2>DB に反映</h2>
<pre>$ ./manage.py migrate</pre>
<h2>Adminユーザーの作成</h2>
<p>OAuthプロバイダを追加設定するためには、Adminユーザーが必要なため作っておきます。</p>
<pre>$ ./manage.py createsuperuser</pre>
<h2>テストサーバの起動</h2>
<pre>$ ./manage.py runserver 8000</pre>
<h2>OAuthプロバイダの設定</h2>
<p><span>プロバイダの設定ページにアクセスするために、一度 Admin サイトにログインしておきます。</span></p>
<p><a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a></p>
<p><img alt="" height="333" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_11.36.32.png" width="439"/></p>
<p></p>
<p>Admin サイトにログイン後、アプリ追加ページを開きます。</p>
<p><a>http://127.0.0.1:8000/o/applications/</a></p>
<p><img alt="" height="180" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_10.40.41.png" width="590"/></p>
<p>Click here をクリックします。</p>
<p>Register a new application ページとなるので、<br/><br/>Name: test-consumer (何でも良い)<br/>Client type: Confidential<br/>Authorization grant type: Authorization code<br/>Redirect urls:<br/>http://localhost:8001/accounts/testprovider/login/callback/<br/>http://127.0.0.1:8001/accounts/testprovider/login/callback/</p>
<p><img alt="" height="698" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-05-24_21.05.49.png" width="582"/></p>
<p></p>
<p>このように入力し、saveします。</p>
<p></p>
<p><a href="http://django-oauth-toolkit.herokuapp.com/consumer/">作成した OAuthプロバイダは、このツールで簡単なテストができます。</a><br/><br/>client_id に先ほどの client_id をコピペし、<br/>Authorization url は http://localhost:8000/o/authorize/ を入れればなんとなくのテストができます。</p>
<h2>テストユーザーの作成</h2>
<p>Admin から、テストユーザーを作っておきます。</p>
<p><a href="http://127.0.0.1:8000/admin/auth/user/">http://127.0.0.1:8000/admin/auth/user/</a></p>
<p>ADD USER をクリックし、適当なユーザーを作っておいてください。<br/>OAuth2 でのログイン時、このユーザーID/パスワードでログインを行うことになります。</p>
<p><img alt="" height="373" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_11.41.29.png" width="644"/></p>
<p></p>
<p>引き続き、OAuthコンシューマの開発を行います。</p>
<p>起動中の Django テストサーバは、そのまま起動させっぱなしにしておいてください。</p>
<p></p>
<h1>OAuth2コンシューマテストアプリの作成</h1>
<h2>Django プロジェクトの作成</h2>
<pre>$ cd test-oauth<br/><br/>$ django-admin.py startproject test_consumer<br/>$ pip install django-allauth<br/><br/>$ cd test_consumer</pre>
<p>ここでは、OAuth クライアントライブラリ django-allauth をインストールしています。</p>
<p>django-allauth は多彩はクライアント接続に対応した、多機能なライブラリで、django1.9 + Python 3.5 でも動きますので使い勝手が良いです。</p>
<p><a href="https://tech.torico-corp.com/blog/category/oauth2/feeds/atom/%20http:/django-allauth.readthedocs.io/en/latest/overview.html" target="_blank">django-allauth のドキュメントはここ</a></p>
<p></p>
<h2>設定</h2>
<p>http://django-allauth.readthedocs.io/en/latest/installation.html</p>
<h3>test_consumer/settings.py</h3>
<h4>TEMPLATES の OPTIONS の context_processors に追加</h4>
<pre>'django.template.context_processors.request',</pre>
<p>Django 1.9 なら、デフォルトで入ってると思います。</p>
<h4>INSTALLED_APPS に追加</h4>
<pre> 'django.contrib.sites',<br/> 'test_consumer',<br/> 'allauth',<br/> 'allauth.account',<br/> 'allauth.socialaccount',<br/> 'allauth.socialaccount.providers.google',<br/> 'allauth.socialaccount.providers.twitter',<br/> 'testprovider', # これから作る</pre>
<p>allauth.socialaccount.providers.* は任意に選んで追加してください。<br/>テストのために google と twitter を追加しています。</p>
<p>どんなプロバイダが準備されているかは公式ドキュメントに書いてあります。</p>
<p>http://django-allauth.readthedocs.io/en/latest/installation.html</p>
<h4>末尾に追加</h4>
<pre>AUTHENTICATION_BACKENDS = (<br/> 'django.contrib.auth.backends.ModelBackend',<br/> 'allauth.account.auth_backends.AuthenticationBackend',<br/>)<br/><br/>SITE_ID = 1<br/>SESSION_COOKIE_NAME = 'test-consumer-session-id'</pre>
<h2>test_consumer/urls.py</h2>
<pre>from django.conf.urls import url, include<br/>from django.contrib import admin<br/>from django.views.generic import TemplateView<br/><br/>urlpatterns = [<br/> url(r'^admin/', admin.site.urls),<br/><br/> url(r'^$', TemplateView.as_view(template_name='index.html')),<br/> url(r'^accounts/', include('allauth.urls')),<br/><br/>]</pre>
<h2>テンプレートの作成</h2>
<p>$ mkdir test_consumer/templates<br/>test_consumer/templates/index.html を作成</p>
<pre>{% load socialaccount %}<br/>{% load account %}<br/><br/><html><br/><head><br/><meta charset="utf-8" /><br/></head><br/><body><br/><br/><a href="{% provider_login_url "twitter" %}">Twitterでログイン</a><br /><br/><a href="{% provider_login_url "google" %}">Googleでログイン</a><br /><br/><a href="{% provider_login_url "testprovider" %}">Test Provider でログイン</a><br /><br/><br/><hr /><br/>{% if user.is_authenticated %}<br/> ログインユーザー: {% user_display user %}<br /><br/> {% for sa in user.socialaccount_set.all %}<br/> {{ sa.extra_data }}<br /><br/> {% endfor %}<br/>{% endif %}<br/></body><br/></html></pre>
<h2>testprovider アダプタの作成</h2>
<p>allauth に入っている、google のアダプタを改修して作るのが最も手軽でしょう。</p>
<p>allauth/socialaccount/providers/google このディレクトリをまるごとコピーし、<br/>testprovider としてペーストします。 ( manage.py がいるディレクトリに)</p>
<p>内容は以下のようになります。</p>
<h3>testprovider/provider.py</h3>
<pre>from allauth.socialaccount import providers<br/>from allauth.socialaccount.providers.base import ProviderAccount<br/>from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider<br/><br/>class TestAccount(ProviderAccount):<br/><br/> def to_str(self):<br/> dflt = super(TestAccount, self).to_str()<br/> return self.account.extra_data.get('name', dflt)<br/><br/>class TestProvider(OAuth2Provider):<br/> id = 'testprovider'<br/> name = 'Test Provider'<br/> account_class = TestAccount<br/><br/> def get_default_scope(self):<br/> return ['read', 'write']<br/><br/> def get_site(self):<br/> settings = self.get_settings()<br/> return settings.get('SITE', 'testprovider')<br/><br/> def extract_uid(self, data):<br/> uid = str(data['user_id'])<br/> return uid<br/><br/> def extract_common_fields(self, data):<br/> return dict(username=data.get('email', 'no name'))<br/><br/>providers.registry.register(TestProvider)</pre>
<h2>testprovider/urls.py</h2>
<pre>from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns<br/>from .provider import TestProvider<br/><br/>urlpatterns = default_urlpatterns(TestProvider)<br/><br/>testprovider/views.py<br/><br/>import requests<br/><br/>from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,<br/> OAuth2LoginView,<br/> OAuth2CallbackView)<br/>from allauth.socialaccount.providers import registry<br/><br/>from .provider import TestProvider<br/><br/>from django.conf import settings<br/><br/>server_url_prefix = getattr(<br/> settings, 'TEST_PROVIDER_URL_PREFIX',<br/> 'http://127.0.0.1:8000')<br/><br/>class TestOAuth2Adapter(OAuth2Adapter):<br/> provider_id = TestProvider.id<br/> access_token_url = server_url_prefix + '/o/token/'<br/> authorize_url = server_url_prefix + '/o/authorize/'<br/> profile_url = server_url_prefix + '/api/profile/'<br/><br/> def complete_login(self, request, app, token, **kwargs):<br/> provider = registry.by_id(app.provider)<br/> resp = requests.get(self.profile_url,<br/> params={'access_token': token.token})<br/><br/> extra_data = resp.json()<br/> return self.get_provider().sociallogin_from_response(<br/> request, extra_data)<br/><br/>oauth2_login = OAuth2LoginView.adapter_view(TestOAuth2Adapter)<br/>oauth2_callback = OAuth2CallbackView.adapter_view(TestOAuth2Adapter)</pre>
<h2>DBのマイグレーション</h2>
<pre>$ ./manage.py migrate</pre>
<h2>Adminユーザーの作成</h2>
<pre>$ ./manage.py createsuperuser</pre>
<h2>テストサーバの起動</h2>
<pre>$ ./manage.py runserver 8001</pre>
<h2>アプリケーションの登録</h2>
<pre>http://127.0.0.1:8001/admin/</pre>
<p>Admin サイトにログイン後、SOCIAL ACCOUNTS の Social applications の +Add をクリックします。</p>
<p><img alt="" height="167" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_11.25.38.png" width="653"/></p>
<p>Provider: Test Provider<br/>Name: Test Provider<br/>Client id: 先ほどの 8000 admin で作った Client id<br/>Secret key: 先ほどの 8000 admin で作った Secret key<br/>Key: 空<br/>Sites: example.com を選択</p>
<p>これで、Save します。</p>
<p>Google, Twitter など他サービスへのログインも必要であれば、ここから追加できます。</p>
<h2>クライアントとしての動作テスト</h2>
<p>先ほど、8000ポートにログインしたブラウザとは別のブラウザを使います。(ログアウト状態になっているため)</p>
<p><a href="http://127.0.0.1:8001/">http://127.0.0.1:8001/</a></p>
<p><img alt="" height="123" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_15.24.08.png" width="242"/></p>
<p>ここで、「Test Provider でログイン」をクリックします。</p>
<p>( AllAuth が自動的にログインフォームを提供してますので、このURLからでもいけます。/accounts/login/ )</p>
<p>そうすると、8000 ポートへ転送され、ログインフォームが出てきます。<br/>今回は時間節約のため、Django Admin のログインフォームを流用していますが、<br/>通常は自作のログインフォームをここで表示させることになると思います。</p>
<p><img alt="" height="333" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_11.36.32.png" width="439"/></p>
<p>8000 (test_provider) で作ったテストユーザーでログインしてみましょう。</p>
<p><img alt="" height="316" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_12.27.10.png" width="629"/></p>
<p>ログインが成功すると、認可を求めるページとなりますので、「Authorize」をクリック。<br/>ちなみに、この認可を求めるページは、8000 (test_provider) の Admin の アプリ設定の、「Skip authorization」にチェックを入れると省略できます。</p>
<p><img alt="" height="173" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_12.28.26.png" width="458"/></p>
<p></p>
<p>ログインに成功すると、8001 ポートに戻ってきます。</p>
<p>http://127.0.0.1:8001/accounts/profile/<br/>が表示されますが、このページはまだ作っていませんので Not found になります。</p>
<p><img alt="" height="290" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_13.00.30.png" width="535"/></p>
<p></p>
<p>情報取得は成功していますので、トップページのURLを手で入力して情報を見てみます。</p>
<p>http://127.0.0.1:8001/</p>
<p>トップページ下部に、API取得した extra_data を表示する箇所があります。<br/>そこに、OAuth2 のプロバイダから取得した情報が表示されるのが確認できます。</p>
<p><img alt="" height="159" src="https://tech.torico-corp.com/media/uploads/site-6/oauth/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2016-06-17_13.57.31.png" width="819"/></p>
<p>OAuthプロバイダの /api/profile/ から取得した 'secret_message': 'The quick brown fox' が、OAuthコンシューマで取得できているのが確認できます。</p>
<p><a href="https://github.com/ytyng/django-test-oauth" target="_blank">ソースコードは Github に上げました。</a></p>
<p></p>