Help us understand the problem. What is going on with this article?

Next.jsでログインフラグによるリダイレクト機能を実装してみた

前書き

Next.jsで自前のログイン機能を作成しようとして情報集めに苦労したのでその備忘録。

前提として、認証処理一式を実装できるライブラリは色々あります。但しSNSとの連携を前提としたモノしかなかった。(僕調べ)

  • 個人学習向けの簡単なログイン機能を実装したい
  • 自前の画面遷移時のリダイレクト判定処理を実装したい

みたいな方々には、もし良ければこんな実装もあるよっていう参考にしていただければ幸いです。

使用技術

  • Next.js(メイン)
    • axios:お馴染み、API叩く用に使用
    • nookies:Cookie制御用の外部ライブラリ
  • バックエンドAPI(なんらかのログイン成功フラグが返せればなんでも良い、自分はSpring Bootを使いました)

機能要件

大要件

  • ログイン済みか否かで遷移させる画面を制御する

小要件

  • ログイン済みフラグの取得のために、ログイン画面からIDとパスワードを入力してログイン用バックエンドAPIを叩く(固定値設定して等価判定しているだけの簡単なモノ)
  • ログイン済みフラグをクライアント側で保持(Cookieへ)
  • Cookieにログイン済みフラグがなければ、遷移時にログイン画面へリダイレクトさせる

実装

ポイント

  • _app.tsx(Jsなら_app.jsx) で画面制御処理を実装→全画面のコンポーネントで呼ばれるため

コード

  • ログイン画面コンポーネント
import Head from 'next/head';
import axios from 'axios';
import { useState } from 'react';
import { Container, Button, Form, Image } from 'react-bootstrap';
import { setCookie } from 'nookies';
import { useRouter } from 'next/router';

interface ILogin {
  userName: string;
  password: string;
}

const initialPayload: ILogin = {
  userName: '',
  password: '',
};

const Login = () => {
  const router = useRouter();

  const [payload, setPayload] = useState<ILogin>(initialPayload);

  const handleChange = (e) => {
    setPayload({
      ...payload,
      [e.target.name]: e.target.value,
    });
  };

  const onClickLogin = () => {
    axios
      .post('/api/login', payload)
      .then((res) => {
        // ログインフラグをクッキーへ、「auth」というキーで登録
        setCookie(null, 'auth', 'true', {
          maxAge: 30 * 24 * 60 * 60, // お好きな期限を
          path: '/',
        });
        router.push('/');
      })
      .catch((e) => {
        console.log('認証エラー');
      });
  };
  return (
    <Container>
      <Head>
        <title>ログイン画面例</title>
      </Head>
      <div className='login-container'>
        <Image
          src='https://placehold.jp/150x150.png'
          roundedCircle
          style={{ marginBottom: '20px' }}
        />
        <Form.Control
          type='text'
          placeholder='User Name'
          name='userName'
          value={payload.userName}
          onChange={handleChange}></Form.Control>
        <Form.Control
          type='password'
          placeholder='Password'
          name='password'
          value={payload.password}
          onChange={handleChange}></Form.Control>
        <Button variant='info' type='button' onClick={onClickLogin}>
          Login
        </Button>
      </div>
    </Container>
  );
};

export default Login;

※イメージこんな画面(若干自前CSS足しているので上記コピーだけでは再現されません)

  • _app.tsx(メイン)
import { NextPageContext } from 'next';
import { AppProps } from 'next/app';
import { parseCookies } from 'nookies';
import { useEffect } from 'react';
import { useRouter } from 'next/router';

const MyApp = ({ Component, pageProps }: AppProps, ctx: NextPageContext) => {
  const router = useRouter();
  const cookies = parseCookies(ctx);

  // 第二引数に空配列を指定してマウント・アンマウント毎(CSRでの各画面遷移時)に呼ばれるようにする
  useEffect(() => {
    // CSR用認証チェック

    router.beforePopState(({ url, as, options }) => {
      // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
      if (url !== '/login' && url !== '/_error') {
        if (typeof cookies.auth === 'undefined') {
          // CSR用リダイレクト処理
          window.location.href = '/login';
          return false;
        }
      }
      return true;
    });
  }, []);

  const component =
    typeof pageProps === 'undefined' ? null : <Component {...pageProps} />;

  return component;
};

MyApp.getInitialProps = async (appContext: any) => {
  // SSR用認証チェック

  const cookies = parseCookies(appContext.ctx);
  // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
  if (
    appContext.ctx.pathname !== '/login' &&
    appContext.ctx.pathname !== '/_error'
  ) {
    if (typeof cookies.auth === 'undefined') {
     // SSR or CSRを判定
      const isServer = typeof window === 'undefined';
      if (isServer) {
        console.log('in ServerSide');
        appContext.ctx.res.statusCode = 302;
        appContext.ctx.res.setHeader('Location', '/login');
        return {};
      } else {
        console.log('in ClientSide');
      }
    }
  }
  return {
    pageProps: {
      ...(appContext.Component.getInitialProps
        ? await appContext.Component.getInitialProps(appContext.ctx)
        : {}),
      pathname: appContext.ctx.pathname,
    },
  };
};

export default MyApp;

後書き

Next(Nuxtも)はSSRとCSRそれぞれを考慮する必要があるからめんどい、、けど良い勉強になった。
Nextはほぼ英語記事しかないので日本語の参考記事も増えたらいいなあ(やっぱり日本ではNuxt・・・?)

参考

※リダイレクト周りはほぼ英語記事参考にしましたが散り散りで集められませんでしたすみません・・。

YU-TA-9
社会人3年目のWEBエンジニア。 IT2社を経て現在受託WEB開発企業でエンジニアしてます。 フルスタックになりたい。 Java/React/Vue/AWS 資格:SAA
https://yu-ta-9.jimdosite.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away