Implementing Password Reset: Claude Code 활용 가이드
implementing password reset: Claude Code 활용. 실용적인 코드 예시를 포함합니다.
비밀번호리셋機能をClaude Code로 구현하기
비밀번호리셋はほぼ모든Web앱に必要な機能ですが、보안上の落とし穴も多い領域です。Claude Code를 활용하면 토큰の安全な생성・검증、レート制限、メール전송を含む堅牢な구현を구축할 수 있습니다。
리셋フローの全体像
- 사용자が이메일 주소を입력
- 서버が리셋토큰を생성しメール전송
- 사용자がメール内のリンクを클릭
- 新しい비밀번호を입력して업데이트
> セキュアな비밀번호리셋機能를 구현해줘。
> 토큰の유효期限は1시간、使用は1回のみ。
> レート制限とメール전송も含めて。
토큰생성とメール전송
// src/services/password-reset.ts
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { prisma } from '@/lib/prisma';
import { sendEmail } from '@/lib/email';
export class PasswordResetService {
private static TOKEN_EXPIRY_HOURS = 1;
async requestReset(email: string) {
const user = await prisma.user.findUnique({ where: { email } });
// 사용자が存在しなくても同じ응답を返す(情報漏洩防止)
if (!user) return { success: true };
// レート制限: 同一メールへの전송は5分に1回まで
const recentRequest = await prisma.passwordReset.findFirst({
where: {
userId: user.id,
createdAt: { gt: new Date(Date.now() - 5 * 60 * 1000) },
},
});
if (recentRequest) return { success: true };
// セキュアな토큰생성
const token = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
// 古い토큰をDelete
await prisma.passwordReset.deleteMany({ where: { userId: user.id } });
// 新しい토큰を저장
await prisma.passwordReset.create({
data: {
userId: user.id,
token: hashedToken,
expiresAt: new Date(
Date.now() + this.constructor.TOKEN_EXPIRY_HOURS * 60 * 60 * 1000
),
},
});
// 리셋メール전송
const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`;
await sendEmail({
to: email,
subject: 'パスワードリセットのご案内',
html: `
<h2>パスワードリセット</h2>
<p>以下のリンクをクリックしてパスワードをリセットしてください。</p>
<a href="${resetUrl}" style="display:inline-block;background:#3B82F6;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;">
パスワードをリセット
</a>
<p style="color:#666;font-size:14px;margin-top:16px;">
このリンクは1時間で有効期限が切れます。<br />
心当たりがない場合は、このメールを無視してください。
</p>
`,
});
return { success: true };
}
async resetPassword(token: string, newPassword: string) {
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
const resetRecord = await prisma.passwordReset.findFirst({
where: {
token: hashedToken,
expiresAt: { gt: new Date() },
used: false,
},
});
if (!resetRecord) {
throw new Error('無効または期限切れのトークンです');
}
// 비밀번호の유효성 검사
if (newPassword.length < 8) {
throw new Error('パスワードは8文字以上である必要があります');
}
const hashedPassword = await bcrypt.hash(newPassword, 12);
// 트랜잭션で비밀번호업데이트と토큰무효化
await prisma.$transaction([
prisma.user.update({
where: { id: resetRecord.userId },
data: { password: hashedPassword },
}),
prisma.passwordReset.update({
where: { id: resetRecord.id },
data: { used: true },
}),
]);
return { success: true };
}
}
리셋폼のUI
// src/app/(auth)/reset-password/page.tsx
'use client';
import { useState } from 'react';
import { useSearchParams } from 'next/navigation';
export default function ResetPasswordPage() {
const searchParams = useSearchParams();
const token = searchParams.get('token');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
setError('パスワードが一致しません');
return;
}
try {
const res = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, password }),
});
if (!res.ok) throw new Error((await res.json()).error);
setStatus('success');
} catch (err: any) {
setError(err.message);
setStatus('error');
}
};
if (status === 'success') {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold mb-4">パスワードを更新しました</h2>
<a href="/login" className="text-blue-600 hover:underline">ログインページへ</a>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg w-full max-w-md">
<h1 className="text-2xl font-bold mb-6">新しいパスワード</h1>
{error && <p className="text-red-500 text-sm mb-4">{error}</p>}
<div className="space-y-4">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="新しいパスワード(8文字以上)"
className="w-full border rounded-lg px-4 py-3"
minLength={8}
required
/>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="パスワードの確認"
className="w-full border rounded-lg px-4 py-3"
required
/>
<button type="submit" className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium">
パスワードを更新
</button>
</div>
</form>
</div>
);
}
보안チェック리스트
- 토큰は暗号学的に安全な乱数で생성する
- DBにはハッシュ化した토큰を저장する
- 토큰の유효期限を設ける(1시간程度)
- 토큰は1回使用後に무효化する
- 사용자の存在有無を応答で漏らさない
関連글
인증全般の설계は인증機能の구현、二要素인증との組み合わせは二要素인증の구현를 확인하세요.
メール전송にはResend(resend.com)がシンプルでおすすめです。
#Claude Code
#password reset
#authentication
#security
#email
무료 제공
무료 PDF: 5분 완성 Claude Code 치트시트
이메일 주소만 등록하시면 A4 한 장짜리 치트시트 PDF를 즉시 보내드립니다.
개인정보는 엄격하게 관리하며 스팸은 보내지 않습니다.
이 글을 작성한 사람
Masa
Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.
관련 글
Use Cases
Claude Code 다국어 글을 매일 발행하기 전에 확인할 7가지
누락된 언어, 깨진 CTA, 반영되지 않은 배포를 막기 위해 다국어 Claude Code 글을 매일 발행하기 전에 확인할 체크리스트입니다.
Use Cases
Codex Automations란? 잠자는 동안 AI가 콘텐츠 운영을 처리하게 하는 방법
Codex Automations로 트래픽 분석, 주제 선정, 글 작성, CTA 개선, 배포까지 자동화하는 실전 가이드.
Use Cases
Claude Code × GCP Cloud Functions 완전 가이드 | 서버리스 함수 초고속 개발
Claude Code로 GCP Cloud Functions를 효율화. HTTP/Pub/Sub/Firestore 트리거 구현부터 로컬 테스트·배포 자동화까지, Masa의 실무 경험을 토대로 실제 코드로 해설.