Go + React

2021年2月13日

【Go】JWT認証をReactとGoの連携で実装(その2)

フロント側のReactを実装する

今度はReactでフロント側の実装に入ります。Reactの解説をするととても長くなるのでここではポイントを絞って解説します。

AWS EC2のセキュリティグループを設定

本記事ではAWS EC2で環境構築しています。
Go側をポート番号8080、React側をポート番号3000で接続するため、
セキュリティグループのインバウンドルールで、カスタムTCPでそれぞれの番号を開放しておいてください。

Create React App でReactアプリの雛形をインストール

Reactをインストールするディレクトリで、Create React App コマンドを実行します。
Reactアプリの雛形がインストールされます。
その他、今回使用するライブラリをインストールします。

今回使用するライブラリ

  • react-dom
  • react-router-dom
  • material-ui/core
  • material-ui/lab

UIコンポーネントライブラリとしてMaterial-UIを一部で使用しています。

Material-UI
https://material-ui.com/

最後にyarn start でデフォルトのReactアプリが起動するか確認しておきます。

$ mkdir react
$ cd react
$ npx create-react-app sample
$ cd sample

$ yarn add react-router-dom
$ yarn add @types/react
$ yarn add @types/react-dom
$ yarn add @types/react-router-dom
$ yarn add @material-ui/core
$ yarn add @material-ui/lab

$ yarn start

ディレクトリ構造(React側)

Create React App で自動的に作成されるディレクトリやファイルの説明は割愛します。
変更や追加作成したものだけ説明します。

├── Go
│   └── (前回の記事参照)
└── React
    └── sample
        ├── public
        │   └──  (中のファイルは変更なし)
        └── src (追加変更したファイルのみ記載)
            ├── App.js
            ├── Auth.js
            ├── Login.js
            ├── Logout.js
            ├── Mypage.js
            └── User.js

追加変更したソース

今回、下記のサイトを参考にさせていただきました。
各ファイルの動きも下記のサイトに準じています。
相違点として、参考サイトには実装されていなかったバックエンド側との認証処理の連携を追加しています。
あとはUIコンポーネントに、react-bootstrapのかわりにmaterial-uiを使用している点です。

tomonoriminegishi / hello-login-app
https://github.com/tomonoriminegishi/hello-login-app

src/App.js

react-routerを利用してルーティングをしています。
Authコンポーネント内が認証が必要な領域となります。

import React, { Component } from 'react';
import {
  BrowserRouter as Router,
  Route,
  Switch,
  Redirect,
} from 'react-router-dom';

import Auth from './Auth';

import Login from './Login';
import Logout from './Logout';
import Mypage from './Mypage';

export default class App extends Component {
  render() {
    return (
      <Router>
        <Switch>
          <Route exact path="/login" component={Login} />
          <Route exact path="/logout" component={Logout} />
          <Auth>
            <Switch>
              <Route exact path="/mypage" component={Mypage} />
              <Redirect from="/" to="/mypage" />
            </Switch>
          </Auth>
        </Switch>
      </Router>
    );
  }
}

src/Auth.js

ログイン済みか判定部分になります。

import React from 'react';
import { Redirect } from 'react-router-dom';
import User from './User';

const Auth = props =>
  User.isLoggedIn() ? props.children : <Redirect to={'/login'} />;

export default Auth;

src/Login.js

前々回の記事と同様、ログインとサインアップのフォーム画面をReactで生成したものです。

import React, { Component } from 'react';
import { Container, FormControl, FormLabel, TextField, Button, Box }  from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { withRouter } from 'react-router-dom';
import User from './User';

class Login extends Component {
  constructor(props) {
    super(props);

    this.state = {
      name: '',
      password: '',
      errMessage: '',
    };
    if (User.isLoggedIn() && localStorage.getItem('token') !== null) {
      this.props.history.push({ pathname: 'mypage' });
    }
  }

  clickLogin = async () => {
    try {
      await User.login(this.state.name, this.state.password);

      this.props.history.push({ pathname: 'mypage' });
    } catch (e) {
      this.setState({ errMessage: 'Wrong name or password.' });
    }
  };

  clickSignup = async () => {
    try {
      await User.signup(this.state.name, this.state.password);

      this.props.history.push({ pathname: 'login' });
    } catch (e) {
      this.setState({ errMessage: 'Wrong name or password.' });
    }
  };

  handleChange = e => {
    this.setState({ [e.target.id]: e.target.value });
  };

  render() {
    return (
      <Container>
        <form className="fetchFormLogin">
          <Box m={1}>
            <h2>Login</h2>
            {this.state.errMessage && (
              <Alert>{this.props.message}</Alert>
            )}
          </Box>
          <Box m={1} p={1}>
            <FormControl>
              <FormLabel>Name:</FormLabel>
              <TextField
                name="name"
                type="text"
                placeholder="Enter your name"
                onChange={this.handleChange}
                value={this.state.nameLogin}
              />
            </FormControl>
          </Box>
          <Box m={1} p={1}>
            <FormControl>
              <FormLabel>password</FormLabel>
              <TextField
                name="password"
                type="password"
                placeholder="Enter your password"
                onChange={this.handleChange}
                value={this.state.passwordLogin}
              />
            </FormControl>
          </Box>
          <Box m={1} p={1}>
            <Button type="button" variant="contained" color="primary" onClick={this.clickLogin}>
            Login
            </Button>
          </Box>
        </form>
        
        <form className="fetchFormSignup">
          <Box m={1}>
            <h2>Sign Up</h2>
            {this.state.errMessage && (
              <Alert>{this.props.message}</Alert>
            )}
          </Box>
          <Box m={1} p={1}>
            <FormControl>
              <FormLabel>Name:</FormLabel>
              <TextField
                name="name"
                type="text"
                placeholder="Enter your name"
                onChange={this.handleChange}
                value={this.state.nameSignup}
              />
            </FormControl>
          </Box>
          <Box m={1} p={1}>
            <FormControl>
              <FormLabel>password</FormLabel>
              <TextField
                name="password"
                type="password"
                placeholder="Enter your password"
                onChange={this.handleChange}
                value={this.state.passwordSignup}
              />
            </FormControl>
          </Box>
          <Box m={1} p={1}>
            <Button type="button" variant="contained" color="secondary" onClick={this.clickSignup}>
            Sign Up
            </Button>
          </Box>
        </form>
      </Container>
    );
  }
}
        
export default withRouter(Login);

src/Logout.js

import React, { Component } from 'react';
import { Container }  from '@material-ui/core';
import { Link } from 'react-router-dom';
import User from './User';

export default class Logout extends Component {
  async componentDidMount() {
    await User.logout();
  }

  render() {
    return (
      <Container>
        <div>
          <h2>Logged out.</h2>
          <div>
            <Link to="/login">Go to Login</Link>
          </div>
        </div>
      </Container>
    );
  }
}

src/Mypage.js

import React, { Component } from 'react';
import { Container }  from '@material-ui/core';
import { Link } from 'react-router-dom';

export default class Mypage extends Component {
  render() {
    return (
      <Container>
        <div>
          <h2>Hello Login app mypage</h2>
          <div>
            <Link to="/logout">Logout</Link>
          </div>
        </div>
      </Container>
    );
  }
}

src/User.js

実際のログイン処理、ログアウト処理、ログイン状態の確認などを行っています。
Go側にリクエスト投げて、レスポンスを受け取り、認証の判定をしています。

class User {
  isLoggedIn = () => this.get('isLoggedIn') === 'true';
  set = (key, value) => localStorage.setItem(key, value);
  get = key => this.getLocalStorage(key);

  getLocalStorage = key => {
    const ret = localStorage.getItem(key);
    const retTtoken = localStorage.getItem('token');
    if (ret && retTtoken) {
      return ret;
    }
    return null;
  };

  signup = async (name, password) => {
    const fetchFormSignup = document.querySelector('.fetchFormSignup');
    const url = 'http://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:8080/user';
    let formDataSignup = new FormData(fetchFormSignup);
    fetch(url, {
        method: 'POST',
        body: formDataSignup
    }).then((response) => {
        if(!response.ok) {
            console.log('response error!');
            return false;
        } else {
          console.log('response good!');
          return response.json();
        }
    }).then((data)  => {
        const title = data.title
        if (title) {
          console.log('title = ' + title);
          window.location.href = '/login';
          return true;
        } else { 
          console.log("error!");
          return false;
        }
    }).catch((error) => {
        console.log(error);
        return false;
    });
  };

  login = async (name, password) => {
    const fetchFormLogin = document.querySelector('.fetchFormLogin');
    const url = 'http://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:8080/login';
    let formDataLogin = new FormData(fetchFormLogin);
    fetch(url, {
        method: 'POST',
        body: formDataLogin
    }).then((response) => {
        if (!response.ok) {
          console.log('response error!');
          return false;
        } else {
          console.log('response good!');
          return response.json();
        }
    }).then((data)  => {
        const token = data.token
        if (token) {
          console.log('token = ' + token)
          this.rReq(token)
        } else { 
          console.log("token error!");
          return false;
        }
    }).catch((error) => {
        console.log(error);
        return false;
    });
  };

  logout = async () => {
    localStorage.removeItem('token');
    if (this.isLoggedIn()) {
      this.set('isLoggedIn', false);
    }
  };

  rReq = async (token) => {
    const url = 'http://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:8080/restricted';
    fetch(url, {
      method: "GET",
      headers: {
      Authorization:
          `Bearer ${token}`,
      },
    }).then(function(response) {
        return response.json();
    }).then(function(json) {
        if (token) {
          console.log('tokenreq = ' + token)
          localStorage.setItem('token', token)
          localStorage.setItem('isLoggedIn', true);
          window.location.href = '/mypage'
        } else { 
          console.log("token error2");
          return false;
        }
    });
  };
}

export default new User();

GoとReactをそれぞれ起動

React側の実装が完了したら、バックエンド側のGoとフロント側のReactを、それぞれのターミナルから起動します。

Go側(:8080で起動)

$ cd go/src/sample
$ go run main.go

React側(:3000で起動)

$ cd react/sample
$ yarn start

ブラウザでReact側にアクセス

http://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:8080/

にアクセスすると、うまくいけば下記のようなログイン画面が表示されます。
ログイン認証も、GoでTemplates版と同様にできるはずです。

ReactとGoでログインフォーム

今回はここまでとなります。

※毎回記載していますが、本当はユーザー情報の登録やログイン情報の参照に際して、入力内容のバリデーションやサニタイズなどのセキュリティ対策をすべきです。
この記事はあくまで実験用なので、このサンプルソースをそのままプロダクトに利用しないでください。

次回の記事では、ローカル環境のDockerにGoとMySQLコンテナ間接続に挑戦します。

LINEで送る
Pocket

label

Written by
isaka

バックエンドエンジニア

CONTACT

お問い合わせ、ご依頼などは下記電話番号かメールアドレスまでご連絡ください。
※内容により回答までお時間をいただく場合がございます、予めご了承ください。

tel. 06-6534-9333

10:00-19:00(※土日祝を除く)