[ Rails & Nuxt ] Devise_token_auth とAuth Moduleでログイン認証機能を実装する

| No. | タイトル |
|:-:|:-:|
| 1 | [Dockerで開発環境を構築する](https://qiita.com/15recruit15/items/85ab5bbf6b900c004186) |
| 2 | [ログイン認証を機能を実装する](https://qiita.com/15recruit15/items/9d6803ad4aa412848a4f)|
| 3 | 記事投稿機能を実装する|
| 4 | [AWS ECSを使ってデプロイする](https://qiita.com/15recruit15/items/73eea044288bfdbb4fa0)  |
| 5 | Circle CIを使って自動テスト•デプロイをする|
---

##はじめに
Rails & Nuxtでポートフォリオを作成するシリーズの第2弾になります。
全5部構成でDocker,CircleCI,AWS等のモダンな技術を組み込んだ作品を完成させる予定です。

本章では簡単なログイン認証機能の実装を行います。

## 事前知識 or 参考資料
### Devise-token-auth
Deviseと組み合わせてRailsにおけるトークン認証を実現するgemです。
↓公式ドキュメント

https://devise-token-auth.gitbook.io/devise-token-auth/usage

### Auth Module
Vuexを使ってログイン状態やユーザー情報を管理してくれます。
最新バージョンでは名前が`nuxt-auth`に変更されていますが、今回は安定版を使用します。
↓公式ドキュメント

https://auth.nuxtjs.org

### Axios
HTTPの非同期通信を簡単に行うことができるJavascriptライブラリ
↓公式ドキュメント

https://axios.nuxtjs.org/

### CORS
**Cross Origin Resource Sharing**の略。自分以外のどのオリジンからのCRUDリクエストを受け付けるか、受け付けないかをフィルターし、セキュリティを高めるためのものです。

https://qiita.com/mtoyopet/items/326ba62d485e9ef0dacd

##Rails側のセットアップ

###Gemの追加

```ruby:Gemfile
#devise関連
gem 'devise',             '4.8.0'
gem 'devise_token_auth',  '1.1.5'
gem 'devise-i18n',        '1.9.4'
#CORS設定
gem 'rack-cors',          '1.1.1'
```

**devise**と**devise_token_auth**を使用して、認証機能を実装します。
**devise-i18n**はdeviseの出力メッセージを翻訳してくれるgemです。

**install**

```
$: docker compose build 
$: docker compose run back rails g devise:install
$: docker compose run back rails g devise_token_auth:install User auth
$: docker compose run back rails g devise:i18n:locale ja
```

### devise-i18n 設定
```ruby:application/config
#deviseの出力メッセージを日本語にする。
config.i18n.default_locale = :ja
```



###ルーティング 設定
`devise_for: users`の部分は、**current_user**などのdevise gemに用意されたメソッドを使えるようにする為の記述です。
devise_token_authのルーティングは:apiという名前空間内に設定します。`/api/auth`というパスでアクセス出来ます。

```ruby:routes.rb
Rails.application.routes.draw do
  devise_for :users
  namespace :api do
    mount_devise_token_auth_for 'User', at: 'auth'
  end
end
```

###CORS対策
Nuxt側のオリジン`localhost:8000`からの接続を許可します。
`:expose`の部分で、`access_token`等、ユーザー認証に必要な要素をHeaderに含めるよう指定します。

```ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8000'

    resource '*',
      headers: :any,
      :expose  => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

###devise_token_authの設定
`config.change_headers_on_each_request`:リクエスト毎にtokenを更新するかどうか。
`token_lifespan`: tokenの有効期間
`headers_name`: 認証に使うヘッダー要素の名前の定義

```ruby:config/initializers/devise_token_auth.rb
DeviseTokenAuth.setup do |config|
  config.change_headers_on_each_request = false
  config.token_lifespan = 2.weeks
  config.headers_names = {:'access-token' => 'access-token',
                          :'client' => 'client',
                          :'expiry' => 'expiry',
                          :'uid' => 'uid',
                          :'token-type' => 'token-type' }
end
```

### コントローラーの設定
application_controllerに以下の記述を追加させることで、`devise_token_auth`のコントローラーを適用させます。
`configure_permitted_parameters`の部分は、strong_parameterを変更しています。今回は新規登録の時に名前も登録したいので、:nameパラメーターを許可します。ちなみにデフォでは**メールアドレス**と**パスワード**の２つです。

```ruby:controllers/application_controller.rb
class ApplicationController < ActionController::Base
        include DeviseTokenAuth::Concerns::SetUserByToken
        skip_before_action :verify_authenticity_token
        before_action :configure_permitted_parameters, if: :devise_controller?

        def configure_permitted_parameters
                devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
        end
end
```

最後にmigrationをしてRails側のセットアップは完了です。

```
$: docker compose run back rails db:migrate
```

##フロントサイド

###authモジュールの追加
```
$: docker-compose run front yarn add @nuxtjs/auth
```
インストールが完了したら、configのmodule内に追加します。

```js:nuxt.config.js
  modules: [
    'bootstrap-vue/nuxt',
    '@nuxtjs/axios',
    '@nuxtjs/auth',
  ],
```

###axiosの設定
`baseURL`は、axiosがHTTPリクエストを送信時のベースとなるURLを指定します。
Railsの立ち上げホストであるlocalhost:3000を指定します。

```js:nuxt.config.js
  axios: {
    baseURL: "http://localhost:3000"
  },
```
devise_token_authは通信時に`access-token`、`client`、`uid`を用いてユーザー認証を行います。その為、axiosでRailsAPIと送受信をする際に、これらのパラメーターをセットしておく必要があります。
pluginsディレクトリ配下にaxios.jsを作成して下さい。

```javascript:plugins/axios.js
export default function({ $axios }) {
  $axios.onRequest(config => {
    config.headers.client = window.localStorage.getItem("client")
    config.headers["access-token"] = window.localStorage.getItem("access-token")
    config.headers.uid = window.localStorage.getItem("uid")
    config.headers["token-type"] = window.localStorage.getItem("token-type")
  })

  $axios.onResponse(response => {
    if (response.headers.client) {
      localStorage.setItem("access-token", response.headers["access-token"])
      localStorage.setItem("client", response.headers.client)
      localStorage.setItem("uid", response.headers.uid)
      localStorage.setItem("token-type", response.headers["token-type"])
    }
  })
}
```

configのpluginsに以下を追加し、いま作成したプラグインを適用させます。

```js:nuxt.config.js
  plugins: [
    '~/plugins/axios.js'
  ],
```

###authの設定

```js:nuxt.config.js
  auth: {
    redirect: {
      login: '/login', //middleware:authを設定したURLにアクセスがあった場合の、リダイレクト先。
      logout: '/', //ログアウト後のリダイレクト先
      callback: false,
      home: '/' ///ログイン後のリダイレクト先。
     },
    strategies: {
      local: {
        endpoints: {
          //ログイン処理に関する設定
          login: { url: '/api/auth/sign_in', method: 'post',propertyName: 'access_token'}, 
          //ログアウト処理に関する設定
          logout: { url: '/api/auth/sign_out', method: 'delete' },
          //ログイン時にユーザー情報を保存するか。
          user: false 
         },
       }
     },
   },
```

続いて認証に必要な各ページを作成していきます。
**ホームページ**

```vue:index.vue
<template>
  <b-container>
    <b-col offset-md="1" md="10" class="mt-3">
        <b-jumbotron class="pb-5">
          <template #header>Hello World!</template>
          <div v-if="this.$auth.loggedIn">
            <h2>ログイン済み</h2>
          </div>
          <div v-if="!this.$auth.loggedIn">
            <h2>未ログイン</h2>
          </div>
          <hr class="my-4">
          <b-button v-if="!this.$auth.loggedIn" variant="primary" to="/signup">サインアップ</b-button>
          <b-button v-if="!this.$auth.loggedIn" variant="info" to="/login">ログイン</b-button>
          <b-button v-if="this.$auth.loggedIn" variant="success" to="/update">アカウント情報変更</b-button>
          <b-button v-if="this.$auth.loggedIn" variant="danger" @click="logout">ログアウト</b-button>
        </b-jumbotron>
    </b-col>
  </b-container>
</template>

<script>
  export default({
    data: function () {
      return {
      }
    },
    methods: {
      async logout() {
        await this.$auth.logout()
        .then( 
          ()=>{
            localStorage.removeItem("access-token")
            localStorage.removeItem("client")
            localStorage.removeItem("uid")
            localStorage.removeItem("token-type")
          }
        )
      }
    },
  })
</script>

<style></style>
```

いくつか補足。
- `this.$auth.loggedIn`はログイン済みがどうかを真偽値で返します。
- `this.$auth.logout`は先ほど設定したloguoutの処理、即ち`http://localhost:3000/api/sign_out`に`delete`リクエストを送り、ルートページにリダイレクトします。
- ログアウト時にlocalStorageに保存されている`access-token`等のアイテムを削除します。

**非ログイン時**
![スクリーンショット 2021-06-22 12.14.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/f85205d2-7207-5b0d-bff5-d015f11ad007.png)


**ログイン後**
![スクリーンショット 2021-06-22 12.54.56（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/6e92dd3d-72cc-8826-ccc4-8e78ede816f2.png)


**サインアップページ**

```vue:signup.vue
<template> 
  <b-container> 
    <b-col offset-md="1" md="10" class="mt-3">
      <h3 class="text-center">登録ページ</h3>

      <Notification :message="error" v-if="error" class="mb-4 pb-3" />

      <b-form @submit.prevent="signup">
        <b-form-group label="名前:">
          <b-form-input placeholder="Enter your nickname" required v-model="name" type="text"></b-form-input>
        </b-form-group>
        <b-form-group label="メールアドレス:">
          <b-form-input placeholder="Enter email" required v-model="email" type="email"></b-form-input>
        </b-form-group>
        <b-form-group label="パスワード:">
          <b-form-input placeholder="Enter password" required v-model="password" type="password"></b-form-input>
        </b-form-group>
        <b-form-group label="パスワード確認用:">
          <b-form-input placeholder="password confirmation" required v-model="password_confirmation" type="password"></b-form-input>
        </b-form-group>
        <b-button block type="submit" variant="primary">Submit</b-button>
      </b-form>
    </b-col>
  </b-container>
</template>

<script>

export default{
  data: function () {
    return {
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
        error: null
    }
  },
  methods: {
    async signup() {
      try{
        await this.$axios.post('/api/auth',{
            name: this.name,
            email: this.email,
            password: this.password,
            password_confirmation: this.password_confirmation
        })    
        await this.$auth.loginWith('local', {
          data: {
            password: this.password,
            email: this.email
          },
        })    
      }catch(e){
        this.error = e.response.data.errors.full_messages
      }
    }
  }
}
</script>

<style></style>
```

- 送信ボタンが押されると、まずは/api/authにpostリクエストを送信し、新規ユーザーの登録を行います。続けてユーザー登録に成功した場合、loginWithメソッドを使ってログイン処理を実行します。
- ログインに成功するとプラグインに記述した処理が実行され、`access_token`等のアイテムがlocalStorageに保存されます。

- `<Notification :message="error" v-if="error" class="mb-4 pb-3" />`
これはリクエストが失敗した場合、エラーメッセージを表示する用のコンポーネントを呼び出しています。エラー内容を`:message`を通じて渡しています。

componentsディレクトリ配下にNotification.vueを作成します。

```vue:components/Notification.vue
<template>
  <b-alert show variant="danger">
    <div v-for="m in message" :key="m.id">
      <span>{{ m }}</span>
    </div>
  </b-alert>
</template>

<script>
export default {
  name: 'Notification',
  props: ['message']
}
</script>
```

こんな感じ
![スクリーンショット 2021-06-22 12.45.39（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295733/abf9e9c6-4315-2924-5f82-9dd38cd9dfc8.png)



**ログインページ**

```vue:login.vue
<template> 
    <b-container> 
      <b-col offset-md="1" md="10" class="mt-3">
        <h3 class = "text-center">ログイン</h3>

          <Notification :message="error" v-if="error" class="mb-4 pb-3" />

          <b-form @submit.prevent="login">
            <b-form-group label="メールアドレス:">
              <b-form-input placeholder="Enter email" required v-model="email" type="email"></b-form-input>
            </b-form-group>
            <b-form-group label="パスワード:">
              <b-form-input placeholder="Enter password" required v-model="password" type="password"></b-form-input>
            </b-form-group>
            <b-button block type="submit" variant="primary">送信</b-button>
          </b-form>
      </b-col>
    </b-container>
</template>

<script>
  export default {
    data: function () {
      return {
        email: '',
        password: '',
        error: null,
      }
    },
    methods: {
      async login() {
          await this.$auth.loginWith('local', {
            data: {
              password: this.password,
              email: this.email
            }
          })
          .then(
            (response) => {
            },
            (error) => {
              this.error = error.response.data.errors
            }
          )
      }
    }
  }
</script>

<style></style>
```

**ユーザー情報変更ページ**

```vue:update.vue
<template> 
  <b-container> 
    <b-col offset-md="1" md="10" class="mt-3">
      <h3 class = "form-title text-center">ユーザー情報変更</h3>
      <Notification :message="error" v-if="error" class="mb-4 pb-3" />

      <b-form @submit.prevent="update">
        <b-form-group label="名前:">
          <b-form-input placeholder="Enter your nickname" required v-model="name" type="text"></b-form-input>
        </b-form-group>
        <b-form-group label="メールアドレス:">
          <b-form-input placeholder="Enter email" required v-model="email" type="email"></b-form-input>
        </b-form-group>
        <b-form-group label="パスワード:">
          <b-form-input placeholder="Enter password" required v-model="password" type="password"></b-form-input>
        </b-form-group>
        <b-form-group label="パスワード確認用:">
          <b-form-input placeholder="password confirmation" required v-model="password_confirmation" type="password"></b-form-input>
        </b-form-group>
        <b-button block type="submit" variant="primary">Submit</b-button>
      </b-form>
    </b-col>
  </b-container>
</template>

<script>

export default{
  middleware: 'auth',
  data: function () {
    return {
        name: '',
        email: '',
        password: '',
        password_confirmation: '',
        error: null
    }
  },
  methods: {
    async update() {
      try{
        await this.$axios.$put('/api/auth',{
            name: this.name,
            email: this.email,
            password: this.password,
            password_confirmation: this.password_confirmation
        })
        this.$router.push('/')
      }catch(e){
        this.error = e.response.data.errors.full_messages
      }
    }
  }
}
</script>

<style></style>
```
`middleware: 'auth'`はこのページへのアクセスをログイン済みのユーザーのみに制限します。

##終わりに
以上でログイン機能の実装は完了です、お疲れ様でした。
最後に開発の役に立つGoogleアプリを2点紹介して終わります。

**Advanced REST client**
GETやPOSTなどのhttpリクエストの通信をチェックしてくれるツール。

https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo/details?hl=ja-JP

**Vue.js devtools**
Googleブラウザ上でVuexの状態を確認できるツール。

https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=ja