🌄

[Django REST Framework] [React] simpleJWTによるユーザー認証(2)

2022/01/21に公開

実行環境

MacOS BigSur -- 11.2.1
Python3 -- 3.8.2
Django -- 3.1.7
djangorestframework -- 3.12.2
djoser -- 2.1.0
djangorestframework-simplejwt -- 4.6.0
npm -- 6.14.4
react -- 17.0.1
react-dom -- 17.0.1
axios -- 0.21.1
react-cookie -- 4.0.3
react-hook-form -- 7.0.0

DRFとReactによるアプリケーションでJWT認証を実装したい

前回の記事⇨
https://qiita.com/kachuno9/items/1fa592093c0fd7074aa2
でDRFによるバックエンドにおけるJWT認証の実装は終わっているのでその続きからです。
今回はReactによるフロントエンドの部分を実装しました。

個人的にはReact側の実装の方がはるかに難しく、分からないことがいっぱいあり、Qiitaの質問で沢山の方々に助けられて実装できたので、その手順を残したいと思います。

認証方法

認証方法の選定については、前回の記事に詳しく書いています。
今回はJWTをCookieに保存する方法で実装しました。

必要なライブラリのインストール

今回必要になる各ライブラリを以下の様にインストールします。(React)

$ npm install react-cookie
$ npm install react-hook-form

これらはCookieを扱うためのライブラリと、ReactHookのフォームのためのライブラリです。

React(フロントエンド)

Default.js

DefaultコンポーネントでルーティングやAPIURLの設定を行います。

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Header from '../Header';
import Footer from '../Footer';
import SignUp from './SignUp';
import Login from './Login';
import Trend from './Trend';
import MyPage from './MyPage';
import PostDetail from './PostDetail';
import Top from './Top';
import Logout from './Logout';

//APIURL
export const apiURL = 'http://localhost:8000/api/v1/';

class Default extends React.Component {
    render() {
        return (
            <div>
              <Header />
              <div className="main">
                  <Switch>
                      <Route exact path="/" component={Top} />
                      <Route exact path="/signup" component={SignUp} />
                      <Route exact path="/login" component={Login} />
                      <Route exact path="/logout" component={Logout} />
                      <Route exact path="/trend" component={Trend} />
                      <Route exact path="/mypage" component={MyPage} />
                      <Route exact path="/post/:id" component={PostDetail} />
                      <Route render={() => <p>not found!.</p>} />
                  </Switch>
              </div>
              <Footer />
            </div>
        );
    }
}

export default Default;

Login.js

Loginコンポーネントでログイン処理を行います。

import React, { useState, useEffect, useRef } from 'react';
import { useCookies } from 'react-cookie';
import axios from 'axios';
import { useForm } from "react-hook-form";
import  { useHistory } from 'react-router-dom';
import { apiURL } from './Default';

const Login = (props) => {
    const history = useHistory();

    const [cookies, setCookie] = useCookies();
    const { register, handleSubmit, watch, errors } = useForm();

    const getJwt = async (data) =>{
        console.log(data)
        await axios.post(`${apiURL}auth/jwt/create/`,
          {
            email:data.email,
            password:data.password,
          },
        )
        .then(function (response) {
          console.log(response.data.access)
          setCookie('accesstoken', response.data.access, { path: '/' }, { httpOnly: true });
          setCookie('refreshtoken', response.data.refresh, { path: '/' }, { httpOnly: true });
          history.push('/');
        })
        .catch(err => {
            console.log("miss");
            alert("EmailかPasswordが違います");
        });
      };

    return (
        <div className="top-wrapper">
          <div class="login">
            <h3>Login</h3>
          </div>
          <div class="login-block">
            <form onSubmit={handleSubmit(getJwt)}>
              <label for="email">Email:</label>
              <input className='form-control' {...register('email')} />
              <label for="password">PassWord:</label>
              <input className='form-control' type="password" {...register('password', { required: true })} />
              <input className='btn btn-secondary' type="submit" value="ログイン" />
            </form>
          </div>
        </div>
    );
  }

  export default Login;

ログインページはこの様になっています。
スクリーンショット 2021-04-10 19.33.35.png

正しいEmail、Passwordを入力することでホーム画面に遷移し、開発者ツールでCookieを確認すると"accesstoken"と"refreshtoken"が追加されていることが確認できます。

useCookies

const [cookies, setCookie] = useCookies();

setCookie('accesstoken', response.data.access, { path: '/' }, { httpOnly: true });
setCookie('refreshtoken', response.data.refresh, { path: '/' }, { httpOnly: true });

React HookのuseCookiesを用いて取得したトークンをCookieに保存する処理を行なっています。
*ちなみに、Cookieを削除したい場合、同様にremoveCookieを使えますが、setCookieが必要無い場合にも、3要素の配列で宣言する必要がある様です。

const [cookies, setCookie, removeCookie] = useCookies();

react-hook-form

form の部分で取得したemail,passwordを用いてJWTを発行してもらう仕様になっていますが、このreact-hook-formのバージョンによって書き方が異なることで大分つまずきました。
Qiitaの質問で教えていただいたのですが、react-hook-formのV7から書き方が大幅に変わっており、以下の様な書き方はエラーを吐きます。

<input className='form-control' name="email" ref={register} />
<input className='form-control' name="password" type="password" ref={register({ required: true })} />

V7からの書き方は以下の様に変更しなければなりませんでした。

<input className='form-control' {...register('email')} />
<input className='form-control' type="password" {...register('password', { required: true })} />

具体的にはこちらの通りです。
https://react-hook-form.com/migrate-v6-to-v7/

トークンをヘッダーに追加してAPI接続

ここまでで、
① Email,Passwordを用いて、APIからJWTトークン(access,refresh)を取得
② トークンをCookieに保存

まで完了しました。あとは、③だけです。
③ ヘッダーにトークンを追加してAPI接続

これについては、axiosを用いてデータを取得する際に、axios.getメソッド内で以下の様にheadersを追加すると、DRF側でパーミッション設定がIsAuthenticatedのデータも取得できる様になります

import Cookies from 'universal-cookie';

const cookies = new Cookies();
省略

headers: {
          'Content-Type': 'application/json',
          'Authorization': `JWT ${cookies.get('accesstoken')}`
         }

これでDRFとReactによるJWT認証の実装をある程度完了できました。まだまだセキュリティ面での対策や、UXについての検討など課題は沢山ありますが、どんどん進めていきたいと思います。^^

参考

以下のページが非常に分かりやすく、参考にさせていただきました。

Discussion