1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactとバックエンドで作る!実践Web開発入門 | 第7章: ユーザー認証の実装

Posted at

はじめに

第6章でデータベースを追加しました。今回は、ユーザー認証(登録・ログイン)を追加し、商品追加をログイン済みユーザーに限定します。JWT(JSON Web Token)を使って安全に認証を行います。

目標

  • ユーザー登録とログイン機能を実装する
  • JWTで認証を管理する
  • APIを保護する

バックエンドの準備

backendで必要なパッケージをインストール:

npm install bcryptjs jsonwebtoken

ユーザー認証の実装

backend/index.jsを以下のように更新:

const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const app = express();
const port = 5000;

app.use(express.json());
app.use(cors());

mongoose.connect('mongodb://localhost:27017/productdb', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}).then(() => console.log('MongoDB connected'));

// ユーザースキーマ
const userSchema = new mongoose.Schema({
  username: String,
  password: String,
});
const User = mongoose.model('User', userSchema);

// 商品スキーマ
const productSchema = new mongoose.Schema({
  name: String,
  price: Number,
});
const Product = mongoose.model('Product', productSchema);

// JWT認証ミドルウェア
const authenticateToken = (req, res, next) => {
  const token = req.headers['authorization']?.split(' ')[1];
  if (!token) return res.status(401).json({ message: 'トークンが必要です' });

  jwt.verify(token, 'secret_key', (err, user) => {
    if (err) return res.status(403).json({ message: '無効なトークン' });
    req.user = user;
    next();
  });
};

// ユーザー登録
app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  const user = new User({ username, password: hashedPassword });
  await user.save();
  res.status(201).json({ message: 'ユーザー登録完了' });
});

// ログイン
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await User.findOne({ username });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ message: '認証失敗' });
  }
  const token = jwt.sign({ id: user._id }, 'secret_key', { expiresIn: '1h' });
  res.json({ token });
});

// APIエンドポイント
app.get('/products', async (req, res) => {
  const products = await Product.find();
  res.json(products);
});

app.post('/products', authenticateToken, async (req, res) => {
  const { name, price } = req.body;
  const product = new Product({ name, price });
  await product.save();
  res.status(201).json(product);
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
  • bcryptでパスワードをハッシュ化。
  • jsonwebtokenでトークンを生成。
  • authenticateTokenで保護されたルートを制限。

React側の更新

frontend/srcAuthContext.tsxを作成:

import React, { createContext, useState, useContext } from 'react';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [token, setToken] = useState(localStorage.getItem('token') || '');

  const login = (newToken) => {
    setToken(newToken);
    localStorage.setItem('token', newToken);
  };

  const logout = () => {
    setToken('');
    localStorage.removeItem('token');
  };

  return (
    <AuthContext.Provider value={{ token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuthContext = () => useContext(AuthContext);

App.tsxを更新:

import React, { useEffect } from 'react';
import './App.css';
import { ProductProvider, useProductContext } from './ProductContext';
import { AuthProvider, useAuthContext } from './AuthContext';

function App() {
  return (
    <AuthProvider>
      <ProductProvider>
        <div className="App">
          <Header />
          <ProductList />
          <Footer />
        </div>
      </ProductProvider>
    </AuthProvider>
  );
}

const Header = () => {
  const { token, logout } = useAuthContext();
  return (
    <header>
      <h1>商品リスト</h1>
      {token && <button onClick={logout}>ログアウト</button>}
    </header>
  );
};

const ProductList = () => {
  const { products, setProducts } = useProductContext();
  const { token, login } = useAuthContext();
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [newProduct, setNewProduct] = React.useState({ name: '', price: '' });
  const [credentials, setCredentials] = React.useState({ username: '', password: '' });

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await fetch('http://localhost:5000/products');
        if (!response.ok) throw new Error('データの取得に失敗しました');
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchProducts();
  }, [setProducts]);

  const handleAddProduct = async (e) => {
    e.preventDefault();
    if (!token) return alert('ログインしてください');
    try {
      const response = await fetch('http://localhost:5000/products', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body: JSON.stringify({ name: newProduct.name, price: Number(newProduct.price) }),
      });
      if (!response.ok) throw new Error('商品の追加に失敗しました');
      const addedProduct = await response.json();
      setProducts((prev) => [...prev, addedProduct]);
      setNewProduct({ name: '', price: '' });
    } catch (err) {
      setError(err.message);
    }
  };

  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch('http://localhost:5000/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      if (!response.ok) throw new Error('ログインに失敗しました');
      const { token } = await response.json();
      login(token);
    } catch (err) {
      setError(err.message);
    }
  };

  if (loading) return <main>読み込み中...</main>;
  if (error) return <main>エラー: {error}</main>;

  return (
    <main>
      {!token ? (
        <form onSubmit={handleLogin}>
          <h2>ログイン</h2>
          <input
            type="text"
            placeholder="ユーザー名"
            value={credentials.username}
            onChange={(e) => setCredentials({ ...credentials, username: e.target.value })}
          />
          <input
            type="password"
            placeholder="パスワード"
            value={credentials.password}
            onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
          />
          <button type="submit">ログイン</button>
        </form>
      ) : (
        <>
          <h2>商品一覧</h2>
          <form onSubmit={handleAddProduct}>
            <input
              type="text"
              placeholder="商品名"
              value={newProduct.name}
              onChange={(e) => setNewProduct({ ...newProduct, name: e.target.value })}
            />
            <input
              type="number"
              placeholder="価格"
              value={newProduct.price}
              onChange={(e) => setNewProduct({ ...newProduct, price: e.target.value })}
            />
            <button type="submit">追加</button>
          </form>
          <ul>
            {products.map((product) => (
              <ProductItem key={product._id} name={product.name} price={product.price} />
            ))}
          </ul>
        </>
      )}
    </main>
  );
};

const ProductItem = ({ name, price }) => (
  <li>
    {name} - {price}</li>
);

const Footer = () => (
  <footer>
    <p>© 2025 React Web開発入門</p>
  </footer>
);

export default App;

動作確認

  1. バックエンドを起動(node index.js)。
  2. フロントエンドを起動(npm start)。
  3. /registerでユーザーを作成(PostmanなどでPOSTリクエスト)。
  4. ログイン後、商品追加が可能に。

次回予告

第8章では、アプリを本番環境にデプロイする方法を学びます。お楽しみに!


この記事が役に立ったら、ぜひ「いいね」や「ストック」をお願いします!質問や感想はコメント欄で気軽にどうぞ。次回も一緒に学びましょう!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?