devise-token-authを用いたユーザー登録機能の作成
背景
個人開発で rails APIモードでログイン機能を実装したいと思い、認証方法を有するgemを探し、devise-token-authというgemを見つけました。
認証の仕組みを理解するのに時間がかかりましたが、自分なりにわかりやすくまとめたので、これからトークンベース認証を用いた実装をしたい方やrails APIモードで認証機能を実装するため devise-token-auth を用いた実装をしたい方の参考にしていただけると幸いです。
そもそも「認証」と「認可」とは?
「認証」は相手が誰であるのか確認・特定することで、「認可」は特定条件下において、対象物を利用可能にする権限を与えることです。
認証方法には数種類あり、その中の1つに「トークンベース認証」という認証方法があります。
これは、この後説明する devise-token-auth でも用いられています。
トークンベース認証とは?
ユーザーの認証情報をサーバー側で管理し、ユーザーが認証に成功するとサーバーから発行されるトークンを利用してユーザーを認証する(対象物を利用可能にする権限を与える)方法のこと。
トークンを確認することでユーザーの同一性を確認することができる。
仕組みとしては下記の通りです。
- ユーザーがログイン情報(アドレスやパスワード)を送信すると、サーバーはユーザーの認証情報を検証します。
- 認証に成功すると、サーバーはトークンを生成し、ユーザーに返します。
- ユーザーはトークンを使用してサーバーにアクセスします。このとき、HTTPヘッダーのAuthorizationフィールド(ユーザーが使用しているOSやブラウザを表すユーザーエージェントがサーバーからの認証を受けるための証明書のこと)にトークンを含めます。
- サーバーは受信したトークンを検証し、トークンが有効であれば、ユーザーを認証します。
(認証成功後にrails APIを叩くことができるといったイメージだと思います。)
次に、今回認証機能を実装するために使用した devise-token-auth というgemの概要について説明します。
devise-token-auth とは?
railsの認証ライブラリであるDeviseを拡張して、API用のトークンベース認証(サーバから生成されたトークンによりそのユーザーが誰であるのか確認・特定すること)を実現することができるGemです。
ドキュメント
rails APIモードで認証機能を実装したいとなったときに、deviseではなくdevise-token-authを用いる理由は下記の通りです。
①APIに特化した機能が備わっていること
②複数の認証方法を組み合わせることができ、ログイン方法に柔軟性があること
③クライアント側に連携させることで、devise-token-authの機能をAPIでもクライアント側でも両方使用することができること
devise-token-authの導入方法などは、下記の参考の部分にまとめたのでそちらを参考にしていただけると幸いです。
ユーザー登録機能の実装方法
今回は、devise-token-authを用いたユーザー登録機能の実装方法について紹介します。
(記事作成時にログイン機能も紹介しようと考えましたが、記事が全体的に長めになってしまったので、次回以降で紹介いたします。)
作成の方針は下記の通りです。
-
フロントエンド側(react側)で、ユーザーの登録情報(Eメール、パスワード、パスワード確認、名前など)を入力し、サーバーにPOSTリクエストを送信します。
-
サーバー側(rails側)で、入力された情報を検証します。
-
入力された情報が有効である場合、
User
モデルのインスタンスを作成し、save
メソッドを呼び出してデータベースに保存します。 -
保存が成功した場合、devise-token-authは自動的にユーザーにトークンを発行します。
これは、uid(ユーザー識別するための固有のID)、access-token(各リクエストのユーザーのパスワード)、client(複数の同時セッションの管理) の3つの値のセットとして表されます。
-
フロントエンド側で、このトークンをローカルストレージなどに保存し、今後のリクエストに使用します。
以上の流れで、Devise Token Authを使用してユーザー作成機能を実装することができます。
重要なのは、トークンを発行し、クライアントに返すことです。
これにより、ユーザーは以降のリクエストで認証され、保護されたエンドポイントにアクセスすることができます。
上記の方針をもとに、devise-token-authを用いてユーザー作成機能を実装してみました。
frontend/src/components/sign_up/SignUp.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from "react-redux";
import { useNavigate } from 'react-router-dom';
import { Field, reduxForm } from 'redux-form';
import { Button } from '@mui/material';
// api
import { userDataCreate } from '../../apis/signUp';
// components
import { renderTextField } from '../modules/renderTextField';
const SignUp = (props) => {
const { handleSubmit } = props;
const form = useSelector(state => state.form);
const values = form && form.signUpForm && form.signUpForm.values;
const pageFlag = useSelector(state => state.pageFlag)
const dispatch = useDispatch()
const navigate = useNavigate();
// ユーザーの新規登録
const userDataSubmit = async(e) => {
e.preventDefault();
const params = {
name: values.name,
email: values.email,
password: values.password,
password_confirmation: values.password_confirmation
}
// 入力されたユーザー情報を送信
await userDataCreate(params, dispatch)
};
// ユーザー情報作成後、登録完了ページ遷移
useEffect(() => {
if (pageFlag.flag) {
navigate('/sign_up_confirmation');
}
}, [pageFlag.flag, navigate]);
return (
<>
<br></br>
<div>
ユーザー登録ページです。
</div>
<br></br>
<form onSubmit={handleSubmit}>
<br></br>
<br></br>
<Field
name="name"
component={renderTextField}
label="名前"
placeholder="名前を入力してください。"
style={{ width: 280 }}
/>
<br></br>
<br></br>
<Field
name="email"
component={renderTextField}
label="メールアドレス"
placeholder="メールアドレスを入力してください。"
style={{ width: 280 }}
/>
<br></br>
<br></br>
<Field
name="password"
component={renderTextField}
label="パスワード"
placeholder="パスワードを入力してください。"
style={{ width: 280 }}
/>
<br></br>
<br></br>
<Field
name="password-confirmation"
component={renderTextField}
label="パスワード確認"
placeholder="もう一度パスワードを入力してください。"
style={{ width: 280 }}
/>
<br></br>
<br></br>
<Button variant="outlined" onClick={userDataSubmit}>新規登録</Button>
</form>
</>
);
};
export default reduxForm({
form: 'signUpForm',
})(SignUp);
上記では、ユーザー作成するための入力フォームを持つcomponentを作成し、react側からrails側へ入力フォームに入力されたデータを送信しています。
frontend/src/apis/signUp.js
import axios from 'axios';
import { usersIndex } from '../urls/index'
import { dispatchUserData } from '../reducks/reducers/user';
import { pageTransitionFlag } from '../reducks/reducers/common';
export const userDataCreate = async(params, dispatch) => {
await axios.post(usersIndex, params)
.then(data => {
console.log(data)
// 取得したresponseより、アクセストークンなどを変数に代入
const accessToken = data.headers['access-token'];
const client = data.headers['client'];
const uid = data.headers['uid'];
// 認証情報をlocalStorageに保存する
localStorage.setItem('access-token', accessToken);
localStorage.setItem('client', client);
localStorage.setItem('uid', uid);
}).catch(error => {
console.log(error);
});
};
frontend/src/urls/index.js
const DEFAULT_API_LOCALHOST = 'http://localhost:3010/api/v1'
export const usersIndex = `${DEFAULT_API_LOCALHOST}/auth`
上記では、ユーザー作成するためのrails APIをPOSTリクエストで実行し、入力フォームから送信されたデータをサーバ側(rails側)へ送信しています。
そして、ユーザーデータが作成されrails 側から返ってきたデータ(data)の中にあるheadersからaccess-token、client、uidを抽出し、localStorageへ保存させます。
このようにすることで、次回以降 rails APIを叩いて何かリクエストを送るときに、これらの情報をもとにrails APIにアクセスできるかどうかを判断することができます。
config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
registrations: 'api/v1/auth/registrations'
}
end
end
end
上記のコードでは、ルーティングを設定しています。
namespace
メソッドを使用して api/v1
以下のルートに対して名前空間を定義し、その中に mount_devise_token_auth_for
メソッドを使用して、 'User'
モデルを使用して /auth
にDevise Token Authの認証ルートをマウントしています。
また、controllers
オプションを使用して、コントローラーのカスタマイズを指定しています。registrations
コントローラーは、 api/v1/auth/registrations
にマウントされます。
app/controllers/api/v1/auth/registrations_controller.rb
class Api::V1::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController
# DeviseTokenAuth::RegistrationsControllerで定義されたcreateメソッド実行後にset_token_infoメソッドを実行
after_action :set_token_info, only: [:create]
private
def sign_up_params
params.permit(:email, :password, :password_confirmation, :name)
end
# ユーザーデータが保存されているか確認し、保存されていれば新しいトークンを作成。
# ヘッダーに access-token と client を設定
def set_token_info
return unless @resource.persisted?
token = @resource.create_new_auth_token
response.set_header('access-token', token['access-token'])
response.set_header('client', token['client'])
end
end
上記のコードは、after_action
コールバックを使用して、ユーザーが作成された後にトークン情報をレスポンスヘッダーに追加するための set_token_info
メソッドを実行しています。
set_token_info
メソッドは、@resource
(ユーザー情報)がデータベースに保存された場合にのみ、新しいトークンを作成し、レスポンスヘッダーに access-token
と client
を設定します。
sign_up_params
メソッドは、ユーザーがAPIに登録するために必要なパラメータを受け取り、必要に応じて許可されたパラメータを返します。
上記のようにreact側からrails側を実装することで、localStorageにユーザーごとのaccess-token、client、uid の情報を持たせることができました。
rails APIを叩くときにこれらの情報をもとに認証操作を行うことで、rails APIへのアクセス(rails APIへのリクエスト)に制限をかけることができるようになります。
まとめ
今回は、トークンベース認証やdevise-token-authを用いたユーザー登録機能の実装方法などを紹介してみました。
devise-token-authは使ったことがなく、ユーザー作成やログイン機能など実際に実装している記事が少なく、実装するのに時間がかかり、とても苦労しました。
この後は、devise-token-authを用いてログイン機能も実装したいと考えているので、実装できたら記事などにまとめ、ログイン機能を実装したい方の参考になれば幸いです。
参考
Discussion