はじめに
第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/src
にAuthContext.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;
動作確認
- バックエンドを起動(
node index.js
)。 - フロントエンドを起動(
npm start
)。 -
/register
でユーザーを作成(PostmanなどでPOSTリクエスト)。 - ログイン後、商品追加が可能に。
次回予告
第8章では、アプリを本番環境にデプロイする方法を学びます。お楽しみに!
この記事が役に立ったら、ぜひ「いいね」や「ストック」をお願いします!質問や感想はコメント欄で気軽にどうぞ。次回も一緒に学びましょう!