以前、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 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 def self .verification(param) user = User .find_by(name: param["name" ]) return user if user.nil ? user.authenticate(param["password" ]) end 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) 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 return render json: {state: "failure" ,msg: "Error" } , status: 403 if cookies[:authed ].nil ? user = User .find_by(session_key: cookies[:authed ]) 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 ]) 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" 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 "rails/test_unit/railtie" Bundler .require (*Rails .groups)module Test183SpaLogin2 class Application < Rails::Application config.load_defaults 5.2 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 mkdir frontcd frontnpm init -y npm install npm install @vue/cli --save-dev npx vue create . 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 : { 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 で認証の仕組みも実装できるようになったら、何かサービス作りたいけど生憎ネタがないんですよねー。
ではでは。