有关[React + Django]的登录功能的创建方法,请进行详细解释

总结

使用React和Django来创建登录页面,并使用自己编写的登录API来实现登录成功后的页面跳转处理。

前提 tí)

    • Djangoのプロジェクトを作成済み

 

    • Reactのアプリケーションを作成済み

 

    • Formの作成はReact Hook Formを使用

 

    • RestAPIを作成するためDjango Rest Frameworkを使用

 

    今回はReactメインで説明するのでDjangoについてはCSRFとCORS以外詳しく説明しません

文件架构

tree
・
├── .gitignore
├── README.md
├── backend
│   ├── application
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── fixtures
│   │   │   └── fixture.json
│   │   ├── migrations
│   │   │   ├── __init__.py
│   │   │   └── 0001_initial.py
│   │   ├── models
│   │   │   ├── __init__.py
│   │   │   └── user.py
│   │   ├── permissions.py
│   │   ├── serializers
│   │   │   ├── __init__.py
│   │   │   └── user.py
│   │   ├── urls.py
│   │   └── views
│   │       ├── __init__.py
│   │       ├── login.py
│   │       └── user.py
│   ├── manage.py
│   ├── poetry.lock
│   ├── project
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── pyproject.toml
├── containers
│   ├── django
│   │   ├── Dockerfile
│   │   ├── Dockerfile.prd
│   │   ├── entrypoint.prd.sh
│   │   └── entrypoint.sh
│   ├── front
│   │   └── Dockerfile
│   ├── nginx
│   │   ├── Dockerfile
│   │   └── nginx.conf
│   └── postgres
│       ├── Dockerfile
│       └── init.sql
├── docker-compose.yml
├── frontend
│   ├── README.md
│   ├── package-lock.json
│   ├── package.json
│   ├── pages
│   │   ├── 404
│   │   │   └── index.js
│   │   ├── index.js
│   │   ├── login_success
│   │   │   └── index.js
└── static

这次我们要解释的是以下的文件

    • docker-compose.yml

 

    • nginx.conf

 

    • settings/base.py

 

    • settings/local.py

 

    • pages/index.js

 

    pages/login_success/index.js

我会逐一解释

Docker的配置

让前端和后端能够互相通信。

    • docker-compose.yml

 

    nginx.conf

进行创建

请参考以下文章以获得有关本次关于创建登录功能的说明的详细信息。

 

version: '3.9'

services:
  db:
    container_name: db
    build:
      context: .
      dockerfile: containers/postgres/Dockerfile
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -U "${POSTGRES_USER:-postgres}" || exit 1
      interval: 10s
      timeout: 5s
      retries: 5
    environment:
      - POSTGRES_NAME
      - POSTGRES_USER
      - POSTGRES_PASSWORD
    ports:
      - '5432:5432' # デバッグ用

  app:
    container_name: app
    build:
      context: .
      dockerfile: containers/django/Dockerfile
    volumes:
      - ./backend:/code
      - ./static:/static
    ports:
      - '8000:8000'
      # デバッグ用ポート
      - '8080:8080'
    command: sh -c "/usr/local/bin/entrypoint.sh"
    stdin_open: true
    tty: true
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy

  mail:
    container_name: mail
    image: schickling/mailcatcher
    ports:
      - '1080:1080'
      - '1025:1025'

  nginx:
    container_name: web
    build:
      context: .
      dockerfile: containers/nginx/Dockerfile
    volumes:
      - ./static:/static
    ports:
      - 80:80
    depends_on:
      - app

  front:
    container_name: front
    build:
      context: .
      dockerfile: containers/front/Dockerfile
    volumes:
      - ./frontend:/code
      - node_modules_volume:/frontend/node_modules
    command: sh -c "npm run dev"
    ports:
      - '3000:3000'
    environment:
      - CHOKIDAR_USEPOLLING=true
      - NEXT_PUBLIC_RESTAPI_URL=http://localhost/back

volumes:
  db_data:
  static:
  node_modules_volume:

networks:
  default:
    name: testnet
upstream front {
    server host.docker.internal:3000;
}

upstream back {
    server host.docker.internal:8000;
}
 
server {
    listen       80;
    server_name  localhost;

    client_max_body_size 5M;
    
    location / {
        proxy_pass http://front/;
    }
 
    location /back/ {
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://back/;
    }

    location /upload/ {
        proxy_pass http://back/upload/;
    }

    location /_next/webpack-hmr {
        proxy_pass http://front/_next/webpack-hmr;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

}

Django: 别理那些破事。

跨域资源共享(CORS)和跨站请求伪造(CSRF)的配置。

前端和后端不可或缺的协作

    • CORS

 

    CSRF

请参考以下文章以获取关于CORS的详细信息。

 

ALLOWED_HOSTS = ["http://localhost", "http://127.0.0.1"]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        # 今回はログイン認証の方法としてSession認証を採用
        "rest_framework.authentication.SessionAuthentication",
    ]
}

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "application.apps.ApplicationConfig",
    # CORS用のパッケージ
    "corsheaders",
]

MIDDLEWARE = [
    # CORS用のMiddleware
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    # CSRF用のMiddleware
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

# 自身以外のオリジンのHTTPリクエスト内にクッキーを含めることを許可する
CORS_ALLOW_CREDENTIALS = True
# アクセスを許可したいURL(アクセス元)を追加
CORS_ALLOWED_ORIGINS = django_settings.TRUSTED_ORIGINS.split()
# プリフライト(事前リクエスト)の設定
# 30分だけ許可
CORS_PREFLIGHT_MAX_AGE = 60 * 30

# CSRFの設定
# これがないと403エラーを返してしまう
# https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1"]

创建登录API

根据以下文章的参考,我们将创建一个登录API。
该API的路径是/api/login/。

from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse, JsonResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ViewSet

from application.models.user import User
from application.serializers.user import LoginSerializer


class LoginViewSet(ViewSet):
    serializer_class = LoginSerializer
    permission_classes = [AllowAny]

    @action(detail=False, methods=["POST"])
    def login(self, request):
        """ユーザのログイン"""
        serializer = LoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        employee_number = serializer.validated_data.get("employee_number")
        password = serializer.validated_data.get("password")
        user = authenticate(employee_number=employee_number, password=password)
        if not user:
            return JsonResponse(
                data={"msg": "社員番号またはパスワードが間違っています"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        else:
            login(request, user)
            return JsonResponse(data={"role": user.Role(user.role).name})

    @action(methods=["POST"], detail=False)
    def logout(self, request):
        """ユーザのログアウト"""
        logout(request)
        return HttpResponse()

 

回应

根页面

在根页面上设置用于登录的组件。

import React from 'react';
import Link from "next/link";
import Login from '../components/elements/Form/Login'

const Login = () => {

  return (
    <>
      <div>
        <Login/>
      </div>
    </>
  );
}

export default Login;

登录功能

请参考下面的文章,我使用了ReactHookForm来创建了登录表单。

 

import { useForm } from 'react-hook-form';
import Cookies from 'js-cookie';
import router from 'next/router';

function Login() {

  type LoginDataType = {
    employee_number: string;
    password: string;
  };

  const { 
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginDataType>({
    // ログインボタンを押した時のみバリデーションを行う
    reValidateMode: 'onSubmit',
  });

  const onSubmit = async (data) => {
    // Nginxとdocker-compose.ymlで設定したAPIのパス
    // http://localhost/back/api/login/
    const apiUrl = process.env.NEXT_PUBLIC_RESTAPI_URL + 'http://localhost/back/api/login/';
    const csrftoken = Cookies.get('csrftoken') || '';
    // ログイン情報をサーバーに送信
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': csrftoken,
      },
      // ユーザー名(社員番号)とパスワードをJSON形式で送信
      body: JSON.stringify(data), 
    });

    if (response.ok) {
      // ログイン成功時に/login_successへ遷移
      console.log('ログイン成功');
      router.push('/login_success');
      // リダイレクトなど、ログイン後の処理を追加
    } else {
      // ログイン失敗時にバックエンドのエラーをアラートで表示("社員番号またはパスワードが間違っています")
      response.json()
      .then(data => {
        const msg = data.msg;
        alert(msg)
      })
    }
  };

  return (
    <div className="Login">
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <input
            id="employee_number"
            name="employee_number"
            placeholder="社員番号"
            {...register('employee_number', {
              required: {
                value: true, 
                message: '社員番号を入力してください',
              },
              pattern: {
                value: /^[0-9]{8}$/,
                message: '8桁の数字のみ入力してください。',
              },
            })} 
          />
            {errors.employee_number?.message && <div>{errors.employee_number.message}</div>}
        </div>
        <div>
          <input
            id="password"
            name="password"
            placeholder="パスワード"
            type="password"
            {...register('password', { 
              required: {
                value: true,
                message: 'パスワードを入力してください'
              },
          />
            {errors.password?.message && <div>{errors.password.message}</div>}
        </div>
        <button type="submit">ログイン</button>
      </form>
    </div>
  );
}

export default Login;

我将逐个解释。 (Wǒ zhú gè jiě shì.)

FetchAPI 取得API

使用FetchAPI将数据POST到自定义的登录API上。
在进行登录之前,没有SessionID和CSRF令牌。
csrftoken的值将为空。
Content-Type将设置为application/json。
请求的BODY将是JSON格式的。

    // Nginxとdocker-compose.ymlで設定したAPIのパス
    // http://localhost/back/api/login/
    const apiUrl = process.env.NEXT_PUBLIC_RESTAPI_URL + '/api/login/';
    const csrftoken = Cookies.get('csrftoken') || '';
    // ログイン情報をサーバーに送信
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRFToken': csrftoken,
      },
      // ユーザー名(社員番号8桁)とパスワードをJSON形式で送信
      body: JSON.stringify(data), 
    });

处理响应

当回复收到时,将使用状态码进行处理。
如果收到200系列的响应,将使用next/router将页面push到/login_success。
如果登录失败(收到的不是200系列的响应),将通过警报提示显示错误消息。

    if (response.ok) {
      // ログイン成功時に/login_successへ遷移
      console.log('ログイン成功');
      router.push('/login_success');
      // リダイレクトなど、ログイン後の処理を追加
    } else {
      // ログイン失敗時にバックエンドのエラーをアラートで表示("社員番号またはパスワードが間違っています")
      response.json()
      .then(data => {
        const msg = data.msg;
        alert(msg)
      })
    }

成功登录后的导航目标

目的地是下列简单的选项

import Link from "next/link";


const LoginSuccess = () => {

  return (
    <>
      <h1>ログイン成功!</h1>
      <div>
        <Link href="/"><h1>Homeへ</h1></Link>
      </div>
    </>
  );
};

export default LoginSuccess;

让我们亲身尝试登录吧!

スクリーンショット 2023-10-22 20.02.29.png

成功登录后,将会显示以下页面,并将SessionID和CSRF令牌保存在Cookie中。

スクリーンショット 2023-10-22 15.38.25.png
スクリーンショット 2023-10-22 21.15.38.png
postgres=# \d django_session
 session_key  | character varying(40)    |           | not null | 
 session_data | text                     |           | not null | 
 expire_date  | timestamp with time zone |           | not null | 
postgres=# select * from django_session;
 fjnp91385waomnbjeyrj9d5xv1yghwhu |.eJxtjEsOAiEQRO_CWgm_6UGX7j0DaWiQUQPJfFbGuzskLDSxFpVKql69mMNtzW5b4uwmYmcmuo5_rEuywzfmMTxiaSzdsdwqD7Ws8-R5m_DeLvxaKT4vfftzkHHJjSZAYEkDAqyQfuYRhIBtA0K9iohyFGBMkNKSRjac7RehkErTxI8e38AJ_E-gQ:1quXMh:m_UZbRXoULHFeFe6lyeb52dJw0Cpuq6VxCr8U8eOWCk | 2023-11-05 12:15:31.119386+00

如果登录失败,会显示以下提示框。

スクリーンショット 2023-10-22 15.44.05.png

以上是上面所提到的。

请参考

 

bannerAds