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

以前、Rails と Vue.js CSRF 対策を意識してシングルページアプリケーションを作ってみるという記事を書きました。

今回は、タイトル通り Rails と Vue.js を用いた SPA でログインの仕組みを用意してみます。

※2020/07/14 追記
Cookie の取り扱い方についての修正事項を追記しています。
Rails と Vue.js を用いた SPA でログイン の仕組みを作ってみる(Cookie での注意編)

目次

実装 1 (バックエンド)

rails new からテーブル作成

Rails は、API モードで用意します。
以下のように実行しました。

1
2
3
4
5
6
7
bundle init

# Gemfileは、https://ccbaxy.xyz/blog/2020/03/17/ruby32/ で用意したGemfileに書き換えて使用する。

bundle exec rails new . --api
< n
bundle install --path=vendor/bundle

認証のための Users テーブルを作るためにマイグレーションファイルを用意します。

1
2
bundle exec rails db:create
bundle exec rails g migration create_users
db/migrate/[数字列]_create_users.rb
1
2
3
4
5
6
7
8
9
10
11
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.string :password_digest
t.string :session_key

t.timestamps
end
end
end

マイグレーションファイルを修正できたら、bundle exec rails db:migreteを実行します。

モデルの作成

users テーブルを作成しているので、user モデルを作成します。
以下の通りです。

app/models/user.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 User < ApplicationRecord
require 'digest'
has_secure_password

# nameと、passwordを用いてユーザーを検証する
def self.verification(param)
user = User.find_by(name: param["name"])
return user if user.nil?
user.authenticate(param["password"])
end

# userに対して、セッションキーを発行と保存を行う
def set_session_key
sha256 = Digest::SHA256.new
sha256.update("#{Time.current}_SOLT")
self.session_key = sha256.hexdigest

if self.save
self.session_key
else
nil
end
end
end

コントローラーと、ルーティング

コントローラを 3 つ用意します。

app/controllers/api/base.rbは、API 用の各コントローラの継承元になります。
API モードでは、Cookie の使用は標準対応ではありません。
Cookie を使用したいので、継承元で読み込めるようにします。

app/controllers/api/base.rb
1
2
3
class Api::Base < ApplicationController
include ActionController::Cookies
end

app\controllers\api\auth_controller.rbは、認証処理で使用するコントローラです。

app\controllers\api\auth_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Api::AuthController < Api::Base
def verification
if u = User.verification(auth_params)
# 認証成功したので、
# cookieにセッション情報を書き込み
cookies[:authed] = { value: u.set_session_key, expires: 1.hour.from_now }
render json: {state:"success",msg:"Login Success"} , status: 200
else
# 認証に失敗したので、エラーを返す。
# 今回は簡単にエラーであることを通知
render json: {state:"failure",msg:"Error"} , status: 403
end
end

private
def auth_params
params.require(:user).permit(:name, :password)
end
end

app\controllers\api\users_controller.rbは、ユーザー情報を返すコントローラーです。
今回は、ユーザーの名前を返す API と rogout を。

app\controllers\api\users_controller.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
25
26
27
28
29
30
31
32
class Api::UsersController < Api::Base
def profile
# Cookieがなければ、無条件にエラー
return render json: {state:"failure",msg:"Error"} , status: 403 if cookies[:authed].nil?

# Cookieの内容から、userを検索
user = User.find_by(session_key: cookies[:authed])

# usermが取得できていれば、nameを返す
if user
render json: {state:"success",msg:"User profile",profile: {name:user.name } } , status: 200
else
render json: {state:"failure",msg:"Error"} , status: 403
end
end

def log_out

return render json: {state:"failure",msg:"Error"} , status: 403 if cookies[:authed].nil?

user = User.find_by(session_key: cookies[:authed])

# cookieを削除
cookies.delete :authed

if user.remove_session_key
render json: {state:"success",msg:"Log Outed" } , status: 200
else
render json: {state:"failure",msg:"Error"} , status: 403
end
end
end

これらへのルーティングをconfig/routes.rbで以下のように設定します。

config/routes.rb
1
2
3
4
5
6
7
Rails.application.routes.draw do
namespace :api do
post "auth/verification", to: "auth#verification"
get "user/profile", to: "users#profile"
post "user/log_out", to: "users#log_out"
end
end

Cookie を使えるように

API モードでは、Cookie の利用が標準ではないのは、前述の通りです。
Cookie の書き込みだけでなく、受け取りも設定します。
config/application.rbを以下の通り編集します。

config/application.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
25
26
27
28
29
require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Test183SpaLogin2
class Application < Rails::Application
config.load_defaults 5.2

# ここ部分を追加し、Cookieを使うようにする
config.middleware.use ActionDispatch::Cookies

config.api_only = true
end
end

ユーザーの追加

ユーザーの登録画面は、今回作成しないのでユーザーは、コンソールで作成します。
以下の通りです。

1
2
3
bundle exec rails c
>u = User.new({name:"test",password:"test00"})
>u.save

実装 2 (フロントエンド)

準備と実行まで

今回のフロントエンドを vue.js で作成するにあたって、以下のように用意します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 現在railsのアプリケーションルートにいるとして
mkdir front
cd front
npm init -y
npm install
npm install @vue/cli --save-dev
npx vue create .
# 現在のディレクトリに作成する
# 設定は、presetを使用せず、Ruterを使用するようにチェックを入れるように選択すること

# ajax通信に使用するので、以下もインストール
npm install axios qs --save

npm run serve

webpack-dev-server-proxy を設定
開発サーバにプロキシ設定を追加します。
front/vue.config.jsを追加します。

front/vue.config.js
1
2
3
4
5
6
module.exports = {
devServer: {
// rails は3000番ポートで起動している
proxy: "http://localhost:3000",
},
};

ここまで出来たらnpm run serveで起動します。
Vue.js のアイコンが見えたら、一旦 OK です。

view の作成

元からある Home.vue の編集と、LogIn.vue を新たに作成します。

front\src\views\Home.vueは以下の通りです。
マウントされると、/api/user/profileに問い合わせ、エラーであれば/log_inに遷移させます。

front\src\views\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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
<div class="home">
{{ name }}
<button v-on:click="logout()">Log Out</button>
</div>
</template>

<script>
const axios = require("axios");

export default {
name: "Home",
components: {},
data: function () {
return {
name: "",
};
},
methods: {
getProfile: async function () {
const self = this;
const result = await axios
.get("/api/user/profile", {
withCredentials: true,
})
.catch(function () {
self.$router.push("/log_in");
return;
});

if (result === undefined) {
return;
}

if (result.data.state != "success") {
this.$router.push("/log_in");
return;
}

this.name = result.data.profile.name;
},
logout: async function () {
const self = this;
const result = await axios
.post("/api/user/log_out", {
withCredentials: true,
})
.catch(function () {
self.$router.push("/log_in");
});

if (result === undefined) {
return;
}

if (result.data.state != "success") {
self.$router.push("/log_in");
}
},
},
mounted: async function () {
await this.getProfile();
},
};
</script>

front\src\views\LogIn.vueは以下の通りです。
name と password を入れるだけの input と、認証情報を post する仕組みが記述されます。

front\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
<template>
<div class="log_in">
log in
<div>
<div>{{ this.message }}</div>
<input type="text" v-model="name" placeholder="NAME" /><br />
<input type="password" v-model="password" placeholder="PASSWORD" /><br />
<button v-on:click="login()">LOGIN</button>
</div>
</div>
</template>

<script>
const axios = require("axios");
const qs = require("qs");

export default {
name: "logIn",
components: {},
data: function () {
return {
name: "",
password: "",
message: "",
};
},
methods: {
login: async function () {
const self = this;
const result = await axios
.post("/api/auth/verification", {
user: {
name: this.name,
password: this.password,
},
paramsSerializer: function (params) {
return qs.stringify(params, { arrayFormat: "brackets" });
},
})
.catch(function () {
self.message = "入力エラー";
});

if (result.data.state == "success") {
this.$router.push("/");
}
},
},
};
</script>

ルーティングの作成

front/src/router/index.jsを以下のように修正します。
編集前に書かれていた/aboutへの設定削除と、/log_inへの設定追加をします。

front/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
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import LogIn from "../views/LogIn.vue";

Vue.use(VueRouter);

const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/log_in",
name: "LogIn",
component: LogIn,
},
];

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

export default router;

動作確認

ここまでできたら、bundel exec rails sで、Rails を起動。
frontディレクトリで npm run serveを実行し、webpack-dev-server を起動します。

webpack-dev-serverは、標準だと 8080 番ポートで起動しているはずなので、ブラウザでlocalhost:8080にアクセスします。

すると/log_inにリダイレクトされるので、コンソールから設定した name と password を入力してログインします。
/に遷移して、現在ログインしているユーザーの name が表示されています。

動作が確認できました。

試しに、Cookie を削除してリロードすると、/log_inにリダイレクトされます。


今回は、Rails と Vue.js を用いた SPA で、ログイン の仕組みを作ってみました。
webpack-dev-server のプロキシ機能を使って Rails と連携させました。

以前書いた記事では、npm スクリプトで、Rails の public ディレクトリにビルドしたファイルを書き出していました。
今回の方法の方が、きれいに収まると感じました。

Cookie で実装認証のトークンを持ちまわしましたが、LocalStrage で持ちまわすパターンの実装も確認を進めているので、
次はそちらをやってみようと考えています。

SPA で認証の仕組みも実装できるようになったら、何かサービス作りたいけど生憎ネタがないんですよねー。

ではでは。