우리 대부분은 계정 복구 프로세스를 한 번 이상 경험했습니다. 비밀번호를 잊어버린 경우 새 비밀번호를 만들고 시스템에 다시 액세스하기 위한 절차가 필요합니다. 이 기사에서는 Node.js, Knex 및 일부 공개되지 않은 도구를 Express와 함께 사용하여 경로를 처리하고 필요한 작업을 수행하는 프로세스를 구현하는 데 중점을 둡니다.
라우터 구현, URL 매개변수 처리, 이메일이나 전화번호만 증거로 사용할 수 있는 경우 사용자에게 무엇을 보낼지 결정, 이메일 제출 관리, 보안 문제 해결 등을 다룹니다.
코딩을 시작하기 전에, 내 공개 페이지에서 액세스할 수 있는 동일한 코드베이스로 작업하고 있는지 확인하고 싶습니다.
이제 비밀번호 분실 흐름의 스키마를 살펴보세요.
서버는 비밀번호 재설정을 위한 유효한 링크가 포함된 이메일을 사용자 메일함으로 보내는 일을 담당하며 토큰과 사용자 존재도 확인합니다.
이메일 서비스를 활용하고 Node.js로 이메일을 보내려면 기존 종속성에 추가로 다음 패키지를 설치해야 합니다.
npm i --save nodemailer handlebars
Nodemailer : SMTP 또는 기타 전송 메커니즘을 사용하여 쉽게 이메일을 보낼 수 있는 강력한 모듈입니다.
핸들바 : 핸들바는 널리 사용되는 JavaScript용 템플릿 엔진입니다. 이를 통해 렌더링 시 데이터로 채워질 수 있는 자리 표시자로 템플릿을 정의할 수 있습니다.
이제 마이그레이션을 생성해야 하므로 제 경우에는 users
테이블에 forgot_password_token
새 열을 추가해야 합니다.
knex migrate:make add_field_forgot_password_token -x ts
생성된 파일에 코드를 설정했습니다.
import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); }
사용자 테이블의 비밀번호 분실 토큰 마이그레이션
그런 다음 최신 파일을 마이그레이션합니다.
knex migrate:knex
이제 forgot_password_token
users
테이블에 설정할 수 있습니다.
비밀번호 분실 및 재설정 로직을 처리하는 컨트롤러를 관리하려면 두 가지 경로를 설정해야 합니다. 첫 번째 경로는 비밀번호 찾기 프로세스를 시작하고, 두 번째 경로는 확인을 위해 URL에 토큰 매개변수를 기대하면서 재설정 프로세스를 처리합니다. 이를 구현하려면 src/routes/
디렉터리에 forgotPasswordRouter.ts
라는 파일을 만들고 다음 코드를 삽입하세요.
import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController);
비밀번호 분실 라우터
두 개의 컨트롤러가 이메일 전송 및 비밀번호 재설정 로직을 관리합니다.
클라이언트가 비밀번호를 잊어버린 경우 세션이 없습니다. 이는 이메일이나 기타 보안 식별자를 제외한 사용자 데이터를 얻을 수 없음을 의미합니다. 우리의 경우 비밀번호 재설정을 처리하기 위해 이메일을 보내고 있습니다. 그 논리를 컨트롤러에 설정할 것입니다.
forgotPasswordRouter.post('/', forgotPasswordController);
'비밀번호를 잊으셨나요?'를 기억하시나요? 일반적으로 로그인 양식의 모든 클라이언트 UI에 있는 로그인 양식 아래 링크가 있습니까? 이를 클릭하면 비밀번호 재설정을 요청할 수 있는 보기로 이동됩니다. 이메일만 입력하면 컨트롤러가 필요한 모든 절차를 처리해 줍니다. 다음 코드를 살펴보겠습니다.
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };
비밀번호 컨트롤러를 잊으셨나요?
본문에서 이메일을 받은 다음 UserModel.findByEmail
사용하여 사용자를 찾습니다. 사용자가 존재하는 경우 TokenService.sign
사용하여 JWT 토큰을 생성하고 만료일이 1일인 forgot_password_token
사용자에게 토큰을 저장합니다. 그런 다음 사용자가 자신의 비밀번호를 변경할 수 있는 토큰과 함께 적절한 링크가 포함된 메시지를 이메일로 보냅니다.
이메일을 보내려면 발신자가 될 새 이메일 주소를 만들어야 합니다.
Google로 이동하여 새 이메일 계정을 만든 다음 계정이 생성되면 Google 계정 관리 링크로 이동하세요. 아바타를 클릭하면 오른쪽 상단에서 찾을 수 있습니다. 그런 다음 왼쪽 메뉴에서 보안 항목을 클릭한 후 2단계 인증을 누르세요. 아래에서 앱 비밀번호 섹션을 찾을 수 있습니다. 화살표를 클릭하세요.
사용해야 하는 이름을 입력합니다. 제 경우에는 Nodemailer
설정하고 Create를 누릅니다.
생성된 비밀번호를 복사하여 .env
파일로 설정하세요. 두 가지 변수를 파일로 설정해야 합니다.
MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"
물론 info@company_name.com
과 같은 적절한 이메일을 받으려면 AWS SES 또는 기타 서비스와 함께 Google Workspace 또는 AWS Amazon WorkMail을 설정해야 합니다. 하지만 우리의 경우에는 간단한 Gmail 계정을 무료로 사용하고 있습니다.
.env
파일이 준비되었으므로 이메일 전송 서비스를 설정할 준비가 되었습니다. 컨트롤러는 생성된 토큰과 메시지 수신자 이메일 주소와 함께 서비스를 사용합니다.
await EmailService.sendPasswordResetEmail(email, token);
src/services/EmailService.ts
만들고 서비스에 대한 클래스를 정의해 보겠습니다.
export class EmailService {}
이제 초기 데이터로 nodemailer
와 함께 사용할 환경을 확보해야 합니다.
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; }
이메일 서비스
서비스 초기화를 처리해야 합니다. 나는 이전에 그것에 대해 썼습니다.
import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); };
서비스 초기화 중
이제 EmailService
클래스 내에서 초기화 생성을 진행해 보겠습니다.
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } }
이메일 서비스 초기화
nodemailer
라이브러리에서 제공하는 초기화 nodemailer.createTransport()
메소드가 있습니다. 이메일을 보내는 데 사용될 운송 개체를 생성합니다. 이 메소드는 전송기에 대한 구성 세부사항을 지정하는 인수로 옵션 객체를 허용합니다.
우리는 Google을 사용하고 있습니다: service: 'gmail'
이메일 서비스 제공업체를 지정합니다. Nodemailer는 다양한 이메일 서비스 제공업체에 대한 기본 지원을 제공하며 gmail
전송기가 Gmail의 SMTP 서버와 작동하도록 구성됨을 나타냅니다.
인증 auth
위해서는 이메일 서비스 제공업체의 SMTP 서버에 액세스하는 데 필요한 자격 증명을 설정해야 합니다.
user
의 경우 이메일을 보낼 이메일 주소로 설정해야 하며 해당 비밀번호는 앱 비밀번호에서 Google 계정에 생성되었습니다.
이제 서비스의 마지막 부분을 설정해 보겠습니다.
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } }
비밀번호 재설정 이메일 보내기
계속하기 전에 클라이언트가 이메일을 받을 때 적절한 호스트를 결정하는 것이 중요합니다. 이메일 본문에 토큰과의 링크를 설정하는 것이 필수적입니다.
import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; };
호스트 받기
템플릿의 경우 handlebars
사용하고 있으며 이를 위해 src/temlates/passwordResetTemplate.hbs
에 첫 번째 HTML 템플릿을 만들어야 합니다.
<!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html>
비밀번호 재설정 템플릿
이제 도우미와 함께 이 템플릿을 재사용할 수 있습니다.
import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); };
템플릿 도우미 생성
이메일을 향상시키기 위해 첨부 파일을 포함할 수도 있습니다. 이렇게 하려면 email_logo.png
파일을 src/assets
폴더에 추가하세요. 그런 다음 다음 도우미 함수를 사용하여 이메일 내에서 이 이미지를 렌더링할 수 있습니다.
import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; });
첨부파일 생성 도우미
해당 도우미를 모두 수집한 후에는 다음을 사용하여 이메일을 보낼 수 있어야 합니다.
const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, });
이 접근 방식은 상당한 확장성을 제공하므로 서비스에서 다양한 콘텐츠가 포함된 이메일을 보내는 데 다양한 방법을 사용할 수 있습니다.
이제 라우터로 컨트롤러를 트리거하고 이메일을 보내 보겠습니다. 이를 위해 나는
콘솔은 메시지가 전송되었음을 알려줍니다.
Message sent: <1k96ah55-c09t-p9k2–[email protected]>
받은편지함에서 새 메시지를 확인하세요.
비밀번호 재설정 링크에는 토큰과 호스트가 포함되어야 합니다.
http://localhost:3000/reset-password/<token>
이 메시지는 개발 프로세스와 관련되므로 포트 3000
여기에 지정됩니다. 이는 비밀번호 재설정을 위한 양식 처리를 담당하는 클라이언트도 개발 환경 내에서 작동함을 나타냅니다.
토큰은 해당 이메일을 보낸 사용자를 얻을 수 있는 TokenService를 사용하여 컨트롤러 측에서 검증되어야 합니다. 토큰을 사용하는 라우터를 복구해 보겠습니다.
forgotPasswordRouter.post('/reset/:token', resetPasswordController);
컨트롤러는 1시간으로 설정된 만료 시간에 따라 토큰이 유효하고 만료되지 않은 경우에만 비밀번호를 업데이트합니다. 이 기능을 구현하려면 src/controllers/
폴더로 이동하여 다음 코드가 포함된 resetPasswordController.ts
라는 파일을 만듭니다.
import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } };
비밀번호 컨트롤러 재설정
이 컨트롤러는 토큰을 수신하고, 이를 검증하고, 해독된 데이터에서 사용자 ID를 추출하고, 해당 사용자를 검색하고, 요청 본문에서 클라이언트가 보낸 새 비밀번호를 획득하고, 데이터베이스에서 비밀번호 업데이트를 진행합니다. 궁극적으로 이를 통해 클라이언트는 새 비밀번호를 사용하여 로그인할 수 있습니다.
이메일 서비스의 확장성은 비밀번호 업데이트를 표시하고 후속 로그인을 활성화하는 것과 같은 확인 또는 성공 메시지 전송과 같은 다양한 접근 방식을 통해 입증됩니다. 그러나 비밀번호 관리는 매우 어려운 일이며, 특히 애플리케이션 보안 강화가 필수적인 경우에는 더욱 그렇습니다.
토큰 비교, 이메일, 비밀번호 확인 등 비밀번호 변경을 허용하기 전 추가 확인을 포함하여 보안을 강화하는 데 사용할 수 있는 다양한 옵션이 있습니다.
또 다른 옵션은 서버 측에서 확인을 위해 코드가 사용자의 이메일로 전송되는 PIN 코드 시스템을 구현하는 것입니다. 이러한 각 조치에는 이메일 전송 기능을 활용하는 것이 필요합니다.
구현된 모든 코드는 다음에서 찾을 수 있습니다.
이 빌드로 자유롭게 실험을 수행하고 이 주제에 대해 어떤 점을 높이 평가하는지 피드백을 공유해 주세요. 매우 감사합니다.
여기에서 제가 이 기사에서 활용한 몇 가지 참고 자료를 찾을 수 있습니다.
여기에도 게시됨