広告 プログラミング

【解説】Social OAuthによるアプリケーション連携(3/3)【Django REST framework】

※本ページには、プロモーション(広告)が含まれています。

悩んでいる人

ログイン情報を用いてユーザを識別できる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. ユーザ情報取得処理

1.と2.は、以下に示す図の赤枠部分が該当します。

認可リクエスト処理・アクセストークン発行処理

また、3.は前々回示した下記のシーケンスそのものとなります。(バックエンド側は実装済みです。)

ユーザ情報取得処理

フロントエンドの構築手順

フロントエンドの構築手順の概要は以下のようになります。

  1. React用の環境変数の設定
  2. 関連ライブラリのインストールとアプリケーションの作成
  3. 各種機能の実装

今回は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_IDGoogle-OAuth2で利用するクライアントID
Google Developer Consoleで確認したクライアントIDを指定する。
Reactで利用する環境変数設定

関連ライブラリのインストールとアプリケーションの作成

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.の対応がメインとなります。

  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時のエンドポイントが定義されています。

エンドポイントの設定例(出典:https://github.com/MomenSherif/react-oauth)

該当する実装例を抜き出すと以下のようになります。

  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を用いたフロントエンド側の実装内容について説明しました。

非常にシンプルな内容でしたが、最低限必要な設定や実装例は紹介できたと考えています。

アプリケーション連携時の参考にしていただければと思います。

スポンサードリンク

-プログラミング
-, , ,