0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

05.認証の実装 - 画像変換サービス開発3

Posted at

目次

  • 【実装】プロジェクトの作成
  • 【実装】メインページの作成
  • 【実装】ログイン・ログアウトの機能の作成
  • 【実装】HTML にログイン・ログアウトの組み込み

【実装】プロジェクトの作成

既にここまでで、画像変換サービス mojipic を開発するための、技術的な知識やツールなどは一通り触れたことになる。

そのためここからは、Play Framework と様々なライブラリを駆使して、Web アプリケーションを構築していく。
まずはこの回では、プロジェクトの作成と HTML のクライアントの実装、認証機能までを実装していく。

では、最初に Play Framework のプロジェクトを作成する。

$ sbt new playframework/play-scala-seed.g8
[info] welcome to sbt 1.5.4 (Amazon.com Inc. Java 1.8.0_292)
[info] loading global plugins from /Users/glaciermelt/.sbt/1.0/plugins
[info] set current project to new (in build file:/private/var/folders/k0/00rg14p52mnbb2521dx5ynsc0000gp/T/sbt_9c5a51f6/new/)

This template generates a Play Scala project 

name [play-scala-seed]: mojipic
organization [com.example]: jp.ed.nnn

Template applied in /Users/glaciermelt/environment/workspace/./mojipic

以上のコマンドで、Play Frameworkのプロジェクトを Gitter8 のテンプレートから作成する。

cd mojipic
sbt run

以上で最初のコンパイルと必要なライブラリのダウンロードが開始する。
以下のように表示されたら、表示された URL にアクセスして、メインページが表示されるかを確認していく。

--- (Running the application, auto-reloading is enabled) ---

[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Enter to stop and go back to the console...)

http://localhost:9000/にアクセスすると、"Welcome to Play!" と書かれたページが表示されるはず。
このプロジェクトの状態から mojipicを作成していく。

【実装】メインページの作成

まずは、モックアップとして作ったワイヤーフレームを確認しながら、トップページの HTML ページを作っていく。

Play Framework は、Twirl というテンプレートエンジンをデフォルトで利用できる。
以下は、リンク先のページにあるサンプルコード。

@(customer: Customer, orders: List[Order])

<h1>Welcome @customer.name!</h1>

<ul>
@for(order <- orders) {
  <li>@order.title</li>
}
</ul>

このサンプルコードがほぼ全てを表しているが、まずテンプレートの最初で @() によって Scala の関数のようにテンプレートで利用する引数を定義できる。
@ 記号を用いてコードの呼び出しができる他、@for などを利用して、ループを記述できる。

Twirl は Scala のコードが書け、コンパイルされるため、型安全に HTML のテンプレートを記述できる。
ただし、 Scala のコードがかけすぎてしまう側面もあるため、ビジネスロジックがテンプレートに流出してしまっていないかどうかは注意が必要。

Twirl はテンプレートエンジンが本来持つべきほとんどの機能を持ち合わせている。
ここでは、認証時の情報を渡して、情報を出し分けする用途程度にしかこのテンプレートエンジンを利用せずに進めていく。
2017年現在、クライアントの種類がスマートフォンデバイスを含めて多岐に渡るため、Web API で多くのh情報を取得できるようにする実装方法が中心的になりつつある。

それでは、mojipic フォルダを IntelliJ Idea で SBT プロジェクトとしてインポートして開いてみる。

IntelliJ IDEA の Import project のメニューから先ほど作成した mojipic フォルダを開き、SBT プロジェクトとしてインポートする。
sbt のダウンロードのタスクが動き始めるので、ダウンロードとビルドが完了するまで待とう。
無事ビルドが完了すると、プロジェクトツールウィンドウが表示される。

メインページを実装する上でまず編集するべきは、

  • app/views/index.scala.html
  • app/views/main.scala.html

以上2つの実装となる。
中身を確認してみよう。
なお、

  • main.scala.htmlは全ての HTML で共通化できる body タグの外側が
  • index.scala.htmlは HomeController.index メソッドで利用される、ルートにアクセスした際の body タグの内側が

実装されている。

app/views/main.scala.html

@*
 * This template is called from the `index` template. This template
 * handles the rendering of the page header and body tags. It takes
 * two arguments, a `String` for the title of the page and an `Html`
 * object to insert into the body of the page.
 *@
@(title: String)(content: Html)

<!DOCTYPE html>
<html lang="en">
    <head>
        @* Here's where we render the page title `String`. *@
        <title>@title</title>
        <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">

    </head>
    <body>
        @* And here's where we render the `Html` object containing
         * the page content. *@
        @content

      <script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
    </body>
</html>
app/views/index.scala.html

@()

@main("Welcome to Play") {
  <h1>Welcome to Play!</h1>
}

これらを利用して、ここに HTML で作ったクライアントを実装していく。

ここでは、Webpack を利用して CSS フレームワークを導入していく。
今回導入するクライアントの構成は、

  • Webpack
  • Babel
  • Materialize
  • React
  • Dropzon.js

以上を基本として導入していく。
そして、この対ミグうで予め利用する可能性のある JS のライブラリ群も一気に導入する。

まずは Webpack だが、モジュール化された JavaScript のライブラリを利用可能にし、1つの JavaScript にまとめてくれる。

そして今回は、Babel も利用する。
この Babel はトランスパイラといい、ES6 以上で書いた JavaScript を ES5 に変換し、ES6 に対応していないブラウザでも利用できるようにしてくれるツール。
具体的には、Webpack のプラグインとして利用する。
実際に多くのユーザーへ JavaScript のクライアントを提供する場合は、ほぼこのツールが必須となる。

そして、CSS フレームワークとしては Materialize を利用する。
HTML で Android などで導入されているマテリアルデザインを利用できるという CSS フレームワークとなる。
利用方法は、CSS と JS を読み込んだ後、対応する CSS の class を適用するだけでデザインが適用される。
Bootstrap などとほとんど同じ使い方。

React は、DOM の UI を今p年とかするためのライブラリ。
今回は、写真の一覧を更新するためのコンポーネントを React で作成する。

そして、Dropzone.js はドラッグ&ドロップで写真の画像をサーバーに送るためのライブラリ。
これもファイルのドラッグ&ドロップでは非常によく利用されるライブラリ。

以上、一気にこれらのライブラリをクライアントに導入する。

まずは、Node.js を自分のPCにインストールする。
Mac の場合は、Nodebrew を利用したインストールがおすすめ。
Node.js の10.14.2がインストールされていることを前提とする。
どうしても 10.14.2 がインストールできない際は、それ以降のバージョンがインストールされていれば良い。
多くの場合動作する。

$ node --version
v14.4.0

と表示されれば問題ない。

これ以降は mojipic のルートディレクトリで作業していく。
まずは Node.js のプロジェクトを作成する。

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (mojipic) 
version: (1.0.0) 
description: Web front client for mojipic
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/glaciermelt/environment/workspace/mojipic/package.json:

{
  "name": "mojipic",
  "version": "1.0.0",
  "description": "Web front client for mojipic",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes) yes

以上のような構成でプロジェクトを作成する。
このタイミングで、.gitignoreに、

node_modules

を追記しておく。
次は Webpack 等のインストールを行う。

npm install webpack@4.26.1 webpack-cli@3.1.2 @babel/core@7.1.6 @babel/preset-react@7.0.0 @babel/preset-env@7.1.6 babel-loader@8.0.4 --save-dev
npm install jquery@3.2.1 materialize-css@0.100.2 react@16.0.0 react-dom@16.0.0 dropzone@5.1.1 --save

(依存関係を解決するため、別途npm audit fixnpm audit -> npm install ◯◯◯を実施)

非常に多いが、これで必要なツールのインストールは完了。
このように複数のツール群を組み合わせたものをツールチェインという。
以上のように、開発環境用のツールチェインとクライアントに含める依存ライブラリに分けてインストールを行えば完了。

次に、Webpack に必要なwebpack.config.jsを作成する。
conf/webpack.config.jsにファイルを作詞し、中身を以下のようにする。

const path = require('path');

module.exports = {
  entry: './app/views/index.js',
  mode: 'none',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, '../public/javascripts')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', { 'modules': false }],
                '@babel/preset-react'
              ]
            }
          }
        ],
        // node_modules は除外する
        exclude: /node_modules/,
      }
    ]
  },
  // ソースマップを有効にする
  devtool: 'source-map'
};

今回は、app/views/index.jsのファイルをpublic/javascripts/main.jsに出力するように実装した。
その他は Babel のプラグインの設定となるので、特に気にする必要はない。
この辺りの設定方法はある程度テンプレート化してしまっている。

なお、今回はトランスパイル前後でのコードの状況が分かりやすいように、コードの関係を明示するソースマップも出力するようにした。
実際にリリースする際には JavaScript は圧縮し、ソースマップも取り除いてコンパイルを行う。
しかし、ここではデバッグのためにそうしない。

次に、app/views/index.jsを実装する。

app/views/index.js

import $ from 'jquery';
import 'materialize-css';
import Dropzone from 'dropzone';
import React from 'react';
import ReactDOM from 'react-dom';

以上が、今回利用するツール郡の利用方法。
import 文は、ECMAScript Modules という機能。
Babelを使うことで利用できる。
基本的には、Node.js の

const $ = require('jquery');

のモジュールのインポートと同様に扱うことができる。
以上の実装では必要となるライブラリだけを読み込み、結合されたpublic/javascripts/main.jsを作成する。

無事、conf/webpack.config.jsができたら、以下のコマンドでコンパイルを実行する。

$ node_modules/.bin/webpack --config conf/webpack.config.js
asset main.js 1.83 MiB [emitted] (name: main) 1 related asset
runtime modules 1.06 KiB 6 modules
modules by path ./node_modules/ 1.83 MiB
  modules by path ./node_modules/scheduler/ 38 KiB
    modules by path ./node_modules/scheduler/cjs/*.js 37.6 KiB 4 modules
    modules by path ./node_modules/scheduler/*.js 412 bytes 2 modules
  modules by path ./node_modules/react/ 76.9 KiB 3 modules
  modules by path ./node_modules/react-dom/ 956 KiB
    ./node_modules/react-dom/index.js 1.33 KiB [built] [code generated]
    ./node_modules/react-dom/cjs/react-dom.production.min.js 116 KiB [built] [code generated]
    ./node_modules/react-dom/cjs/react-dom.development.js 839 KiB [built] [code generated]
  modules by path ./node_modules/prop-types/ 4 KiB
    ./node_modules/prop-types/checkPropTypes.js 3.69 KiB [built] [code generated]
    ./node_modules/prop-types/lib/ReactPropTypesSecret.js 314 bytes [built] [code generated]
./app/views/index.js 143 bytes [built] [code generated]
webpack 5.44.0 compiled successfully in 2197 ms

のように表示されれば、コンパイルは成功。
public/javascripts/main.jsにはライブラリが結合され、ES6 が ES5 にトランスパイルされた JavaScript が用意されている。

次に、これを読み込んでスタイルが変わるかどうかを確認する。
Materialize では CSS の読み込みと、viewport の設定を head 要素内で行う必要がある。
そのため、main.scala.htmlを以下のように編集する。

app/views/main.scala.html

<head>
        @* Here's where we render the page title `String`. *@
        <title>@title</title>

        <!-- Compiled and minified CSS -->
        <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css">

        <!--Let browser know website is optimized for mobile-->
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

        <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
app/views/main.scala.html

以上は CDN から Materialized の CSS をファイル取得するためのものなので、外部のリソースを読み込めるよう、conf/application.confに、

conf/application.conf

play.filters.headers.contentSecurityPolicy = null

以上を記述する。

また、Materialize の CSS のクラスを確認しながら、index.scala.htmlも以下のように編集する。

app/views/index.scala.html

  <h1 class="blue-text text-darken-2">Welcome to Play!</h1>

として、Materialized のテキストに青く色付けをする class を追加した。

なお、既にpublic/javascripts/main.jsは読み込まれた状況になっている。
そのためsbt runで起動している状態であれば、再読み込みをすること。

無事テキストが青色に変化すれば、ツールチェインの動作と、Materialized の導入が完了したことになる。

ここからは、本来ならば HTML コーディングを行って、デザインを作成する。
ただ、この大規模 Web アプリケーションを学ぶにあたり、 HTML の実装はそこまで重要ではないので、今回は作成した以下のapp/views/index.scala.htmlと、app/views/index.jsをそのまま利用していく。

app/views/index.scala.html

@()

@main("MOJIPIC - 写真に文字を書き加え") {

<nav class="light-blue lighten-4" role="navigation">
    <div class="nav-wrapper container"><a id="logo-container" href="#" class="brand-logo"><img src="@routes.Assets.versioned("images/logo.png")"></img></a>
        <ul class="right hide-on-med-and-down">
            <li>
                <a class="light-blue-text text-darken-3" href="/login">twitterでログイン</a>
                <!--<a class="light-blue-text text-darken-3" href="/logout">ログアウト</a>-->
            </li>
        </ul>

        <ul id="nav-mobile" class="side-nav">
            <li>
                <a href="/login">twitterでログイン</a>
                <!--<a href="/logout">ログアウト</a>-->
            </li>
        </ul>
        <a href="#" data-activates="nav-mobile" class="button-collapse"><i class="material-icons">menu</i></a>
    </div>
</nav>

<div class="section no-pad-bot" id="index-banner">
    <div class="container">
        <br><br>
        <h3 class="header center blue-text">画像をドラッグ&ドロップ</h3>
        <div class="row center">
            <h5 class="header col s12 light">ドロップエリアにjpg画像かpng画像をドロップしてください</h5>
        </div>
        <div class="row center">
            <div class="input-field col s12">
                <input class="validate" type="text" id="overlaytext-shown" name="overlaytext-shown" maxlength="20" value="LGTM">
                <label class="active" for="overlaytext-shown">テロップ</label>
            </div>
        </div>
        <div class="row center">
            <div class="input-field col s2">
                <input class="validate" type="text" id="overlaytextsize-shown" name="overlaytextsize-shown" maxlength="5" +value="60">
                <label class="active" for="overlaytextsize-shown">文字サイズ (pt)</label>
            </div>
        </div>
        <div class="row center">
            <div class="col s12 blue z-depth-5">
                <h2 class="blue-text text-lighten-5"> ドロップエリア </h2>
            </div>
        </div>
        <br><br>

    </div>
</div>

<div class="divider"></div>

<div class="container">
    <h3 class="header center blue-text">画像一覧</h3>
    <div id="picture-grid" class="row center">
    </div>
</div>
<footer class="page-footer light-blue lighten-4">
    </div>
    <div class="footer-copyright">
        <div class="container">
            <a class="light-blue-text text-darken-3" href="https://nnn.ed.nico">このアプリケーションはN予備校により提供されています。</a>
        </div>
    </div>
</footer>
}
app/views/index.js

import $ from 'jquery';
import 'materialize-css';
import Dropzone from 'dropzone';
import React from 'react';
import ReactDOM from 'react-dom';

// Materializedの設定
$(function () {
  // Materialized Menu
  $('.button-collapse').sideNav();
}); // end of document ready

以上を実装した後、

node_modules/.bin/webpack --config conf/webpack.config.js

で再コンパイルして、ブラウザを再読み込みしよう。
これでHTMLのフロントエンドは完成。
特に処理などは実装せず、HTML の見た目だけを用意した。

なお、レスポンシブデザイン対応がされているため、画面を小さくした際にメニューが表示されるかどうかも確認しよう。
JavaScript の更新が取り込まれていれば、レスポンシブデザインも利用できる。

また、public/imagesフォルダに配置する

  • favicon.png
  • logo.png

もそれぞれ、GitHub で配布している。
ダウンロードして配置しておこう。
これでとりあえずの見た目としてのクライアントサイドは完成した。

【実装】ログイン・ログアウトの機能の作成

次に、Twitter の OAuth 認証を使ったログイン、ログアウトの機能を実装する。
Twitter の OAuth 認証には、twitter4j とうライブラリを利用する。
Java のライブラリではあるが、もちろん Scala からも利用できる。
また今回は、セッションを保持するのに Play Framework の Cache 機能をそのまま利用する。
この Cache 機能は内部的には Ehcache というサーバーで利用できるキャッシュライブラリが利用されている。
加えて JAXB API にも依存しているので、こちらも必要。

アプリケーション・サーバーを増やしてスケールさせる場合には、この方法以外にも Redis や memchached などの他のキャッシュサーバーを利用することも可能。
キャッシュは基本的にはメモリ上にデータを置いて高速にアクセスできるデータ構造を提供する他、揮発時間を定義して、自動的に消去される仕組みを持ち合わせている。

まずはbuild.sbtを編集して、この twitter4j, Cache, JAXB API を利用できるように依存関係に追加する。

libraryDependencies += guice
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test
libraryDependencies += ehcache
libraryDependencies += "javax.xml.bind" % "jaxb-api" % "2.3.1"
libraryDependencies += "org.twitter4j" % "twitter4j-core" % "4.0.6"

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "jp.ed.nnn.controllers._"

以上、2行を足す。
Import Changesのリンクが InttelliJ IDEA のエディタの右下に現れるので、実行する。

次に、Twitter のアプリケーションの登録を終わらせる。
Twitter Apps にアクセスして Create App を選択し、

Name: ${重複していなければなんでも良い}
Description: ${説明文、例: Web front client for mojipic}
Website: ${例: http://example.com}
Callback URL: http://localhost:9000/oauth_callback

上記のようにアプリケーションを登録して作る。
その後、Keys and Access Tokens タブより

  • Consumer Key (API Key)
  • Consumer Secret (API Secret)

を取得しよう。
後でプログラム上から取得できるように、conf/application.confに記載する。

conf/application.conf

# https://www.playframework.com/documentation/latest/Configuration
play.filters.headers.contentSecurityPolicy = null

# Twitter settings
mojipic.documentrooturl="http://localhost:9000"
mojipic.consumerkey="LAdrAbR8XXXXXXXXXXXXXX"
mojipic.consumersecret="WVOBXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

ここでは自分で作成した Consumer Key と Consumer Secret を利用するように。
なお、documentrooturlは、後で OAuth のコールバックに利用する。

そしてまずは twitter4j のコードサンプルを見ながら、TwitterAuthenticatorを実装していく。
こちらで認証を実行してセッションキーから Twitter のアクセストークンを Cache 上に保存でき、セッション ID から Twitter のアクセストークンを取得できる。

appフォルダ以下にinfrastructure.twitterのパッケージを作成し、app/infrastructure.twitter/TwitterAuthenticator.scalaを IDEA の class を作成する機能より作成し、以下のように実装する。

app/infrastructure.twitter/TwitterAuthenticator.scala

package infrastructure.twitter

import javax.inject.Inject

import play.api.Configuration
import play.api.cache.SyncCacheApi
import twitter4j.{Twitter, TwitterFactory}
import twitter4j.auth.AccessToken

import scala.concurrent.duration._
import scala.util.control.NonFatal

class TwitterAuthenticator @Inject() (
                                       configuration: Configuration,
                                       cache: SyncCacheApi
                                     ) {

  val CacheKeyPrefixTwitter = "twitterInstance"

  val ConsumerKey = configuration.get[String]("mojipic.consumerkey")
  val ConsumerSecret = configuration.get[String]("mojipic.consumersecret")

  private[this] def cacheKeyTwitter(sessionId: String): String = CacheKeyPrefixTwitter + sessionId

  /**
    * Twitterの認証を開始する
    * @param sessionId Twitterの認証をしたいセッションID
    * @param callbackUrl コールバックURL
    * @return 投稿者に認証してもらうためのURL
    * @throws TwitterException 何らかの理由でTwitterの認証を開始できなかった
    */
  def startAuthentication(sessionId: String, callbackUrl: String): String =
    try {
      val twitter = new TwitterFactory().getInstance()
      twitter.setOAuthConsumer(
        ConsumerKey,
        ConsumerSecret
      )
      val requestToken = twitter.getOAuthRequestToken(callbackUrl)
      cache.set(cacheKeyTwitter(sessionId), twitter, 30.seconds)
      requestToken.getAuthenticationURL
    } catch {
      case NonFatal(e) =>
        throw TwitterException(s"Could not get a request token. SessionId: $sessionId", e)
    }

  /**
    * Twitterのアクセストークンを取得する
    * @param sessionId Twitterの認証をしたいセッションID
    * @param verifier OAuth Verifier
    * @return アクセストークン
    * @throws TwitterException 何らかの理由でTwitterのアクセストークンを取得できなかった
    */
  def getAccessToken(sessionId: String, verifier: String): AccessToken =
    try {
      cache.get[Twitter](cacheKeyTwitter(sessionId)).get.getOAuthAccessToken(verifier)
    } catch {
      case NonFatal(e) =>
        throw TwitterException(s"Could not get an access token. SessionId: $sessionId", e)
    }
}

case class TwitterException(message: String = null, cause: Throwable = null)
  extends RuntimeException(message, cause)

以上のように実装できる。

@Inject()は Play Framework における DI の取得方法。
DI は依存性注入 (Dependency Injection) という手法で、インスタンス管理を行いながら、そのインスタンスに必要となる別なインスタンスを自動的に組み合わせてくれるという仕組み。

Play Framework では設定やキャッシュに必要なインスタンス情報は、@Inject()をつけたクラスを宣言し、必要な方をコンストラクタ引数に設定することで取得できる。
必要になったクラスのインスタンスはほとんどの場合、この DI の仕組みを使って取得できるようになっている。

Play Framework のリクエストを処理する Controller のインスタンス自体も、この仕組みによって運用されている。

具体的な方法は、Play Framework の DI のドキュメントを読んでみると良い。

ここでは、

  • startAuthentication
  • getAccessToken

という認証を行い、セッションにアクセストークンを保存するメソッドと、セッションキーからアクセストークンを取得するメソッドの2つを実装した。
また、継承する全てのコントローラーで Twitter のログインを強制するコントローラーも実装する。

まずは、controller/TwitterLoginControllerを IDEA の class を作成する機能より作成し、以下のように実装する。

controllers/TwitterLoginController.scala

package controllers

import java.util.UUID

import play.api.cache.SyncCacheApi
import play.api.mvc._
import twitter4j.auth.AccessToken

import scala.concurrent.{ExecutionContext, Future}

case class TwitterLoginRequest[A](sessionId: String, accessToken: Option[AccessToken], request: Request[A]) extends WrappedRequest[A](request)

abstract class TwitterLoginController(protected val cc: ControllerComponents) extends AbstractController(cc) {
  val cache: SyncCacheApi
  val sessionIdName = "mojipic.sessionId"

  def TwitterLoginAction = new ActionBuilder[TwitterLoginRequest, AnyContent] {
    override protected def executionContext: ExecutionContext = cc.executionContext

    override def parser: BodyParser[AnyContent] = cc.parsers.defaultBodyParser

    def invokeBlock[A](request: Request[A], block: TwitterLoginRequest[A] => Future[Result]) = {
      val sessionIdOpt = request.cookies.get(sessionIdName).map(_.value)
      val accessToken = sessionIdOpt.flatMap(cache.get[AccessToken])
      val sessionId = sessionIdOpt.getOrElse(UUID.randomUUID().toString)
      val result = block(TwitterLoginRequest(sessionId, accessToken, request))
      implicit val executionContext: ExecutionContext = cc.executionContext
      result.map(_.withCookies(Cookie(sessionIdName, sessionId, Some(30 * 60))))
    }

  }
}

以上では、Play の AbstractController を継承し、ActionBuilderクラスを利用して、TwitterLoginAction を作成している。
この実装は、Play Framework が提供している Controller の Custom Action の実装を踏襲し、それを先ほど作成した TwitterAuthenticator に対応させたものとなっている。

実装としては、リクエストの Cookie からセッション ID を取得し、それを利用してキャッシュからアクセストークンを取得する。
その後、与えられた Action 内の block 関数を実行し、Cookie の更新を行う。
Play Framework は Controller では、この Action を利用してリクエストを処理している。
Action 自体は基本的には提供されたものを利用することがほとんどだが、このようにログインを強制するような特殊な Action を実装することもできる。

そしてこれを利用して、今度はログイン・ログアウトを行うハンドラと OAuth のコールバックを受けるハンドラを実装する。
controllers/OAuthControllerを IDEA の class を作成する機能より作成し、以下のように実装する。

controllers/OAuthController.scala

package controllers

import javax.inject.Inject

import infrastructure.twitter.{TwitterAuthenticator, TwitterException}
import play.api.{Configuration}
import play.api.cache.SyncCacheApi
import play.api.mvc.ControllerComponents

import scala.concurrent.duration._

class OAuthController @Inject()(
                                 cc: ControllerComponents,
                                 twitterAuthenticator: TwitterAuthenticator,
                                 configuration: Configuration,
                                 val cache: SyncCacheApi
                               ) extends TwitterLoginController(cc) {

  val documentRootUrl = configuration.get[String]("mojipic.documentrooturl")

  def login = TwitterLoginAction { request =>
    try {
      val callbackUrl = documentRootUrl + routes.OAuthController.oauthCallback(None).url
      val authenticationUrl = twitterAuthenticator.startAuthentication(request.sessionId, callbackUrl)
      Redirect(authenticationUrl)
    } catch {
      case e: TwitterException => BadRequest(e.message)
    }
  }

  def oauthCallback(verifierOpt: Option[String]) = TwitterLoginAction { request =>
    try {
      verifierOpt.map(twitterAuthenticator.getAccessToken(request.sessionId, _)) match {
        case Some(accessToken) =>
          cache.set(request.sessionId, accessToken, 30.minutes)
          Redirect(documentRootUrl + routes.HomeController.index.url)
        case None => BadRequest(s"Could not get OAuth verifier. SessionId: ${request.sessionId}")
      }
    } catch {
      case e: TwitterException => BadRequest(e.message)
    }
  }

  def logout = TwitterLoginAction { request =>
    cache.remove(request.sessionId)
    Redirect(documentRootUrl + routes.HomeController.index.url)
  }
}

ログインでは認証を開始し、コールバックでは TwitterLoginController の提供する TwitterLoginAction を利用して、セッションからアクセストークンを取得し、logout ではキャッシュからセッションを除去することでログアウトする。

routes.OAuthController.oauthCallback(None).urlは、conf/routesファイルを追記して Controller のメソッドとルーティングを紐づけるまではコンパイルエラーが起きる。
しかし、とりあえず今は気にする必要はない。

conf/routesを以下のように実装する。

conf/routes

# An example controller showing a sample home page
GET     /                           controllers.HomeController.index

# Twitter Login
GET     /login                         controllers.OAuthController.login
GET     /logout                        controllers.OAuthController.logout
GET     /oauth_callback                controllers.OAuthController.oauthCallback(oauth_verifier: Option[String])

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file

以上で、ログイン・ログアウトを含めた認証をする処理の実装は完了。
実際にsbt runでコンパイルと実行をして、 http://localhost:9000/loginhttp://localhost:9000/logoutが動作するか試してみよう。
特にエラーなくログイン・ログアウトの実行ができ、ログイン時に Chrome のデベロッパーツールで Cookie にmojipic.sessionIdを見つけることがdけいれば、ここまでの実装は完成。

【実装】HTML にログイン・ログアウトの組み込み

もうすでにセッション内に、Twitter のアクセストークンを取得できている。
それを利用して、トップページのログイン・ログアウトの表示と、画像のドラッグ&ドロップの表示が変わるようにしてみよう。
本来、ドラッグ&ドロップエリアには、ログイン時にしか表示されないはず。
そうなるように実装していく。

まずはHomeControllerTwitterLoginControllerを継承するように実装して、テンプレートに AccessToken のインスタンスを渡すようにしている。

controllers/HomeController.scala

package controllers

import javax.inject._

import play.api.cache.SyncCacheApi
import play.api.mvc._

/**

...

* application's home page.
 */
@Singleton
class HomeController @Inject()(val cache: SyncCacheApi,
                               cc: ControllerComponents) extends TwitterLoginController(cc) {

  /**
   * Create an Action to render an HTML page.

...

   * will be called when the application receives a `GET` request with
   * a path of `/`.
   */
  def index() = TwitterLoginAction { implicit request: TwitterLoginRequest[AnyContent] =>
    Ok(views.html.index(request.accessToken))
  }
}

これで無事 index に対して AccessToken のインスタンスを渡すことができた。
今度はこれを利用して、HTML のテンプレートを更新しておく。

また、いずれ JavaScriptのコードより AJAX を利用した情報取得時も AccessToken の情報が必要となるので、クライアントのグローバルオブジェクトに AccessToken を保存しておくように実装する。

views/index.scala.htmlを以下のように実装する。

app/views/index.scala.html

@(accessToken: Option[twitter4j.auth.AccessToken])


@main("MOJIPIC - 写真に文字を書き加え") {

...

    <div class="nav-wrapper container"><a id="logo-container" href="#" class="brand-logo"><img src="@routes.Assets.versioned("images/logo.png")"></img></a>
        <ul class="right hide-on-med-and-down">
            <li>
                @if(accessToken.isEmpty) {
                <a class="light-blue-text text-darken-3" href="/login">twitterでログイン</a>
                } else {
                <a class="light-blue-text text-darken-3" href="/logout">ログアウト</a>
                }
            </li>
        </ul>

        <ul id="nav-mobile" class="side-nav">
            <li>
                @if(accessToken.isEmpty) {
                <a href="/login">twitterでログイン</a>
                } else {
                <a href="/logout">ログアウト</a>
                }
            </li>
        </ul>
        <a href="#" data-activates="nav-mobile" class="button-collapse"><i class="material-icons">menu</i></a>
    </div>
</nav>
@if(accessToken.isDefined) {
<div class="section no-pad-bot" id="index-banner">
    <div class="container">
        <br><br>

...

    </div>
</div>
}
<div class="divider"></div>

<div class="container">

...

        </div>
    </div>
</footer>
<script>
    var Mojipic = (function(){
    var accessTokenUserId = @Html(accessToken.map(t => s""""${t.getUserId}"""").getOrElse("null"));
        return {
            twitterId : function(){return accessTokenUserId;}
        };
    })();
</script>
}

以上が終わったら、早速リンクを利用してログイン、ログアウトを HTML のクライアントから実行してみよう。
また、ログイン時 Chrome のデベロッパーツールを利用してMojipic.twitterId()を呼び出した時に、ログインユーザーの Twitter の内部 ID が表示されれば無事実装は完了。

まとめ

  • Play Framework では Twirl というコンパイルされるテンプレートエンジンを利用する
  • ES6 以上が利用される JavaScript のクライアントを ES5 に対応するものへとトランスパイルするための Babel というトランスパイラを利用する
  • Play Framework では、設定やキャッシュの情報を Controller などで利用するために DI という依存するクラスのインスタンスを注入してくれる技術を利用する
0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?