/** * Password Validator - 비밀번호 정책 검증 * * 강력한 비밀번호 정책: * - 최소 12자 이상 * - 대문자 포함 * - 소문자 포함 * - 숫자 포함 * - 특수문자 포함 * * @author TK-FB-Project * @since 2026-02-04 */ /** * 비밀번호 강도 검증 * * @param {string} password - 검증할 비밀번호 * @param {Object} options - 옵션 (기본값 사용 권장) * @returns {Object} { valid: boolean, errors: string[], strength: string } */ const validatePassword = (password, options = {}) => { const config = { minLength: options.minLength || 12, requireUppercase: options.requireUppercase !== false, requireLowercase: options.requireLowercase !== false, requireNumbers: options.requireNumbers !== false, requireSpecialChars: options.requireSpecialChars !== false, maxLength: options.maxLength || 128 }; const errors = []; let strength = 0; // 필수 검증 if (!password || typeof password !== 'string') { return { valid: false, errors: ['비밀번호를 입력해주세요.'], strength: 'invalid' }; } // 길이 검증 if (password.length < config.minLength) { errors.push(`비밀번호는 최소 ${config.minLength}자 이상이어야 합니다.`); } else { strength += 1; } if (password.length > config.maxLength) { errors.push(`비밀번호는 ${config.maxLength}자를 초과할 수 없습니다.`); } // 대문자 검증 if (config.requireUppercase && !/[A-Z]/.test(password)) { errors.push('대문자를 1개 이상 포함해야 합니다.'); } else if (/[A-Z]/.test(password)) { strength += 1; } // 소문자 검증 if (config.requireLowercase && !/[a-z]/.test(password)) { errors.push('소문자를 1개 이상 포함해야 합니다.'); } else if (/[a-z]/.test(password)) { strength += 1; } // 숫자 검증 if (config.requireNumbers && !/\d/.test(password)) { errors.push('숫자를 1개 이상 포함해야 합니다.'); } else if (/\d/.test(password)) { strength += 1; } // 특수문자 검증 const specialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/; if (config.requireSpecialChars && !specialChars.test(password)) { errors.push('특수문자를 1개 이상 포함해야 합니다. (!@#$%^&*()_+-=[]{};\':"|,.<>/?)'); } else if (specialChars.test(password)) { strength += 1; } // 공백 검증 if (/\s/.test(password)) { errors.push('비밀번호에 공백을 포함할 수 없습니다.'); } // 연속된 문자 검증 (선택적) if (/(.)\1{2,}/.test(password)) { errors.push('동일한 문자를 3회 이상 연속 사용할 수 없습니다.'); } // 강도 계산 let strengthLabel; if (strength <= 2) { strengthLabel = 'weak'; } else if (strength <= 3) { strengthLabel = 'medium'; } else if (strength <= 4) { strengthLabel = 'strong'; } else { strengthLabel = 'very_strong'; } return { valid: errors.length === 0, errors, strength: strengthLabel, score: strength }; }; /** * 간단한 비밀번호 검증 (기존 호환용) * 모든 조건을 만족하면 true, 아니면 false * * @param {string} password - 검증할 비밀번호 * @returns {boolean} 유효 여부 */ const isValidPassword = (password) => { return validatePassword(password).valid; }; /** * 비밀번호 검증 결과를 한국어 메시지로 반환 * * @param {string} password - 검증할 비밀번호 * @returns {string|null} 오류 메시지 (유효하면 null) */ const getPasswordError = (password) => { const result = validatePassword(password); if (result.valid) { return null; } return result.errors.join(' '); }; /** * Express 미들웨어: 요청 body의 password 또는 newPassword 필드 검증 * * @param {string} fieldName - 검증할 필드명 (기본: 'password') * @returns {Function} Express 미들웨어 */ const validatePasswordMiddleware = (fieldName = 'password') => { return (req, res, next) => { const password = req.body[fieldName] || req.body.newPassword; if (!password) { return next(); // 비밀번호 필드가 없으면 다음 미들웨어로 } const result = validatePassword(password); if (!result.valid) { return res.status(400).json({ success: false, error: '비밀번호가 보안 요구사항을 충족하지 않습니다.', details: result.errors, code: 'WEAK_PASSWORD' }); } next(); }; }; module.exports = { validatePassword, isValidPassword, getPasswordError, validatePasswordMiddleware };