概要:今回は、2部連載シリーズでお届けするチュートリアルの第1部として、Vue.js、Spring Boot、Kotlin、GraphQLを使ってモダンなウェブアプリケーションを構築する方法をご紹介していきます。また、Auth0を使用してバックエンドとフロントエンドアプリケーションを保護する方法についてもご紹介します。本シリーズで構築するアプリケーションの概要については、このGitHubレポジトリをご覧ください。
“フロントエンドにVue.js、バックエンドにSpring Boot、Kotlin、GraphQL APIを使用してモダンなウェブアプリケーションを構築する方法を紹介します。”
Tweet This
前提条件
このチュートリアルを効果的に利用するには、システムに次のツールがインストールされている必要があります。
- JDK。KotlinはJVMベースの言語で、JDK 8でSpring Bootをブートストラップするので、JDK 8以上がインストールされている必要があります。まだインストールしていない場合は、こちらからインストールしてください。
- フロントエンド開発用のNode.jsとNPM。Node.jsをインストールしていない場合は、このリソースを参照してください。インストールされているか確認するには、コマンドラインで
を実行すると、インストールされているNode.jsのバージョンが表示されます。NPMに関しては心配しなくて大丈夫です。たいていはNode.jsのインストール時に一緒にインストールされます。node -v
- Vue CLI。まだインストールしていない場合はこのリソースを使用してインストールします。このページでは、システムにインストールされているVueのバージョンも調べられます。Vue CLIは、Vue.jsアプリケーションをスキャフォールディングするコマンドラインツールです。これはVueでReact.js用のReact Appの作成に相当するものです。
- 最後に、お好みのテキストエディタまたはIDE。お勧めはVSCodeまたはIntelliJ IDEAです。
こうしたツールに加えて、このチュートリアルではSpring BootとKotlin、Vue.ks、Vuex、Vue 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
次に"Dependencies"セクションで、
Web
、H2
、JPA
ライブラリなどの検索ボックスを使用します。それではプロジェクトを生成してください。するとブラウザで.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
を追加します。これでSpring Bootがポート8080ではなくポート8888で待機するようになります。 次に、server.port=8888
パッケージにcom.example.MovieReviewBoard
を作成してSpring Bootアプリケーションにhelloworldエンドポイントを作成します。HelloWorldController.kt
という行を新しいファイルの冒頭に加えてから、次のコードを追加します。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 } }
注:
メソッドがバックエンドAPIにクロスオリジンのサポートを追加します。このメソッドは、サーバーが送信するHTTP応答にsimpleCorsFilter()
ヘッダーが設定されます。このヘッダーの値をAccess-Control-Allow-Origin
に設定すると、このドメインからサーバーへのリクエスト送信を許可することをブラウザに指示できます。 これを設定したら、コマンドラインを使ってhttp://localhost:8080
内でMovieReviewBoard/backend/
を実行するか、IDEで実行してバックエンドサーバーを実行します。ブラウザで./gradlew bootRun
に移動すると、次のページが表示されます。http://localhost:8888/helloworld
Vue.jsアプリケーションのブートストラップ
バックエンドの構成要素が整ったので、次はVue CLIを使ってフロントエンドアプリケーションをスキャフォールディングする必要があります。上記の前提条件セクションにある指示に従って、Vue CLIがインストールされているか確認してください。アプリケーションをスキャフォールディングするには、ターミナルを開いてMovieReviewBoardディレクトリに移動し、次のコマンドを実行します。
vue create frontend
ターミナルにダイアログが作成されて設定に関する質問が提示されるので、次のように応答します。
最後の質問の答えを確認すると、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>{{ greeting }}</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メッセージが表示されます。Spring BootとVue.jsアプリケーションの保護
現時点ではバックエンドに誰でもアクセスできる状態にあります。これでは事業に差し障りがあるので、APIを保護しましょう。APIの保護には、Auth0のようなアイデンティティ管理プロバイダが必要です。セキュリティにAuth0を使用するには、アカウントがない場合にはアカウントを作成し、アカウントがある場合にはログインする必要があります。
Auth0アカウントにサインアップしたら、Auth0でバックエンドAPIを表すAPIを作成して構成し、リクエストを認証する必要があります。これを行うには、Auth0ダッシュボードのAPIセクションに移動して「Create API」ボタンをクリックします。すると、ダッシュボードに入力の必要なフォームが表示されます。
- Name:これはAPIのラベルです(例えば「Spring Boot Kotlin」にします)。
- Identifier:これはAPIのURL識別子です(ここでは
など、有効なURLに近いものにします)。http://localhost:8888
- 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.security
のSecurityConfig.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を選択する必要があります。
では、「Create」ボタンをクリックしてください。Auth0により新しいアプリケーションのQuick Startセクションにリダイレクトされます。そこから「Setting」タブをクリックして
http://localhost:8080/callback
を「Allowed 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_トークン、アクセストークン、ユーザープロファイルを戻すように指示します。上記の各メソッドの機能は次のとおりです。
:Auth0にログインフローを開始するよう指示します。login
:Auth0が認証を完了すると、Auth0ウェブ認証インスタンスで設定されたhandleAuthentication()
にリダイレクトされ、redirectUri
、id_token
、access_token
(アクセストークンの有効期限)がブラウザのURLに提供されます。このメソッドはURLからこれらの値を取得し、解析してexpires_in
を呼び出し、これをメモリに保存します。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」ボタンをクリックすると、次が表示されます。“Vue.jsとSpring Bootアプリケーションは@Auth0を使って簡単に保護できます。”
Tweet This
終わりに
これでシリーズの第1部は終了です。今回はフロントエンドとバックエンドの両方をブートストラップすることで、第2部に向けて基礎作りができました。Auth0で保護し、フロントエンドがバックエンドからのデータをどう使用するかを説明しました。第2部では、GraphQL APIの構築、Vuexでのステート管理、Vue-Routerでのルーティング、そしてBootstrap、Google Fonts、Font AwesomeによるUIの完成を重点的に説明していきます。