Rails と Vue.js を用いた SPA でログイン の仕組みを作ってみる(改)

以前、Vue.js と Rails の組み合わせをする記事を複数書いていました。

今までの組み合わせ方が、正直なところあまりよく用いられる方法でないようでした。

CORS を解決して、組み合わせる方法を確認できたので、メモがてらまとめておきます。

目次

参考

作るもの

フロントを vue.js
バックエンドの API サーバーを rails で作成し、ログイン機能を用意する。

概要

今回用意する構成の概要は以下の通りです。

(drow.io で描いてみました。Rails のアイコンと Vue.js のアイコン?もあったので便利でした。)

準備 1 API - Rails 側

これまでは認証の機能を自前で作っていましたが、今回は devise を使用して用意します。

  • ログインユーザーを管理するAccountを devise を使用して作る
  • 認証の API と、ユーザー情報問い合わせの API を用意する
  • CORS 対応する

以下、Rails の環境設定が済んでいてYay! You’re on Rails!の画面は確認できているものとします。

API の作成

devise 設定 環境用意

devise を使用してAccountモデルを作成します。

Gemfile に以下を記述し、bundle installを実行します。

1
gem 'devise'
1
2
3
4
5
6
7
8
# deviseのインストール
bundle exec rails generate devise:install

# Accountモデル他の作成
bundle exec rails generate devise Account

# マイグレーションの実行
bundle exec rails db:migrate

ここまでで、devise の設定が行われますが、ルーティングは不要なのでskipで除外します。

config/routes.rb
1
2
3
4
5
Rails.application.routes.draw do
# 標準のdeviseが用意するルーティングは一切使わない
# しかしdevise_for 書かないとcurrent_hogehogeなどのメソッドが使用できなくなるので注意する
devise_for :accounts, skip: [:sessions, :passwords, :registrations]
end

コントローラの用意

app/controllers/api以下のコントローラのベースになるapp/controllers/api/application_controller.rb

app/controllers/api/application_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Api::ApplicationController < ActionController
# CSRF対策用のトークンを用いないのでチェックを外す
skip_before_action :verify_authenticity_token

# リクエストがxhr(XMLHttpRequest)であることをチェックする。
before_action :check_xhr_header

private
def check_xhr_header
return if request.xhr?

render json: { error: 'forbidden' }, status: :forbidden
end
end

ログイン・ログアウト処理を行うapp/controllers/api/session_controllser.rb

app/controllers/api/session_controllser.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Api::SessionController < Api::ApplicationController
def log_in
account = Account.find_for_database_authentication(email: account_param[:email])

return render json: {result: false} if account.nil?

if account.valid_password?(account_param[:password])
sign_in :account, account
render json: {state: true}
else
render json: {state: false}
end
end

def log_out
sign_out current_account
render json: {state: true}
end

private
def account_param
params.require(:account).permit(:email, :password)
end
end

ログインしたユーザーがアカウント情報を問合せできるapp/controllers/api/accounts_controller.rb

app/controllers/api/accounts_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Api::AccountsController < Api::ApplicationController

before_action :account_login

def show
render json: {email: current_account[:email]}
end

private
def account_login
# ログインしていない場合空のjsonを返す
# リダイレクトさせたいわけではないので authenticate_account! は使わないことにした
return render json: {} unless account_signed_in?
end
end

ルーティングの用意

config/routes.rb
1
2
3
4
5
6
7
8
9
Rails.application.routes.draw do
devise_for :accounts, skip: [:sessions, :passwords, :registrations]

namespace :api do
post '/login', to: 'session#log_in'
post '/logout', to: 'session#log_out'
get '/account', to: 'accounts#show'
end
end

CORS 設定

rack-cors を使用して CORS の設定を追加します。

Gemfile に以下を記述し、bundle installを実行します。

1
gem 'rack-cors'

rack-corsの設定を記述します。

config/initializers/cors.rb
1
2
3
4
5
6
7
8
9
10
11
12
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
# APIを実行するドメイン 今回は、フロントはlocalhost:3001で立ち上げるので、以下の通り
origins "localhost:3001"
# アクセスしていいリクエストを定義。表現が使えたので、/api以下に制限した。
# credentials: true を付与することでsession・cookieのやり取りができる
resource "^/api",
headers: :any,
methods: [:get, :post, :patch, :delete, :head, :options],
credentials: true
end
end

ここまでの設定で、localhost:3001にアクセスしたクライアントからの xhr(XMLHttpRequest) のみに応答するようになりました。

準備 2 フロント - Vue.js 側

先に作成した API へアクセスするためのフロント側のアプリケーションを作成します。

Vue.js 環境準備

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# @vue/cli インストール
npm install @vue/cli --save-dev

# プロジェクトの作成
npx vue create test-app

# 確認用途なのでデフォルトの一番上を選ぶ
> . ([Vue 2] babel, router)
Default ([Vue 2] babel, eslint)
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features

cd test-app

# APIのアクセスに使用するaxiosをインストール
npm install axios --save

vue.config.jsを作成して、開発サーバーのポートと、proxy の設定を追加します。

vue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
devServer: {
// 開発サーバーのポート設定
port: 3001,
// 特定のパス以下をrailsに転送する設定
proxy: {
"^/api": {
target: "http://localhost:3000",
},
},
},
};

ここまで出来たら、npm run serveで開発サーバーを起動します。

API を叩くためのページの用意

vue-router のルーティング記述

src/router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Vue from "vue";
import VueRouter from "vue-router";
import index from "../views/index.vue";
import login from "../views/Login.vue";
import login from "../views/Home.vue";

Vue.use(VueRouter);

const routes = [
{
path: "/",
name: "Index",
component: index,
},
{
path: "/login",
name: "Login",
component: login,
},
{
path: "/home",
name: "Home",
component: home,
},
];

const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});

export default router;

ページ作成

/ の時に表示するIndex.vue

src/views/Index.vue
1
2
3
4
5
<template>
<div class="Index">
<h1>This is index page</h1>
</div>
</template>

/loginの時に表示するsrc/views/Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<template>
<div class="login">
log in
<div>
<div>{{ this.message }}</div>
<input type="text" v-model="email" placeholder="NAME" /><br />
<input type="password" v-model="password" placeholder="PASSWORD" /><br />
<button v-on:click="login()">LOGIN</button>
</div>
</div>
</template>

<script>
import axios from "../../util/axios";
const qs = require("qs");

export default {
name: "logIn",
data: function () {
return {
email: "",
password: "",
message: "",
};
},
methods: {
async login() {
const self = this;
const result = await axios
.post("/api/login", {
account: {
email: this.email,
password: this.password,
},
paramsSerializer: function (params) {
return qs.stringify(params, { arrayFormat: "brackets" });
},
})
.catch((e) => {
console.error(e);
});

if (!result) {
this.message = "エラー";
return;
}
if (!result.data) {
this.message = "エラー";
return;
}

if (result.data.state) {
//結果を基にページ遷移
this.$router.push("/home");
}
},
},
};
</script>

/homeの時に表示するHome.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<div class="home">
<span>{{ email }}</span>
</div>
</template>

<script>
import axios from "../../util/axios";

export default {
name: "Home",
data() {
return {
email: "",
};
},
methods: {
async getAccountData() {
const result = await axios.get("/api/account").catch((e) => {
console.error(e);
});

if (!result) {
// エラーの場合ログイン画面へ遷移させる
this.redirectLogin();
return;
}
if (!result.data.email) {
// エラーの場合ログイン画面へ遷移させる
this.redirectLogin();
return;
}

this.email = result.data.email;
},
redirectLogin() {
//ページ遷移
this.$router.push("/login");
},
},
async mounted() {
this.getAccountData();
},
};
</script>

ログアウトのリンクのコンポーネント。

src/components/Logout.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<a v-on:click="login()">Logout</a>
</template>

<script>
import axios from "../../util/axios";

export default {
name: "logout",
methods: {
async login() {
const self = this;
const result = await axios.post("/api/logout").catch((e) => {
console.error(e);
});

if (!result) {
this.message = "エラー";
return;
}
if (!result.data) {
this.message = "エラー";
return;
}

if (result.data.state) {
//結果を基にページ遷移
this.$router.push("/");
}
},
},
};
</script>

axios のカスタマイズ

バックエンドの rails への post をするために、http クライアントとして使用する axios のカスタマイズを行います。

util/axios.js
1
2
3
4
5
6
7
8
9
10
import axios from "axios";

export default axios.create({
// ヘッダーにX-Requested-Withを追加する
headers: {
"X-Requested-With": "XMLHttpRequest",
},
// リクエストに、sessionとcookieを含めるようにする
withCredentials: true,
});

先に示した.vueファイルでは、こちらのデフォルト設定を施したaxiosを使わせます。

リクエストヘッダーに"X-Requested-With": "XMLHttpRequest"を付与し、Rails 側ではrequest.xhr?にて xhr のリクエストであるかをチェックしています。
拡張ヘッダーは、いわゆるフォームでリクエストする際には使用ができません。
そのうえで、origin のチェックを行うことで、CSRF を防ぎます。

動作確認

2 つのコンソールでnpm run servebundle exec rails sの 2 つを実行します。
localhost:3001にアクセスして、ログインを試します。

/では、機能がないページを表示しているので、Loginを押して遷移します。

ログインのフォームが表示されるので、入力します。

ログインすると、/homeに遷移して、メールアドレス(マスクしています)が表示されます。

ログアウトすると/loginに遷移されます。
ログインしていない状態で、/homeに遷移しても、/loginに戻されます。

ドメインが異なるバックエンドのサーバーへのリクエストを送り、ログインの機能を作ることができました。


今回は、これまでやってきたログイン機能の実装を別ドメインのサーバーで立ててもできるように試みました。
これまでの rails の public にビルドしたファイルを展開させる方法よりもよほどこちらの方がいいと感じています。
ホットリロードも効きますし。
フロントエンドは変わってもバックエンドを Rails で立てるなら毎回この構成でいいと感じました。

気が付いたらログインログアウトの機能だけで、4 回ほど記事を書いてしまったので、そろそろ別なことがしたいですがややネタ切れです。

ではでは。