Reactアプリケーションにログイン認証を追加する方法
著者は、 Creative Commons を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
多くのWebアプリケーションは、パブリックページとプライベートページが混在しています。 パブリックページは誰でも利用できますが、プライベートページはユーザーログインが必要です。 authentication を使用して、どのユーザーがどのページにアクセスできるかを管理できます。 React アプリケーションは、ユーザーがログインする前にプライベートページにアクセスしようとする状況を処理する必要があり、ユーザーが正常に認証されたらログイン情報を保存する必要があります。
このチュートリアルでは、トークンベースの認証システムを使用してReactアプリケーションを作成します。 ユーザートークンを返すモックAPIを作成し、トークンを取得するログインページを作成し、ユーザーを再ルーティングせずに認証を確認します。 ユーザーが認証されていない場合は、ユーザーがログインして、専用のログインページに移動せずに続行できるようにする機会を提供します。 アプリケーションを構築するときは、トークンを保存するためのさまざまな方法を検討し、セキュリティを学習し、各アプローチのトレードオフを経験します。 このチュートリアルでは、localStorageおよびsessionStorageにトークンを保存することに焦点を当てます。
このチュートリアルを終了するまでに、Reactアプリケーションに認証を追加し、ログインおよびトークンストレージ戦略を完全なユーザーワークフローに統合できるようになります。
前提条件
- Node.jsを実行する開発環境が必要になります。 このチュートリアルは、Node.jsバージョン10.22.0およびnpmバージョン6.14.6でテストされました。 これをmacOSまたはUbuntu18.04にインストールするには、Node.jsをインストールしてmacOSにローカル開発環境を作成する方法またはのPPAを使用したインストール]セクションの手順に従います。 Ubuntu18.04にNode.jsをインストールする方法。
- Create React App でセットアップされたReact開発環境で、不要なボイラープレートが削除されています。 これを設定するには、ステップ1 —Reactクラスコンポーネントの状態を管理する方法のチュートリアルの空のプロジェクトを作成します。 このチュートリアルでは、プロジェクト名として
auth-tutorial
を使用します。 - Reactを使用してAPIからデータをフェッチします。 ReactのuseEffectフックを使用してWebAPIを呼び出す方法でAPIの操作について学ぶことができます。
- また、JavaScript、HTML、およびCSSの基本的な知識も必要です。これらは、 HTMLシリーズでWebサイトを構築する方法、 CSSでHTMLをスタイル設定する方法、およびJavaScriptでコーディングする方法。
ステップ1—ログインページを作成する
このステップでは、アプリケーションのログインページを作成します。 まず、 React Router をインストールし、完全なアプリケーションを表すコンポーネントを作成します。 次に、ユーザーが新しいページにリダイレクトされることなくアプリケーションにログインできるように、任意のルートでログインページをレンダリングします。
この手順を完了すると、ユーザーがアプリケーションにログインしていないときにログインページを表示する基本的なアプリケーションができあがります。
まず、npm
を使用してreactルーターをインストールします。 2つの異なるバージョンがあります。ReactNativeで使用するWebバージョンとネイティブバージョンです。 Webバージョンをインストールします。
npm install react-router-dom
パッケージがインストールされ、インストールが完了するとメッセージが表示されます。 メッセージは若干異なる場合があります。
Output... + react-router-dom@5.2.0 added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s ...
次に、Dashboard
とPreferences
という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
次に、次の強調表示されたコードを追加して、Dashboard
とPreferences
をインポートします。
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
からBrowserRouter
、Switch
、および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;
アプリケーションのテンプレートとして機能するwrapper
のclassName
と<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
のクラスと20px
のpadding
に置き換えます。
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-wrapper
のclassName
で<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
flex
のdisplay
を追加し、flex-direction
をcolumn
に設定して要素を垂直に配置し、 [を追加して、コンポーネントをページの中央に配置します。 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
を呼び出して、戻り値をtoken
とsetToken
に設定します。
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... + cors@2.8.5 + express@4.17.1 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
をインポートしてから、app
でuse
メソッドを呼び出してアプリケーションに追加します。
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
という新しいpropをLogin
コンポーネントに渡しました。 新しいプロップから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 }
次に、ローカル状態を作成して、Username
とPassword
をキャプチャします。 データを手動で設定する必要がないため、<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)攻撃に対して脆弱です。 危険なのは、悪意のあるユーザーがコードをアプリケーションにロードできる場合、localStorage
、sessionStorage
、およびアプリケーションからもアクセス可能なすべてのCookieにアクセスできることです。 非メモリストレージ方式の利点は、ユーザーエクスペリエンスを向上させるために、ユーザーがログインする必要がある回数を減らすことができることです。
このチュートリアルでは、sessionStorage
とlocalStorage
について説明します。これらは、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
を配置し、useState
をgetToken
で初期化します。 これにより、トークンがフェッチされ、初期状態として設定されます。
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
およびsaveToken
が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); }; return { setToken: saveToken, token } }
ファイルを保存して閉じます。
次に、App.js
を開きます。
nano src/components/App/App.js
getToken
およびsetToken
関数を削除します。 次に、useToken
をインポートし、setToken
とtoken
の値を破棄する関数を呼び出します。 フックを使用しなくなったため、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シリーズのコーディング方法ページに戻ってください。