先日書いた Laravel+Vue&PKCEのSPA環境構築メモ
のReact版も作成してみました。
※ Laravel側は全く同じなので割愛し、見出しを合わせて差分のところだけ記載しています
https://github.com/pei-miyapei/laravel-react-spa
(こちらは簡単なAPIの実装とUIフレームワーク(Ant Design)を導入した状態です)
構成
API server コンテナ(SPA バックエンド、ユーザー管理)
(割愛)
SPA クライアントコンテナ(SPA フロントエンド)
- Vite
- React 17
- TypeScript
- react-router
- js-pkce
React+Vite+TypeScript環境の構築
Vue版との違いは、react-tsのテンプレートを使用しているというだけです。
あとはおなじ。
yarn
yarn create vite temp --template react-ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true,
port: 3000,
// https: true,
},
});
SPA側の実装
react-router, js-pkceをインストール
yarn add react-router-dom@6 js-pkce
ルーティングの設定
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { About } from '../views/About';
import { AuthorizationCallback } from '../views/Auth/AuthorizationCallback';
import { Home } from '../views/Home';
export const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</BrowserRouter>
);
};
App.tsxのコンテンツはreact-routerの中身(<router>)のみにしておきます
※ Vue版とファイル構成も合わせているだけで、別にApp.tsxを使う必要はありません
import './App.css';
import { Router } from './routes/Router';
function App() {
return <Router />;
}
export default App;
MasterPage.tsx(Layout)を追加しておく
未認証時に丸ごと非表示にしたいため、
別途レイアウト用のVueを作成してその中にコンテンツページを表示するようにしておきます。
Vue版ではLayout.vueになっています。
※ ant-designを入れた際にLayoutというコンポーネント名が被ったため…
import { Link, Outlet } from 'react-router-dom';
export const MasterPage = () => {
return (
<div id="nav">
<Link to='/'>Home</Link> |
<Link to='/about'>About</Link>
</div>
<Outlet />
);
};
このコンポーネントの入れ子のルーティングは以下のように書けます
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { About } from '../views/About';
import { Home } from '../views/Home';
import { MasterPage } from '../views/MasterPage';
export const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<MasterPage />}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Route>
</Routes>
</BrowserRouter>
);
};
ネストされたルートについての詳細はこちら
https://reactrouter.com/docs/en/v6/getting-started/tutorial#nested-routes
トークンを保持する入れ物
これに関してはCookieなどに保存する場合は不要です。
ここでは変数内に持っているだけという状態で実装するため、その入れ物です。
(タブを閉じたら消えます)
Vue版ではProvide/Injectという機能を使用しました。
Reactにも同様のContextという機能があり、これを使用します。
import { createContext, useContext, useState } from 'react';
class AuthTokens {
constructor(public accessToken = '', public refreshToken = '') {}
}
// ProviderProps
const authProps = (
tokens = new AuthTokens(),
handleSetTokens = (accessToken = '', refreshToken = '') => {}
) => {
const hasToken = () => tokens.accessToken !== '';
return { tokens, handleSetTokens, hasToken };
};
export type AuthProps = ReturnType<typeof authProps>;
// Context
let AuthContext = createContext(authProps());
// Provider
export const AuthProvider = ({ children }: any) => {
const [tokens, setTokens] = useState(new AuthTokens());
const handleSetTokens = (accessToken = '', refreshToken = '') => {
setTokens({ ...tokens, ...new AuthTokens(accessToken, refreshToken) });
};
const props = authProps(tokens, handleSetTokens);
return <AuthContext.Provider value={props}>{children}</AuthContext.Provider>;
};
// Consumer
export const useAuthContext = () => useContext(AuthContext);
Vue版だと上記をプラグインの機能を使用してアプリ全体に適用しました。
ReactではProviderの子要素で使用することができるので、こちらも最上位から適用します
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { AuthProvider } from './store/AuthContext'; // ← 追記
ReactDOM.render(
<React.StrictMode>
<AuthProvider> {/* ← 追記 */}
<App />
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
ページの作成
残りの
- トークンチェック&認可リクエストするガワページ
- 認可コードコールバック用ページ
を作成します。
トークンチェック&認可リクエストするガワページ
- 認可が必要なページにかませて使用
- トークンがなければ認可リクエストを発行(if ~)
- トークンがなければデフォルトスロットの中身(コンテンツ)の描画を行わない(v-if)
- トークンは先ほど作成した、プラグインでprovideされた入れ物を召喚して確認(injectAuth)
というコンポーネント
import PKCE from 'js-pkce';
import { useAuthContext } from '../../store/AuthContext';
export const AuthGuard = ({ children }: any) => {
const { hasToken } = useAuthContext();
if (!hasToken()) {
const pkce = new PKCE({
client_id: '1', // `php artisan passport:client --public` したときのIDです
redirect_uri: location.origin + '/auth/callback', // 戻ってくるURL
authorization_endpoint: 'http://localhost/server/oauth/authorize', // Laravel側の認可エンドポイント
requested_scopes: '*',
});
location.replace(pkce.authorizeUrl());
return <></>;
} else {
return <>{children}</>;
}
};
を作成。
認証が必要なページにこのコンポーネントを導入します。
今回はコンテンツページは全部認証が必要なページとして、
最初に作成したグローバルメニューを持ったMasterPage.tsxに仕込みます
import { Link, Outlet } from 'react-router-dom';
import { AuthGuard } from '../components/Auth/AuthGuard';
export const MasterPage = () => {
return (
<AuthGuard>
<div id="nav">
<Link to='/'>Home</Link> |
<Link to='/about'>About</Link>
</div>
<Outlet />
</AuthGuard>
);
};
認可コードコールバック用ページ
Laravelで認証後戻ってくるページ。受け取った認可コードとトークンを交換します。
Laravel側で指定した /auth/callback
のページになります。
js-pkceのexchangeForAccessTokenというメソッドでトークンを受け取れます。
それを用意した入れ物(injectAuth)に保存(auth.setToken)し、
ここでは再度トップ画面に戻ります
import PKCE from 'js-pkce';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthContext } from '../../store/AuthContext';
export const AuthorizationCallback = () => {
const pkce = new PKCE({
client_id: '1',
redirect_uri: location.origin + '/auth/callback',
token_endpoint: 'http://localhost/server/oauth/token',
});
const { handleSetTokens } = useAuthContext();
const navigate = useNavigate();
useEffect(() => {
pkce.exchangeForAccessToken(document.location.href).then((response) => {
handleSetTokens(response.access_token, response.refresh_token);
// 認証後に遷移するページへ
navigate('/', { replace: true });
});
}, []);
return <></>;
};
このページを /auth/callback のURLで公開します
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { About } from '../views/About';
import { AuthorizationCallback } from '../views/Auth/AuthorizationCallback'; // ← 追加
import { Home } from '../views/Home';
import { MasterPage } from '../views/MasterPage';
export const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/auth/callback' element={<AuthorizationCallback />} /> {/* ← 追加 */}
<Route path='/' element={<MasterPage />}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Route>
</Routes>
</BrowserRouter>
);
};
一応完了
というわけでとりあえずこれでVue版同様、
React側にアクセスするとトークンがないためLaravelに遷移、
認証・承認後トークンをゲットして、ページがみられるようになると思います。
(これはAntDesignなどが入ってるので見た目違うと思いますが…)
締め
Reactはまだ全然でして…
おかしな点などがありましたらこっそりご教示ください…orz