概要:今回は、2部連載シリーズでお届けするチュートリアルの第1部として、Vue.js、Spring Boot、Kotlin、GraphQLを使ってモダンなウェブアプリケーションを構築する方法をご紹介していきます。また、Auth0を使用してバックエンドとフロントエンドアプリケーションを保護する方法についてもご紹介します。本シリーズで構築するアプリケーションの概要については、このGitHubレポジトリをご覧ください。

"フロントエンドにVue.js、バックエンドにSpring Boot、Kotlin、GraphQL APIを使用してモダンなウェブアプリケーションを構築する方法を紹介します。"

前提条件

このチュートリアルを効果的に利用するには、システムに次のツールがインストールされている必要があります。

  • JDK。KotlinはJVMベースの言語で、JDK 8でSpring Bootをブートストラップするので、JDK 8以上がインストールされている必要があります。まだインストールしていない場合は、こちらからインストールしてください。
  • フロントエンド開発用のNode.jsとNPM。Node.jsをインストールしていない場合は、このリソースを参照してください。インストールされているか確認するには、コマンドラインでnode -vを実行すると、インストールされているNode.jsのバージョンが表示されます。NPMに関しては心配しなくて大丈夫です。たいていはNode.jsのインストール時に一緒にインストールされます。
  • Vue CLI。まだインストールしていない場合はこのリソースを使用してインストールします。このページでは、システムにインストールされているVueのバージョンも調べられます。Vue CLIは、Vue.jsアプリケーションをスキャフォールディングするコマンドラインツールです。これはVueでReact.js用のReact Appの作成に相当するものです。
  • 最後に、お好みのテキストエディタまたはIDE。お勧めはVSCodeまたはIntelliJ IDEAです。

こうしたツールに加えて、このチュートリアルではSpring BootとKotlinVue.ksVuexVue Routerの基礎知識も必要となります。

モダンなウェブアプリケーションのアーキテクチャ

モダンなウェブアプリケーションのほとんどは、バックエンドAPIと通信するシングルページアプリケーション(SPA)として構築されます。なぜSPAなのでしょうか?SPAは、サーバーがブラウザにシングルテンプレートを提供するブラウザベースのアプリケーションであり、他のページを動的にレンダリングするためにJavaScriptが使用されます。SPAはたいてい最初のページの読み込みに時間がかかりますが、その後はスムーズなユーザーエクスペリエンスを提供します。コンテンツのアップデートでは、SPAはJavaScriptでバックエンドAPIにクエリする必要があります。従来、APIはRESTパターンに従って設計されてきましたが、現在では多くの人が、RESTよりも効率が良いという理由からGraphQLを使ってAPIを構築しています。これは、GraphQLでは複数の連続したREST APIリクエストを1つのAPIリクエストに変換できるからです。

では、KotlinとVue.jsはなぜ使うのでしょうか? * Kotlinでは開発者の生産性アップを図れる。 * Vue.jsはドキュメンテーションが優れている。

構築するアプリケーション

このシリーズでは、ユーザーがお気に入りの映画を鑑賞して監督情報を表示し、評価することのできる"Moview Review Board"というアプリケーションを構築していきます。すでにシリーズのタイトルからお分かりかと思いますが、Vue.js、Vuex、Vue Router、VueのEvent BusでSPAを構築し、Spring Boot、Kotlin、GraphQLでバックエンドAPIを構築します。

Spring Bootアプリケーションのブートストラップ

では、構築するアプリケーションが分かったところで、Spring Initializrに話を移して、必要なツールとライブラリを選んでいきましょう。Spring Initializrページでは、フォームで次のアイテムを選択する必要があります。

  • Project:Gradle Project
  • Language:Kotlin
  • Spring Boot:2.1.5
  • Group: com.example
  • Artifact:MovieReviewBoard

Spring Initializrの設定

次に"Dependencies"セクションで、WebH2JPAライブラリなどの検索ボックスを使用します。

Dependenciesの設定

それではプロジェクトを生成してください。するとブラウザで.zipファイルがダウンロードされます。このフォルダを解凍して、そこにbackendフォルダを作成します。このbackendフォルダへのパスはMovieReviewBoard/backendとします。そして、MovieReviewBoardフォルダのすべてのコンテンツをbackendフォルダに入れます。これでbackendフォルダのコンテンツを好みのSpring Boot IDEにロードできるようになります。

注:デフォルトでは、Spring Bootのservletコンテナはポート8080で待機するため、システム内の他のプログラムはそのポート番号で待機できなくなります。残念ながらWebpack(Vue.jsが使用するサーバー)もポート8080で実行されるので、同時に両方のプログラムを実行することはできなくなります。この問題を解決するには、アプリケーション内の/MovieReviewBoard/backend/src/main/resources/application.propertiesファイルに移動してserver.port=8888を追加します。これでSpring Bootがポート8080ではなくポート8888で待機するようになります。 次に、com.example.MovieReviewBoardパッケージにHelloWorldController.ktを作成してSpring Bootアプリケーションにhelloworldエンドポイントを作成します。package com.example.MovieReviewBoardという行を新しいファイルの冒頭に加えてから、次のコードを追加します。

import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.core.Ordered
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import org.springframework.web.filter.CorsFilter
import java.util.*
import javax.servlet.Filter

@RestController
class HelloWorldController {

    @GetMapping("/helloworld")
    fun greet(): String {
        return "helloworld!"
    }

    @Bean
    fun simpleCorsFilter(): FilterRegistrationBean<*> {
        val source = UrlBasedCorsConfigurationSource()
        val config = CorsConfiguration()
        config.allowCredentials = true
        // *** 下記のURLはVueクライアントURLとポートに一致させる必要がある***
        config.allowedOrigins = Collections.singletonList("http://localhost:8080")
        config.allowedMethods = Collections.singletonList("*")
        config.allowedHeaders = Collections.singletonList("*")
        source.registerCorsConfiguration("/**", config)
        val bean = FilterRegistrationBean<Filter>(CorsFilter(source))
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE)
        return bean
    }
  }

注:simpleCorsFilter()メソッドがバックエンドAPIにクロスオリジンのサポートを追加します。このメソッドは、サーバーが送信するHTTP応答にAccess-Control-Allow-Originヘッダーが設定されます。このヘッダーの値をhttp://localhost:8080に設定すると、このドメインからサーバーへのリクエスト送信を許可することをブラウザに指示できます。 これを設定したら、コマンドラインを使ってMovieReviewBoard/backend/内で./gradlew bootRunを実行するか、IDEで実行してバックエンドサーバーを実行します。ブラウザでhttp://localhost:8888/helloworldに移動すると、次のページが表示されます。

Helloworldエンドポイントページ

Vue.jsアプリケーションのブートストラップ

バックエンドの構成要素が整ったので、次はVue CLIを使ってフロントエンドアプリケーションをスキャフォールディングする必要があります。上記の前提条件セクションにある指示に従って、Vue CLIがインストールされているか確認してください。アプリケーションをスキャフォールディングするには、ターミナルを開いてMovieReviewBoardディレクトリに移動し、次のコマンドを実行します。

vue create frontend

ターミナルにダイアログが作成されて設定に関する質問が提示されるので、次のように応答します。

Vueスキャフォールディングの設定

最後の質問の答えを確認すると、Vue CLIがVueアプリケーションの入ったfrontendフォルダを生成します。次に、バックエンドAPIのhelloworldエンドポイントを使用するボタンを追加します。そのためには、MovieReviewBoard/frontend/src/views/About.vueページを次のように変更する必要があります。

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button @click="callAPI">Call API</button>
    <h2></h2>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "about",
  data() {
    return {
      greeting: ""
    };
  },
  methods: {
    callAPI() {
      axios
        .get("http://localhost:8888/helloworld")
        .then(resp => {
          this.greeting = resp.data;
        })
        .catch(err => {
          this.greeting = err;
        });
    }
  }
};
</script>

上記の.vueファイルによって、Aboutページのテンプレートにボタンが追加され、@click属性をcallAPIに設定するときにこのボタンのonClickイベントハンドラーとしてcallAPI()メソッドも追加されます。CallAPIメソッドが呼び出されると、バックエンドからhelloworldエンドポイントが呼び出され、これに応答してhelloworld! メッセージでテンプレートを更新する、greetingデータ属性が更新されます。このテストを行う前に、axios(バックエンドAPIを呼び出すためにcallAPI()で使用されるHTTPクライアントモジュール)を追加します。コマンドラインでMovieReviewBoard/frontend/ディレクトリに移動して、次を実行します。

npm install axios --save

この変更を行ったら、コマンドラインかIDEを使って、MovieReviewBoard/frontend/npm run serveを実行してフロントエンドサーバーを起動し、MovieReviewBoard/backend/./gradlew bootRunを実行してバックエンドサーバーを起動させます。2台のサーバーが起動したら、ブラウザでhttp://localhost:8080/#/aboutに移動して「Call API」ボタンをクリックします。するとサーバーからのhelloworldメッセージが表示されます。

HelloWorldメッセージ

Spring BootとVue.jsアプリケーションの保護

現時点ではバックエンドに誰でもアクセスできる状態にあります。これでは事業に差し障りがあるので、APIを保護しましょう。APIの保護には、Auth0のようなアイデンティティ管理プロバイダが必要です。セキュリティにAuth0を使用するには、アカウントがない場合にはアカウントを作成し、アカウントがある場合にはログインする必要があります。

Auth0アカウントにサインアップしたら、Auth0でバックエンドAPIを表すAPIを作成して構成し、リクエストを認証する必要があります。これを行うには、Auth0ダッシュボードのAPIセクションに移動して「Create API」ボタンをクリックします。すると、ダッシュボードに入力の必要なフォームが表示されます。

  • Name:これはAPIのラベルです(例えば「Spring Boot Kotlin」にします)。
  • Identifier:これはAPIのURL識別子です(ここではhttp://localhost:8888など、有効なURLに近いものにします)。
  • Signing Algorithm:このフィールドにはRS256を選択します。

その後、「Create」ボタンをクリックしてAPIの作成を完了します。次に、backend/build.gradleファイルを開いて次のコードを追加します。

// ...

dependencies {
  // ...
  implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.1.3.RELEASE'
}

// ...

この依存関係により、OAuth 2.0.でAPIを保護する機能が加わります。このライブラリを使用するのに必要な設定を追加するには、2つの手順が必要です。まず最初に、securityパッケージをcom.example.MovieReviewBoardパッケージに作成し、そこにSecurityConfig.ktファイルを作成します。その後、以下のコードをcom.example.MovieReviewBoard.securitySecurityConfig.ktファイルにコピーします。

package com.example.MovieReviewBoard.security

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer

@Configuration
@EnableResourceServer
class SecurityConfig : ResourceServerConfigurerAdapter() {

    @Value("\${security.oauth2.resource.id}")
    private lateinit var resourceId: String

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
                .mvcMatchers("/helloworld").authenticated()
                .anyRequest().permitAll()
    }

    @Throws(Exception::class)
    override fun configure(resources: ResourceServerSecurityConfigurer) {
        resources.resourceId(resourceId)
    }
  }

上記の@EnableResourceServerの注釈は、OAuth 2.0.で保護されたリソースサーバーに便利です。これによって、受信したリクエストを認証するSpring Securityフィルタが自動で有効になります。Spring Bootがこれを処理するには、resourceIdとも呼ばれるAuth0 API識別子を登録して、どのエンドポイントを保護したいのか(ここでは/helloworld)を特定してやる必要があります。

次に、APIに送信されるアクセストークンを検証するために、resourceIdとAPI用のエンドポイントを定義してパブリックキーを取得する必要があります。これにはMovieReviewBoard/backend/src/main/resources/application.propertiesを開いて次の設定を追加します。

# ...
security.oauth2.resource.id=<YOUR-AUTH0-API-IDENTIFIER>
security.oauth2.resource.jwk.keySetUri=https://<YOUR-AUTH0-DOMAIN>/.well-known/jwks.json

上記の手順を完了すると、APIのhelloworldエンドポイントが保護されるようになります。両方のアプリケーションを実行してから、ブラウザでhttp://localhost:8080/#/aboutに移動して「Call API」ボタンをクリックすると、401ステータスのエラーメッセージが表示されるはずです。このページは、helloworldエンドポイントでコンテンツを表示する権限がないことを示しています。ここでは次のようなaboutページが表示されるはずです。

認証されないリクエスト

Auth0でAPIを保護したことで、APIにリクエストを送信するにはAuth0からアクセストークンを取得しなければならなくなりました。SPAでアクセストークンを取得するには、これもAuth0でSPAを保護する必要があります。Auth0でSPAの認証を実装するには、Auth0アプリケーションを作成することから始めなければいけません。そのためには、Auth0ダッシュボードの「Applications」ページに移動して、「Create Application」ボタンをクリックします。ボタンをクリックするとAuth0にダイアログが表示されます。そこにアプリケーションの名前(例えば"MovieReviewBoard"などにします)を入力して、アプリケーションのタイプにSPAを選択する必要があります。

Auth0アプリケーションの作成

では、「Create」ボタンをクリックしてください。Auth0により新しいアプリケーションのQuick Startセクションにリダイレクトされます。そこから「Setting」タブをクリックしてhttp://localhost:8080/callbackを「Alloowed Callback URLs」フィールドに、http://localhost:8080を「Allowed Web Origins」、「Allowed Logout URLs」、「Allowed Origins(CORS)」の各フィールドに追加します。「Save Changes」をクリックして設定を保存します。次に、SPAにAuth0を統合する必要があります。まず、プロジェクトのフロントエンドディレクトリで次のコマンドを実行して、アプリケーションにauth0.jsをインストールします。

npm install --save auth0-js

これでプロジェクトにauth0.js依存関係ができるので、それを使用して認証サービスを作成する必要があります。これにはMovieReviewBoard/frontend/authディレクトリを作成して、そこにauthService.jsファイルを作成します。最後に、authService.jsに次のコードを追加します。

import auth0 from "auth0-js";
import authConfig from "../auth_config.json";
import eventBus from "../event-bus";

const webAuth = new auth0.WebAuth({
  domain: authConfig.domain,
  redirectUri: `${window.location.origin}/callback`,
  clientID: authConfig.clientId,
  audience: authConfig.audience,
  responseType: "token id_token",
  scope: "openid profile email"
});

class AuthService {
  accessToken = null;
  idToken = null;
  profile = null;
  tokenExpiry = null;
  // ユーザーログインフローを開始する
  login(customState) {
    webAuth.authorize({
      appState: customState
    });
  }
  // Auth0からのコールバックリクエストを処理する
  handleAuthentication() {
    return new Promise((resolve, reject) => {
      webAuth.parseHash((err, authResult) => {
        if (err) {
          reject(err);
        } else {
          this.localLogin(authResult);
          resolve(authResult.idToken);
        }
      });
    });
  }
  localLogin(authResult) {
    this.idToken = authResult.idToken;
    this.profile = authResult.idTokenPayload;
    this.accessToken = authResult.accessToken;
    // JWT有効期限の時間を秒からミリ秒に変換する
    this.tokenExpiry = new Date(this.profile.exp * 1000);
    localStorage.setItem("loggedIn", "true");
    eventBus.$emit("login");
  }
}

export default new AuthService();

現時点ではこのコードは意味不明に見えるかもしれませんが、ここから詳しく説明していきます。上記のファイルは、まずはじめにauth0-js認証ライブラリ、そしてAuth0アプリケーションの設定の一部を含むJSONファイルauth_config.json(このファイルの作成方法は後ほど説明するのでご心配なく)とVueイベントバスをインポートします。その後、auth_config.jsonの設定を使用してAuth0ウェブ認証インスタンスを作成し、Auth0にID_トークンアクセストークンユーザープロファイルを戻すように指示します。上記の各メソッドの機能は次のとおりです。

  • login:Auth0にログインフローを開始するよう指示します。
  • handleAuthentication():Auth0が認証を完了すると、Auth0ウェブ認証インスタンスで設定されたredirectUriにリダイレクトされ、id_tokenaccess_tokenexpires_in(アクセストークンの有効期限)がブラウザのURLに提供されます。このメソッドはURLからこれらの値を取得し、解析してlocalLoginを呼び出し、これをメモリに保存します。

どうも説明が多くて失礼しました!次は、MovieReviewBoard/frontend/auth_config.jsonを作成し、そこにAuth0アプリケーションの設定を次のように追加します。

{
    "domain": "<Auth0-domain>",
    "clientId": "<Auth0-App-ClientId>",
    "audience": "<Auth0-API-IDENTIFIER>"
}

その後、MovieReviewBoard/frontend/event-bus.jsファイルを作成して次のコードを追加します。

import Vue from "vue";
const EventBus = new Vue();
export default EventBus;

すると、アプリケーションはVueのイベントバスを使用してイベントの作成と処理が行えるようになります。authService.jsファイルにAuth0ウェブ認証インスタンスのredirectUriプロパティを定義しますが、このリダイレクトURLのルートとコンポーネントはまだ作成していません。この問題を解決するには、まず下記のように、MovieReviewBoard/frontend/src/router.jsでルートを作成します。

// ..................
import Callback from "./components/Callback.vue";

export default new Router({
  mode: "history",
  routes: [
    //..............
    {
      path: "/callback",
      name: "callback",
      component: Callback
    },
    //.....................
  ]
});

このファイルがCallbackコンポーネントのルートをアプリケーションに追加し、ルーティングモードを履歴モードに変更します(ルーティングモードの詳細はこちら)。次に、MovieReviewBoard/frontend/src/componentsディレクトリにCallback.vueファイルを作成して次のコードを追加します。

<template>
  <div>Loading.............</div>
</template>

<script>
import eventBus from "../../event-bus";
import authService from "../../auth/authService";
export default {
  name: "callback",
  methods: {
    handleLogin() {
      this.$router.push("/about");
    }
  },
  created() {
    authService.handleAuthentication();
    eventBus.$on("login", () => this.handleLogin());
  }
};
</script>

<style scoped></style>

Auth0で認証後にこのコンポーネントを作成すると、認証サービスのhandleAuthentication()メソッドにより解析されるURLで、Auth0のトークンが取得できます。handleAuthentication()は、locallogin()を通してログインイベントを生成します。このイベントはその後、aboutページにリダイレクトするhandlelogin()を呼び出すことにより、上記のcreated()メソッドで処理されます。これでaboutページが表示され、認証も済んだので、About.vueページを変更することでSpring Boot APIに認可リクエストを送ることができるようになりました。

<!-- leaving everything else unchanged -->
<script>
import axios from "axios";
import authService from "../../auth/authService";
export default {
  name: "about",
  data() {
    return {
      greeting: "",
      accessToken: null
    };
  },
  created() {
    this.accessToken = authService.accessToken;
  },
  methods: {
    callAPI() {
      axios({
        method: "GET",
        url: "http://localhost:8888/helloworld",
        headers: { authorization: `Bearer ${this.accessToken}` }
      })
        .then(resp => {
          this.greeting = resp.data;
        })
        .catch(err => {
          this.greeting = err;
        });
    }
  }
};
</script>

APIへのHTTP呼び出しについては、トークンで認可ヘッダーを設定することに注意してください。ここまでの手順を完了すれば、アプリケーションがユーザーを認証できるようになるはずですね。それが実はまだなのです。認証プロセスを始動させるログインボタンをまだ追加していないのです。そのためには、Home.vueページを次のように修正します。

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" /><br />
    <button @click="login">Login</button>
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

<script>
// @は/srcへのエイリアス
import HelloWorld from "@/components/HelloWorld.vue";
import authService from "../../auth/authService";
export default {
  name: "home",
  components: {
    HelloWorld
  },
  methods: {
    login() {
      authService.login();
    }
  }
};
</script>

テンプレートにはブレークとボタンタグが追加されていますね。ボタンタグには、ボタン上のクリックイベントを待機して、アプリケーションの認証プロセスを始動させるlogin()メソッドを呼び出すイベントリスナー@clickがあります。両方のサーバーを実行して認証し、「Call API」ボタンをクリックすると、次が表示されます。

HelloWorldメッセージ

"Vue.jsとSpring Bootアプリケーションは@Auth0を使って簡単に保護できます。"

終わりに

これでシリーズの第1部は終了です。今回はフロントエンドとバックエンドの両方をブートストラップすることで、第2部に向けて基礎作りができました。Auth0で保護し、フロントエンドがバックエンドからのデータをどう使用するかを説明しました。第2部では、GraphQL APIの構築、Vuexでのステート管理、Vue-Routerでのルーティング、そしてBootstrap、Google Fonts、Font AwesomeによるUIの完成を重点的に説明していきます。