Reactアプリケーションにログイン認証を追加する方法

提供:Dev Guides
移動先:案内検索

著者は、 Creative Commons を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

多くのWebアプリケーションは、パブリックページとプライベートページが混在しています。 パブリックページは誰でも利用できますが、プライベートページはユーザーログインが必要です。 authentication を使用して、どのユーザーがどのページにアクセスできるかを管理できます。 React アプリケーションは、ユーザーがログインする前にプライベートページにアクセスしようとする状況を処理する必要があり、ユーザーが正常に認証されたらログイン情報を保存する必要があります。

このチュートリアルでは、トークンベースの認証システムを使用してReactアプリケーションを作成します。 ユーザートークンを返すモックAPIを作成し、トークンを取得するログインページを作成し、ユーザーを再ルーティングせずに認証を確認します。 ユーザーが認証されていない場合は、ユーザーがログインして、専用のログインページに移動せずに続行できるようにする機会を提供します。 アプリケーションを構築するときは、トークンを保存するためのさまざまな方法を検討し、セキュリティを学習し、各アプローチのトレードオフを経験します。 このチュートリアルでは、localStorageおよびsessionStorageにトークンを保存することに焦点を当てます。

このチュートリアルを終了するまでに、Reactアプリケーションに認証を追加し、ログインおよびトークンストレージ戦略を完全なユーザーワークフローに統合できるようになります。

前提条件

ステップ1—ログインページを作成する

このステップでは、アプリケーションのログインページを作成します。 まず、 React Router をインストールし、完全なアプリケーションを表すコンポーネントを作成します。 次に、ユーザーが新しいページにリダイレクトされることなくアプリケーションにログインできるように、任意のルートでログインページをレンダリングします。

この手順を完了すると、ユーザーがアプリケーションにログインしていないときにログインページを表示する基本的なアプリケーションができあがります。

まず、npmを使用してreactルーターをインストールします。 2つの異なるバージョンがあります。ReactNativeで使用するWebバージョンとネイティブバージョンです。 Webバージョンをインストールします。

npm install react-router-dom

パッケージがインストールされ、インストールが完了するとメッセージが表示されます。 メッセージは若干異なる場合があります。

Output...
+ [email protected]
added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
...

次に、DashboardPreferencesという2つのコンポーネントを作成して、プライベートページとして機能させます。 これらは、ユーザーがアプリケーションに正常にログインするまで表示されないコンポーネントを表します。

まず、ディレクトリを作成します。

mkdir src/components/Dashboard
mkdir src/components/Preferences

次に、テキストエディタでDashboard.jsを開きます。 このチュートリアルでは、nanoを使用します。

nano src/components/Dashboard/Dashboard.js

Dashboard.jsの中に、Dashboardの内容の<h2>タグを追加します。

auth-tutorial / src / components / Dashboard / Dashboard.js

import React from 'react';

export default function Dashboard() {
  return(
    <h2>Dashboard</h2>
  );
}

ファイルを保存して閉じます。

Preferencesについても同じ手順を繰り返します。 コンポーネントを開きます。

nano src/components/Preferences/Preferences.js

コンテンツを追加します。

auth-tutorial / src / components / Preferences / Preferences.js

import React from 'react';

export default function Preferences() {
  return(
    <h2>Preferences</h2>
  );
}

ファイルを保存して閉じます。

いくつかのコンポーネントができたので、コンポーネントをインポートして、App.js内にルートを作成する必要があります。 Reactアプリケーションでのルーティングの完全な紹介については、チュートリアルReactRouterを使用してReactアプリでルーティングを処理する方法を確認してください。

まず、App.jsを開きます。

nano src/components/App/App.js

次に、次の強調表示されたコードを追加して、DashboardPreferencesをインポートします。

auth-tutorial / src / components / App / App.js

import React from 'react';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <></>
  );
}

export default App;

次に、react-router-domからBrowserRouterSwitch、およびRouteをインポートします。

auth-tutorial / src / components / App / App.js

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <></>
  );
}

export default App;

アプリケーションのテンプレートとして機能するwrapperclassName<h1>タグを使用して、周囲の<div>を追加します。 スタイルを適用できるように、App.cssをインポートしていることを確認してください。

次に、DashboardおよびPreferencesコンポーネントのルートを作成します。 BrowserRouterを追加してから、Switchコンポーネントを子として追加します。 Switchの中に、各コンポーネントにpathを含むRouteを追加します。

チュートリアル/src/components/App/App.js

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

ファイルを保存して閉じます。

最後のステップは、メインの<div>にパディングを追加して、コンポーネントがブラウザーの端に直接配置されないようにすることです。 これを行うには、CSSを変更します。

App.cssを開きます:

nano src/components/App/App.css

内容を.wrapperのクラスと20pxpaddingに置き換えます。

auth-tutorial / src / components / App / App.css

.wrapper {
    padding: 20px;
}

ファイルを保存して閉じます。 これを行うと、ブラウザがリロードされ、基本的なコンポーネントが見つかります。

各ルートを確認してください。 http:// localhost:3000 / dashboard にアクセスすると、ダッシュボードページが表示されます。

ルートは期待どおりに機能していますが、少し問題があります。 ルート/dashboardは保護されたページである必要があり、認証されていないユーザーが表示できないようにする必要があります。 プライベートページを処理するにはさまざまな方法があります。 たとえば、ログインページの新しいルートを作成し、 React Routerを使用して、ユーザーがログインしていない場合にリダイレクトすることができます。 これは優れたアプローチですが、ユーザーはルートを失い、最初に表示したいページに戻る必要があります。

邪魔にならないオプションは、ルートに関係なくログインページを生成することです。 このアプローチでは、ユーザートークンが保存されていない場合にログインページをレンダリングし、ユーザーがログインすると、最初にアクセスしたのと同じルートになります。 つまり、ユーザーが/dashboardにアクセスした場合でも、ログイン後も/dashboardルートを使用します。

まず、Loginコンポーネント用の新しいディレクトリを作成します。

mkdir src/components/Login

次に、テキストエディタでLogin.jsを開きます。

nano src/components/Login/Login.js

ユーザー名とパスワードの送信<button><input>を使用して基本フォームを作成します。 パスワードの入力タイプは必ずpasswordに設定してください。

auth-tutorial / src / components / Login / Login.js

import React from 'react';

export default function Login() {
  return(
    <form>
      <label>
        <p>Username</p>
        <input type="text" />
      </label>
      <label>
        <p>Password</p>
        <input type="password" />
      </label>
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}

Reactのフォームの詳細については、チュートリアルReactでフォームを作成する方法を確認してください。

次に、ユーザーにログインを求める<h1>タグを追加します。 <form><h1>login-wrapperclassName<div>に包みます。 最後に、Login.cssをインポートします。

auth-tutorial / src / components / Login / Login.js

import React from 'react';
import './Login.css';

export default function Login() {
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

ファイルを保存して閉じます。

基本的なLoginコンポーネントができたので、スタイルを追加する必要があります。 Login.cssを開きます:

nano src/components/Login/Login.css

flexdisplayを追加し、flex-directioncolumnに設定して要素を垂直に配置し、 [を追加して、コンポーネントをページの中央に配置します。 X157X]からcenterを使用して、コンポーネントをブラウザーの中央に配置します。

auth-tutorial / src / components / Login / Login.css

.login-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
}

Flexboxの使用の詳細については、CSSFlexboxチートシートを参照してください。

ファイルを保存して閉じます。

最後に、ユーザートークンがない場合は、App.js内にレンダリングする必要があります。 App.jsを開きます:

nano src/components/App/App.js

ステップ3では、トークンを保存するためのオプションについて説明します。 今のところ、 useStateHookを使用してトークンをメモリに保存できます。

reactからuseStateをインポートし、useStateを呼び出して、戻り値をtokensetTokenに設定します。

auth-tutorial / src / components / App / App.js

import React, { useState } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  const [token, setToken] = useState();
  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

Loginコンポーネントをインポートします。 条件文を追加して、tokenが偽の場合にLoginを表示します。

setToken関数をLoginコンポーネントに渡します。

auth-tutorial / src / components / App / App.js

import React, { useState } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function App() {
  const [token, setToken] = useState();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

今のところ、トークンはありません。 次のステップでは、APIを呼び出し、戻り値を使用してトークンを設定します。

ファイルを保存して閉じます。 これを行うと、ブラウザがリロードされ、ログインページが表示されます。 http:// localhost:3000 / dashboard にアクセスすると、トークンがまだ設定されていないため、ログインページが表示されます。

このステップでは、トークンを設定するまで表示されるプライベートコンポーネントとログインコンポーネントを使用してアプリケーションを作成しました。 また、ページを表示するようにルートを構成し、ユーザーがまだアプリケーションにログインしていない場合にすべてのルートでLoginコンポーネントを表示するチェックを追加しました。

次のステップでは、ユーザートークンを返すローカルAPIを作成します。 LoginコンポーネントからAPIを呼び出し、成功するとトークンをメモリに保存します。

ステップ2—トークンAPIを作成する

このステップでは、ユーザートークンをフェッチするためのローカルAPIを作成します。 Node.js を使用してモックAPIを構築し、ユーザートークンを返します。 次に、ログインページからそのAPIを呼び出し、トークンを正常に取得した後にコンポーネントをレンダリングします。 この手順を完了すると、ログインページと保護されたページが機能するアプリケーションが作成され、ログイン後にのみアクセスできるようになります。

トークンを返すバックエンドとして機能するサーバーが必要になります。 Node.jsとExpressWebフレームワークを使用して、サーバーをすばやく作成できます。 Expressサーバーの作成の詳細については、チュートリアルNode.jsの基本的なExpressサーバーを参照してください。

開始するには、expressをインストールします。 サーバーは最終ビルドの要件ではないため、必ずdevDependencyとしてインストールしてください。

corsもインストールする必要があります。 このライブラリは、すべてのルートでクロスオリジンリソースシェアリングを有効にします。

警告:本番アプリケーションのすべてのルートでCORSを有効にしないでください。 これはセキュリティの脆弱性につながる可能性があります。


npm install --save-dev express cors

インストールが完了すると、成功メッセージが表示されます。

Output...
+ [email protected]
+ [email protected]
removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
...

次に、アプリケーションのルートにあるserver.jsという名前の新しいファイルを開きます。 このファイルを/srcディレクトリに追加しないでください。これは、このファイルを最終ビルドの一部にしたくないためです。

nano server.js

expressをインポートし、express()を呼び出して新しいアプリを初期化し、結果をappという変数に保存します。

auth-tutorial / server.js

const express = require('express');
const app = express();

appを作成したら、ミドルウェアとしてcorsを追加します。 まず、corsをインポートしてから、appuseメソッドを呼び出してアプリケーションに追加します。

auth-tutorial / server.js

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

次に、app.useで特定のルートを聞きます。 最初の引数はアプリケーションがリッスンするパスであり、2番目の引数はアプリケーションがパスを提供するときに実行されるコールバック関数です。 コールバックは、リクエストデータと結果を処理するres引数を含むreq引数を取ります。

/loginパスのハンドラーを追加します。 トークンを含むJavaScriptオブジェクトを使用してres.sendを呼び出します。

auth-tutorial / server.js

const express = require('express');
const cors = require('cors')
const app = express();

app.use(cors());

app.use('/login', (req, res) => {
  res.send({
    token: 'test123'
  });
});

最後に、app.listenを使用して、ポート8080でサーバーを実行します。

auth-tutorial / server.js

const express = require('express');
const cors = require('cors')
const app = express();

app.use(cors());

app.use('/login', (req, res) => {
  res.send({
    token: 'test123'
  });
});

app.listen(8080, () => console.log('API is running on http://localhost:8080/login'));

ファイルを保存して閉じます。 新しいターミナルウィンドウまたはタブで、サーバーを起動します。

node server.js

サーバーが起動していることを示す応答を受け取ります。

OutputAPI is running on http://localhost:8080/login

http:// localhost:8080 / login にアクセスすると、JSONオブジェクトが見つかります。

ブラウザでトークンを取得すると、GETリクエストが作成されますが、ログインフォームを送信すると、POSTリクエストが作成されます。 問題ない。 app.useを使用してルートを設定すると、Expressはすべてのリクエストを同じように処理します。 本番アプリケーションでは、より具体的に、各ルートに対して特定のリクエストメソッドのみを許可する必要があります。

APIサーバーを実行しているので、ログインページからリクエストを行う必要があります。 Login.jsを開きます:

nano src/components/Login/Login.js

前の手順では、setTokenという新しいpropLoginコンポーネントに渡しました。 新しいプロップからPropTypeを追加し、 destructurepropsオブジェクトを追加してsetTokenプロップを引き出します。

auth-tutorial / src / components / Login / Login.js

import React from 'react';
import PropTypes from 'prop-types';

import './Login.css';

export default function Login({ setToken }) {
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
}

次に、ローカル状態を作成して、UsernamePasswordをキャプチャします。 データを手動で設定する必要がないため、<inputs>を制御されていないコンポーネントにします。 制御されていないコンポーネントの詳細については、Reactでフォームを作成する方法を参照してください。

auth-tutorial / src / components / Login / Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';

import './Login.css';

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)}/>
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)}/>
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

次に、サーバーに対してPOSTリクエストを行う関数を作成します。 大規模なアプリケーションでは、これらを別のディレクトリに追加します。 この例では、サービスをコンポーネントに直接追加します。 ReactコンポーネントでのAPIの呼び出しの詳細については、チュートリアルReactでuseEffectフックを使用してWebAPIを呼び出す方法を確認してください。

loginUserという非同期関数を作成します。 この関数はcredentialsを引数として取り、POSTオプションを使用してfetchメソッドを呼び出します。

auth-tutorial / src / components / Login / Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
...

最後に、usernameおよびpasswordを使用してloginUserを呼び出すhandleSubmitというフォーム送信ハンドラーを作成します。 setTokenを呼び出すと、正常に結果が得られます。 <form>onSubmitイベントハンドラーを使用してhandleSubmitを呼び出します。

auth-tutorial / src / components / Login / Login.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();

  const handleSubmit = async e => {
    e.preventDefault();
    const token = await loginUser({
      username,
      password
    });
    setToken(token);
  }

  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)} />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

注:完全なアプリケーションでは、Promiseが解決する前にコンポーネントがアンマウントされる状況を処理する必要があります。 詳細については、チュートリアルReactのuseEffectフックを使用してWebAPIを呼び出す方法を確認してください。


ファイルを保存して閉じます。 ローカルAPIがまだ実行されていることを確認してから、ブラウザを開いて http:// localhost:3000 /dashboardにアクセスします。

ダッシュボードの代わりにログインページが表示されます。 フォームに記入して送信すると、Webトークンを受け取り、ダッシュボードのページにリダイレクトされます。

これで、動作するローカルAPIと、ユーザー名とパスワードを使用してトークンを要求するアプリケーションができました。 しかし、まだ問題があります。 トークンは現在、ローカル状態を使用して保存されています。つまり、JavaScriptメモリに保存されています。 新しいウィンドウやタブを開いたり、ページを更新したりすると、トークンが失われ、ユーザーは再度ログインする必要があります。 これについては、次のステップで対処します。

このステップでは、アプリケーションのローカルAPIとログインページを作成しました。 トークンを送信するノードサーバーを作成する方法と、サーバーを呼び出してログインコンポーネントからトークンを保存する方法を学習しました。 次のステップでは、ページの更新またはタブ間でセッションが持続するように、ユーザートークンを保存する方法を学習します。

ステップ3—sessionStorageおよびlocalStorageを使用してユーザートークンを保存する

このステップでは、ユーザートークンを保存します。 さまざまなトークンストレージオプションを実装し、各アプローチのセキュリティへの影響を学習します。 最後に、ユーザーが新しいタブを開いたり、セッションを閉じたりしたときに、さまざまなアプローチによってユーザーエクスペリエンスがどのように変化するかを学習します。

このステップの終わりまでに、アプリケーションの目標に基づいてストレージアプローチを選択できるようになります。

トークンを保存するためのいくつかのオプションがあります。 すべてのオプションにはコストとメリットがあります。 簡単に言うと、オプションは、JavaScriptメモリへの保存、 sessionStorage への保存、 localStorage への保存、cookieへの保存です。 主なトレードオフはセキュリティです。 現在のアプリケーションのメモリの外部に保存されている情報は、クロスサイトスクリプティング(XSS)攻撃に対して脆弱です。 危険なのは、悪意のあるユーザーがコードをアプリケーションにロードできる場合、localStoragesessionStorage、およびアプリケーションからもアクセス可能なすべてのCookieにアクセスできることです。 非メモリストレージ方式の利点は、ユーザーエクスペリエンスを向上させるために、ユーザーがログインする必要がある回数を減らすことができることです。

このチュートリアルでは、sessionStoragelocalStorageについて説明します。これらは、Cookieを使用するよりも最新のものであるためです。

セッションストレージ

メモリ外に保存する利点をテストするには、メモリ内ストレージをsessionStorageに変換します。 App.jsを開きます:

nano src/components/App/App.js

useStateの呼び出しを削除し、setTokenおよびgetTokenという2つの新しい関数を作成します。 次に、getTokenを呼び出し、結果をtokenという変数に割り当てます。

auth-tutorial / src / components / App / App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
}

function getToken() {
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
     ...
    </div>
  );
}

export default App;

同じ関数名と変数名を使用しているため、Loginコンポーネントまたは残りのAppコンポーネントのコードを変更する必要はありません。

setToken内で、setItemメソッドを使用して、userToken引数をsessionStorageに保存します。 このメソッドは、最初の引数としてキーを取り、2番目の引数として文字列を取ります。 つまり、JSON.stringify関数を使用して、userTokenをオブジェクトから文字列に変換する必要があります。 tokenのキーと変換されたオブジェクトを使用してsetItemを呼び出します。

auth-tutorial / src / components / App / App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
  sessionStorage.setItem('token', JSON.stringify(userToken));
}

function getToken() {
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      ...
    </div>
  );
}

export default App;

ファイルを保存します。 これを行うと、ブラウザがリロードされます。 ユーザー名とパスワードを入力して送信すると、ブラウザはログインページを表示しますが、ブラウザのコンソールツールを見ると、トークンがsessionStorageに保存されていることがわかります。 この画像はFirefoxのものですが、Chromeやその他の最新のブラウザでも同じ結果が得られます。

次に、正しいページをレンダリングするためにトークンを取得する必要があります。 getToken関数内で、sessionStorage.getItemを呼び出します。 このメソッドは、引数としてkeyを取り、文字列値を返します。 JSON.parseを使用して文字列をオブジェクトに変換し、tokenの値を返します。

auth-tutorial / src / components / App / App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
  sessionStorage.setItem('token', JSON.stringify(userToken));
}

function getToken() {
  const tokenString = sessionStorage.getItem('token');
  const userToken = JSON.parse(tokenString);
  return userToken?.token
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      ...
    </div>
  );
}

export default App;

tokenプロパティにアクセスするときは、オプションの連鎖演算子?.を使用する必要があります。これは、アプリケーションに最初にアクセスしたとき、sessionStorage.getItem('token')の値がundefinedになるためです。 ]。 プロパティにアクセスしようとすると、エラーが発生します。

ファイルを保存して閉じます。 この場合、すでにトークンが保存されているため、ブラウザが更新されると、プライベートページに移動します。

開発者ツールのストレージタブでトークンを削除するか、開発者コンソールでsessionStorage.clear()と入力して、トークンをクリアします。

今少し問題があります。 ログインすると、ブラウザはトークンを保存しますが、ログインページは引き続き表示されます。

問題は、トークンの取得が成功したことをコードがReactに警告しないことです。 データが変更されたときに再レンダリングをトリガーする状態を設定する必要があります。 Reactのほとんどの問題と同様に、それを解決する方法は複数あります。 最もエレガントで再利用可能なものの1つは、カスタムフックを作成することです。

カスタムトークンフックの作成

カスタムフックは、カスタムロジックをラップする関数です。 カスタムフックは通常、1つ以上の組み込みのReactフックをカスタム実装とともにラップします。 カスタムフックの主な利点は、コンポーネントから実装ロジックを削除して、複数のコンポーネントで再利用できることです。

慣例により、カスタムフックはキーワードuse*で始まります。

AppディレクトリにあるuseToken.jsという名前の新しいファイルを開きます。

nano src/components/App/useToken.js

これは小さなフックになり、App.jsで直接定義すると問題ありません。 ただし、カスタムフックを別のファイルに移動すると、コンポーネントの外部でフックがどのように機能するかがわかります。 このフックを複数のコンポーネントで再利用し始める場合は、別のディレクトリに移動することもできます。

useToken.js内で、reactからuseStateをインポートします。 ファイルにJSXがないため、Reactをインポートする必要がないことに注意してください。 useTokenという関数を作成してエクスポートします。 この関数内で、useStateフックを使用して、token状態とsetToken関数を作成します。

auth-tutorial / src / components / App / useToken.js

import { useState } from 'react';

export default function useToken() {
  const [token, setToken] = useState();

}

次に、getToken関数をuseHookにコピーし、useToken内に配置したため、矢印関数に変換します。 関数を標準の名前付き関数のままにしておくこともできますが、最上位の関数が標準で、内部関数が矢印関数の場合は読みやすくなります。 ただし、各チームは異なります。 1つのスタイルを選択し、それに固執します。

状態宣言の前にgetTokenを配置し、useStategetTokenで初期化します。 これにより、トークンがフェッチされ、初期状態として設定されます。

auth-tutorial / src / components / App / useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };
  const [token, setToken] = useState(getToken());

}

次に、App.jsからsetToken関数をコピーします。 矢印関数に変換し、新しい関数にsaveTokenという名前を付けます。 トークンをsessionStorageに保存することに加えて、setTokenを呼び出して、トークンを状態に保存します。

auth-tutorial / src / components / App / useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    sessionStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };
}

最後に、tokenおよびsaveTokensetTokenプロパティ名に設定されているオブジェクトを返します。 これにより、コンポーネントに同じインターフェイスが提供されます。 値を配列として返すこともできますが、オブジェクトを使用すると、これを別のコンポーネントで再利用する場合に、必要な値のみを非構造化することができます。

auth-tutorial / src / components / App / useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    sessionStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  return {
    setToken: saveToken,
    token
  }
}

ファイルを保存して閉じます。

次に、App.jsを開きます。

nano src/components/App/App.js

getTokenおよびsetToken関数を削除します。 次に、useTokenをインポートし、setTokentokenの値を破棄する関数を呼び出します。 フックを使用しなくなったため、useStateのインポートを削除することもできます。

auth-tutorial / src / components / App / App.js

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import useToken from './useToken';

function App() {

  const { token, setToken } = useToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Switch>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
          <Route path="/preferences">
            <Preferences />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

ファイルを保存して閉じます。 これを行うと、ブラウザが更新され、ログインするとすぐにそのページに移動します。 これは、カスタムフックでuseStateを呼び出しているために発生しています。これにより、コンポーネントの再レンダリングがトリガーされます。

これで、トークンをsessionStorageに保存するためのカスタムフックができました。 これでページを更新でき、ユーザーはログインしたままになります。 ただし、別のタブでアプリケーションを開こうとすると、ユーザーはログアウトされます。 sessionStorageは、特定のウィンドウセッションにのみ属します。 新しいタブではデータを利用できず、アクティブなタブを閉じるとデータが失われます。 タブ間でトークンを保存する場合は、localStorageに変換する必要があります。

localStorageを使用してWindows間でデータを保存する

sessionStorageとは異なり、localStorageはセッション終了後もデータを保存します。 これは、ユーザーが新しいログインなしで複数のウィンドウとタブを開くことができるため、より便利ですが、セキュリティ上の問題がいくつかあります。 ユーザーがコンピューターを共有している場合、ブラウザーを閉じてもアプリケーションにログインしたままになります。 明示的にログアウトするのはユーザーの責任です。 次のユーザーは、ログインしなくてもアプリケーションにすぐにアクセスできます。 これはリスクですが、アプリケーションによっては便利なだけの価値がある場合があります。

localStorageに変換するには、useToken.jsを開きます。

nano src/components/App/useToken.js

次に、sessionStorageのすべての参照をlocalStorageに変更します。 呼び出すメソッドは同じになります。

auth-tutorial / src / components / App / useToken.js

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = localStorage.getItem('token');
    const userToken = JSON.parse(tokenString);
    return userToken?.token
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    localStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  return {
    setToken: saveToken,
    token
  }
}

ファイルを保存します。 これを行うと、ブラウザが更新されます。 localStorageにはまだトークンがないため、再度ログインする必要がありますが、ログインした後は、新しいタブを開いたときにログインしたままになります。

このステップでは、sessionStorageおよびlocalStorageを使用してトークンを保存しました。 また、コンポーネントの再レンダリングをトリガーし、コンポーネントロジックを別の関数に移動するためのカスタムフックを作成しました。 また、sessionStorageおよびlocalStorageが、ログインせずに新しいセッションを開始するユーザーの機能にどのように影響するかについても学びました。

結論

認証は、多くのアプリケーションの重要な要件です。 セキュリティ上の懸念とユーザーエクスペリエンスの組み合わせは恐ろしいものになる可能性がありますが、データの検証とコンポーネントの適切なタイミングでのレンダリングに焦点を当てると、軽量のプロセスになる可能性があります。

各ストレージソリューションには、明確な長所と短所があります。 アプリケーションが進化するにつれて、選択が変わる可能性があります。 コンポーネントロジックを抽象的なカスタムフックに移動することで、既存のコンポーネントを中断することなくリファクタリングできるようになります。

Reactチュートリアルをもっと読みたい場合は、 Reactトピックページを確認するか、React.jsシリーズのコーディング方法ページに戻ってください。