ログイン情報を用いてユーザを識別できるWebサービスを作成したい。
ただし、新規にアカウントを作るのではなく、SNSなどの既存アカウントと連携した構成としたい。
また、モダンなフロントエンドを用いたいため、バックエンドはRESTfulな構成としたい。
こんなお悩みを解決します。
今回は、前回の続きで、フロントエンド側の設定について解説します。
前回の記事では、バックエンド側の設定ついて解説しています。詳しくは以下の記事を参考にしてください。
【解説】Social OAuthによるアプリケーション連携(2/3)【Django REST framework】
続きを見る
実装結果
前回同様、先に実装結果を確認したい方は、以下のリンクからGitHubへアクセスしてください。
https://github.com/yuruto-free/django-rest-framework/tree/v0.2.0
今回の構築対象
今回は、下記の3つの処理をReactで実装していきます。
- 認可リクエストの処理
- 認可レスポンスを受け、アクセストークンを発行する処理
- ユーザ情報取得処理
1.と2.は、以下に示す図の赤枠部分が該当します。
また、3.は前々回示した下記のシーケンスそのものとなります。(バックエンド側は実装済みです。)
フロントエンドの構築手順
フロントエンドの構築手順の概要は以下のようになります。
- React用の環境変数の設定
- 関連ライブラリのインストールとアプリケーションの作成
- 各種機能の実装
今回はDockerを用いて環境構築を行っていますが、ホストマシンに直接環境を構築したい場合もあると考えているため、過程が読み取れるよう、上記に示した順番で解説していきます。
今回説明する内容のメインは、「各種機能の実装」部分のため、内容を先に確認したい場合は、コチラをクリックしてください。
また、前回同様、以下のようなディレクトリ構成を想定しています。
./
|-- docker-compose.yml
|-- LICENSE
|-- README.md
|-- wrapper.sh
|-- django/
| |-- Dockerfile
| |-- execute.sh
| |-- README.md
| |-- requirements.txt
| |-- uwsgi.template
| |-- sqlite/
│ | `-- db.sqlite3
| `-- src/
| |-- manage.py
| |-- accounts/
| | |-- admin.py
| | |-- apps.py
| | |-- models.py
| | |-- pipeline.py
| | |-- serializers.py
| | |-- tests.py
| | |-- urls.py
| | |-- views.py
| | |-- __init__.py
| | `-- migrations/
| | |-- 0001_initial.py
| | `-- __init__.py
| `-- config/
| |-- asgi.py
| |-- define_module.py
| |-- urls.py
| |-- wsgi.py
| |-- __init__.py
| `-- settings/
| |-- base.py
| |-- development.py
| |-- production.py
| `-- __init__.py
|-- envs/
| |-- django/
| | |-- .env
| | `-- README.md
| |-- mysql/
| | |-- .env
| | `-- README.md
| `-- react/
| |-- .env
| `-- README.md
|-- react/
| |-- Dockerfile
| |-- execute.sh
| |-- public/
| | |-- favicon.ico
| | |-- index.html
| | |-- manifest.json
| | `-- robots.txt
| `-- src/
| |-- App.js
| |-- App.test.js
| |-- index.js
| |-- reportWebVitals.js
| |-- setupTests.js
| `-- components
| |-- login-logout.js
| `-- user-info.js
`-- static
`-- .gitkeep
React用の環境変数の設定
GitHubで公開しているDocker環境を利用する場合と利用しない場合で手順が異なるため、それぞれ分けて説明します。
GitHubで公開しているDocker環境を利用する場合
下記に示す内容をenvs/react/.env
ファイルとして保存します。
具体的な環境変数の意味は後述する表を参考にしてください。
REACT_APP_GOOGLE_CLIENT_ID=google-oauth2-client-id
GitHubで公開しているDocker環境を利用しない場合
下記に示す内容を.env
ファイルとして保存します。(保存先は各自の環境に揃えてください)
REACT_APP_GOOGLE_CLIENT_ID=google-oauth2-client-id
環境変数の説明
環境変数の位置づけを以下に示します。
ここで、設定時の具体的な値は、ご自身の環境に合わせて変更する必要があります。
環境変数名 | 位置づけ |
---|---|
REACT_APP_GOOGLE_CLIENT_ID | Google-OAuth2で利用するクライアントID Google Developer Consoleで確認したクライアントIDを指定する。 |
関連ライブラリのインストールとアプリケーションの作成
GitHubで公開しているDocker環境を利用する場合と利用しない場合で手順が異なるため、それぞれ分けて説明します。
GitHubで公開しているDocker環境を利用する場合
トップディレクトリで、以下のコマンドを実行し、コンテナイメージを作成します。
# shell scriptに実行権限を与える
chmod +x wrapper.sh
# Docker Imageを作成する
./wrapper.sh build
GitHubで公開しているDocker環境を利用しない場合
Node(バージョン 18)がインストールされている前提で説明を進めます。
React Appの作成
以下のコマンドを実行し、React Appを作成します。
npx create-react-app app -timeout=60000
関連ライブラリのインストール
以下のコマンドを実行し、関連ライブラリをインストールします。
npm install axios @react-oauth/google@latest
package.jsonの修正
同一ディレクトリにあるpackage.json
を以下のように修正します。
# 変更前
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
# 変更後
"scripts": {
# WATCHPACK_POLLING=trueを追加
"start": "WATCHPACK_POLLING=true react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
【重要】各種機能の実装
フロントエンドの構築を行う上での要となる部分です。GitHubで公開しているDocker環境を利用する場合は、すでに作成済みのため、対応不要です。
フロントエンド側は、次の3つに分けて構築していきます。
- 全体構成(
App.js
に対応) - ログイン・ログアウトの処理(
components/login-logout.js
に対応) - ユーザ情報取得処理(
components/user-info.js
に対応)
全体構成
ここでは、以下の2点を満たすコンポーネントを作成します。
- ソーシャルログインの状態(未ログイン、ログイン中)を管理
- ログインコンポーネント、ユーザ情報コンポーネント、ログアウトコンポーネントの配置
※表示非表示は、個々のコンポーネント内で管理します
実装例は以下のようになります。また、@react-oauth/google
の使い方は作者のGitHubを参照してください。
https://github.com/MomenSherif/react-oauth
import { useState } from 'react';
import { GoogleOAuthProvider } from '@react-oauth/google';
import { Login, Logout } from './components/login-logout.js';
import UserInfo from './components/user-info.js';
function App() {
// 環境変数からクライアントIDを取得
const googleClientID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
// ユーザのログイン状態を管理するstateを生成
const [user, setUser] = useState({});
// 認可レスポンス返却時に呼ばれる処理を定義
const handleSocialLogin = (response) => {
setUser(response);
};
// ログアウトボタン押下時に呼ばれる処理を定義
const handleSocialLogout = () => {
setUser({});
};
return (
<>
// ログインコンポーネント、ログアウトコンポーネントでクライアントIDが利用できるように、
// 「GoogleOAuthProvider」コンポーネントでラップする
<GoogleOAuthProvider clientId={googleClientID}>
// 必要なコンポーネントを配置
<Login
user={user}
text="Sign in with Google"
onSuccessCallback={(response) => handleSocialLogin(response)}
/>
<UserInfo
user={user}
/>
<Logout
user={user}
text="Sign out"
onClick={() => handleSocialLogout()}
/>
</GoogleOAuthProvider>
</>
);
}
export default App;
ログイン・ログアウトの処理
ログイン時は、以下の2点を順に実行します。ただし、1.はライブラリ内で実装済みのため、ここでは2.の対応がメインとなります。
- 認可リクエストを出す処理
- 認可レスポンスを受け取った後、バックエンドにアクセストークンを発行する処理
実装例を以下に示します。
import axios from 'axios';
import { useGoogleLogin, googleLogout } from '@react-oauth/google';
const getLoginStatus = (user) => Object.keys(user).length > 0;
const baseURL = 'http://localhost:8000';
const redirectURL = 'http://localhost:3000';
const Login = (props) => {
const handleAuthorizationCode = (response) => {
// Convert authorization code to access token
axios.post(`${baseURL}/api/login/social/jwt-pair/`, {
provider: 'google-oauth2',
code: response.code,
redirect_uri: redirectURL,
}).then((res) => {
props.onSuccessCallback(res.data);
}).catch((err) => {
console.log('Auth Error:', err);
});
};
const login = useGoogleLogin({
onSuccess: handleAuthorizationCode,
onError: (err) => console.log(err),
flow: 'auth-code',
});
if (getLoginStatus(props.user)) {
return null;
}
return (
<button onClick={() => login()}>
{props.text}
</button>
);
};
const Logout = (props) => {
// 後述
};
export {
Login,
Logout,
};
認可リクエストの処理
先に述べたように、ライブラリに組み込まれている機能を利用します。
DOMと関連する処理を抜き出すを以下のようになります。
const login = useGoogleLogin({
onSuccess: handleAuthorizationCode, // ユーザから同意が得られた場合に呼ばれる処理
onError: (err) => console.log(err), // ユーザから拒否された場合に呼ばれる処理
flow: 'auth-code', // 認可リクエスト(認可コードの発行)のフローを実行する旨を明記
});
// 中略
// ボタンクリック時にログイン処理が実行されるよう、onClickにコールバック処理を追加する
return (
<button onClick={() => login()}>
{props.text}
</button>
);
認可レスポンスの受領・アクセストークン発行
ここから、バックエンド側の設定と紐づけて処理していく必要があります。
認可リクエストの処理において、認可レスポンスを受け取るコールバック関数をhandleAuthorizationCode
として設定しました。
このコールバック関数内から以下の内容のPOSTリクエストを投げることで、アクセストークンが取得できます。
項目 | 内容 |
---|---|
エンドポイント | /api/login/social/jwt-pair/ |
データ | ・provider: 'google-oauth2' ・ code: response.code ・ redirect_uri: redirectURL ※redirectURLは、http://localhost:3000となる |
戻り値 | ・access token ※ユーザ情報を取得する際の認証時に利用する ・refresh token ※アクセストークンの有効期限が切れた際に利用する |
ここで、エンドポイントの「social/jwt-pair/
」は、rest-social-auth
のライブラリの仕様に合わせて、付与しています。
※コチラの5.3節にPOST時のエンドポイントが定義されています。
該当する実装例を抜き出すと以下のようになります。
const handleAuthorizationCode = (response) => {
// Convert authorization code to access token
axios.post(`${baseURL}/api/login/social/jwt-pair/`, {
provider: 'google-oauth2',
code: response.code,
redirect_uri: redirectURL,
}).then((res) => {
props.onSuccessCallback(res.data);
}).catch((err) => {
console.log('Auth Error:', err);
});
};
ログアウトの処理
ログアウト時の実装例を以下に示します。
import axios from 'axios';
import { useGoogleLogin, googleLogout } from '@react-oauth/google';
const getLoginStatus = (user) => Object.keys(user).length > 0;
const baseURL = 'http://localhost:8000';
const redirectURL = 'http://localhost:3000';
// 中略
const Logout = (props) => {
const handleLogout = () => {
googleLogout();
props.onClick();
};
if (!getLoginStatus(props.user)) {
return null;
}
return (
<div>
<button onClick={() => handleLogout()}>
{props.text}
</button>
</div>
);
};
export {
Login,
Logout,
};
内容は非常に単純で、ライブラリで提供されているログアウト処理を呼び出した後、App.js
で管理しているログイン状態を初期化する処理を呼び出します。
ユーザ情報取得処理
最後に、ユーザ情報取得処理について説明します。
注意点として、アクセストークンの有効期限が切れていないことが前提となるため、アクセストークンが切れた後は、現時点の実装ではReact上で情報を更新することはできません。
今回はサンプルというのもあり、ログイン処理後、5分以内であれば動作するようになっています。
バックエンド側でJWTのアクセストークンのライフタイムを以下のように設定しているため、アクセストークンの有効期限は5分以内となります。
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # アクセストークンの有効期限
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'AUTH_HEADER_TYPES': ('JWT', ),
'AUTH_TOKEN_CLASSES': (
'rest_framework_simplejwt.tokens.AccessToken',
),
}
上記の制約の下で、ユーザ情報を取得する際の実装例を以下に示します。
import axios from 'axios';
const UserInfo = (props) => {
if (Object.keys(props.user).length === 0) {
return null;
}
const handleCollectUserList = (user) => {
const baseURL = 'http://localhost:8000';
const token = user.token;
// Collect user data used by access token
axios.get(`${baseURL}/api/accounts/users/`, {
headers: {
// 下記の内容をHTTPのAuthorizationリクエストヘッダーとして付与
// auth-scheme: JWT(バックエンド側の「AUTH_HEADER_TYPES」で指定した文字列)
// token: アクセストークン
Authorization: `JWT ${token}`,
'Content-Type': 'application/json; charset=utf-8',
},
}).then((res) => {
const userLists = document.querySelector('#user-lists');
const out ='<ul>' + res.data.map((_user, idx) => {
const _tmp = Object.entries(_user).map(([key, value]) => `<li>${key}: ${value}</li>`).join(' ');
return `<li key=${idx}><ul>${_tmp}</ul></li>`;
}).join('\n') + '</ul>';
userLists.innerHTML = out;
}).catch((err) => {
console.log('Error:', err);
});
};
return (
<>
<ul>
{Object.keys(props.user).map((key) => (
<li key={key}>{key}: {props.user[key]}</li>
))}
</ul>
<h1>User List</h1>
<div id="user-lists"></div>
<button onClick={() => handleCollectUserList(props.user)}>
Collect User List
</button>
</>
);
};
export default UserInfo;
上記の実装例の中で、以下が重要な箇所となります。
axios.get(`${baseURL}/api/accounts/users/`, {
headers: {
// 下記の内容をHTTPのAuthorizationリクエストヘッダーとして付与
// auth-scheme: JWT(バックエンド側の「AUTH_HEADER_TYPES」で指定した文字列)
// token: アクセストークン
Authorization: `JWT ${token}`,
'Content-Type': 'application/json; charset=utf-8',
},
})
REST_FRAMEWORK = {
# 権限に関する基本的なポリシー: 認証が必要
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
# 認証方法: JWTによる認証
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
バックエンド側で以下のように設定しているため、バックエンドが保持しているデータを取得する際はJWTによる認証が必要になります。
このため、以下のように、ヘッダーを付与してデータへのアクセス時に認証が行われるようにします。
Authorization: `JWT ${token}`
以上で、Social OAuth+Django REST Framework+Reactによるアプリケーション連携の方法の説明は終了となります。
まとめ
今回は、Reactを用いたフロントエンド側の実装内容について説明しました。
非常にシンプルな内容でしたが、最低限必要な設定や実装例は紹介できたと考えています。
アプリケーション連携時の参考にしていただければと思います。