Skip to Content
문서가이드인증 (Authentication)

인증 (Authentication)

ASAPJS는 JWT(JSON Web Token) 기반 인증을 내장하고 있습니다. 라우트 데코레이터의 auth 옵션 하나로 보호 라우트와 공개 라우트를 구분하며, 토큰 검증은 프레임워크가 자동으로 처리합니다.

이 가이드에서는 JWT 설정부터 로그인 구현, 보호 라우트 사용까지 전체 인증 흐름을 단계별로 구현합니다.


사전 준비

인증 기능을 사용하려면 애플리케이션 설정에 JWT 시크릿 키를 등록해야 합니다.

// example/src/index.ts import { Application } from '@asapjs/core'; const config = { name: 'ASAPJS Example', port: 3000, basePath: 'api', extensions: ['@asapjs/sequelize'], auth: { jwt_access_token_secret: process.env.JWT_SECRET || 'default-secret-key', }, }; const app = new Application(__dirname, config); app.run();

auth.jwt_access_token_secret 값은 런타임에 getConfig()를 통해 jwtVerification 미들웨어가 읽습니다. 이 값이 없으면 첫 번째 인증 요청에서 오류가 발생합니다.

주의: 프로덕션 환경에서는 반드시 환경 변수를 통해 시크릿 키를 주입하세요. default-secret-key 같은 하드코딩된 값은 개발용으로만 사용합니다.


Step 1: JWT 헬퍼 작성

토큰 서명과 검증을 담당하는 유틸리티를 작성합니다.

// example/src/utils/jwt.ts import jwt from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET || 'default-secret-key'; const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'; export interface JwtPayload { userId: number; email: string; } export function jwtSign(payload: JwtPayload, expiresIn: string = JWT_EXPIRES_IN): string { return jwt.sign(payload, JWT_SECRET, { expiresIn }); } export function jwtVerify(token: string): JwtPayload { try { return jwt.verify(token, JWT_SECRET) as JwtPayload; } catch (error) { throw new Error('Invalid or expired token'); } }

jwtSign은 Application 레이어에서 로그인 성공 시 호출합니다. jwtVerify는 필요에 따라 수동 검증이 필요할 때 사용합니다(라우트 레벨 검증은 프레임워크가 자동 처리).


Step 2: 로그인 DTO 정의

로그인 요청과 응답의 형태를 DTO로 정의합니다. DTO는 Swagger 문서와 타입 안전성을 동시에 제공합니다.

// example/src/user/dto/LoginRequestDto.ts import { ExtendableDto, TypeIs } from '@asapjs/sequelize'; import UsersTable from '../domain/entity/UsersTable'; export default class LoginRequestDto extends ExtendableDto { @TypeIs.STRING({ comment: '이메일' }) email: string; @TypeIs.PASSWORD({ comment: '비밀번호' }) password: string; }
// example/src/user/dto/LoginResponseDto.ts import { ExtendableDto, TypeIs } from '@asapjs/sequelize'; import UsersTable from '../domain/entity/UsersTable'; export default class LoginResponseDto extends ExtendableDto { @TypeIs.STRING({ comment: 'Access Token' }) accessToken: string; @TypeIs.STRING({ comment: 'Refresh Token' }) refreshToken: string; }
  • TypeIs.PASSWORD는 Swagger 문서에서 format: password로 표시됩니다.
  • defineTable은 DTO가 어떤 테이블과 연관되는지 메타데이터로 지정합니다.

Step 3: 로그인 비즈니스 로직

Application 레이어에서 이메일/비밀번호 검증과 JWT 발급을 처리합니다.

// example/src/user/application/UserApplication.ts import bcrypt from 'bcrypt'; import UsersTable from '../domain/entity/UsersTable'; import LoginRequestDto from '../dto/LoginRequestDto'; import { jwtSign } from '../../utils/jwt'; export default class UserApplication { private users: typeof UsersTable; constructor() { this.users = UsersTable; } async login(dto: LoginRequestDto) { // 사용자 조회 const user = await this.users.findOne({ where: { email: dto.email } }); if (!user) { throw new Error('Invalid email or password'); } // 비밀번호 검증 const isPasswordValid = await bcrypt.compare(dto.password, user.password); if (!isPasswordValid) { throw new Error('Invalid email or password'); } // JWT 토큰 생성 const payload = { userId: user.id, email: user.email }; const accessToken = jwtSign(payload, '1d'); const refreshToken = jwtSign(payload, '7d'); return { accessToken, refreshToken, }; } async getUserInfo(user: any) { if (!user || !user.userId) { throw new Error('Unauthorized'); } const userRecord = await this.users.findByPk(user.userId); if (!userRecord) { throw new Error('User not found'); } return { id: userRecord.id, email: userRecord.email, name: userRecord.name, }; } }

핵심 포인트:

  • bcrypt.compare로 해시된 비밀번호와 비교합니다.
  • 이메일이 없거나 비밀번호가 틀려도 동일한 에러 메시지를 반환하여 보안을 강화합니다.
  • jwtSign으로 access token(1일)과 refresh token(7일)을 각각 발급합니다.

Step 4: 컨트롤러에서 auth 옵션 사용

@Post@Get 데코레이터의 auth 옵션으로 공개/보호 라우트를 구분합니다.

// example/src/user/controller/UserController.ts import { RouterController, Get, Post, ExecuteArgs } from '@asapjs/router'; import UserApplication from '../application/UserApplication'; import LoginRequestDto from '../dto/LoginRequestDto'; import LoginResponseDto from '../dto/LoginResponseDto'; import UserInfoDto from '../dto/UserInfoDto'; export default class UserController extends RouterController { public tag = 'User'; public basePath = '/users'; private userService: UserApplication; constructor() { super(); this.registerRoutes(); this.userService = new UserApplication(); } // 공개 라우트 — auth 생략 (기본값 false) @Post('/login', { title: '로그인', description: '이메일과 비밀번호로 로그인합니다', body: LoginRequestDto, response: LoginResponseDto, }) async login({ body }: ExecuteArgs) { const dto = body as LoginRequestDto; return await this.userService.login(dto); } // 보호 라우트 — auth: true @Get('/me', { title: '내 정보 조회', description: '현재 로그인한 사용자의 정보를 조회합니다', auth: true, response: UserInfoDto, }) async getMe({ user }: ExecuteArgs) { return await this.userService.getUserInfo(user); } }
  • auth 를 생략하거나 false로 설정하면 토큰 없이 접근할 수 있습니다.
  • auth: true로 설정하면 유효한 Bearer 토큰이 필수입니다.
  • 보호 라우트에서 ExecuteArgs.user에는 JWT 디코딩된 페이로드가 자동으로 주입됩니다.

jwtVerification 미들웨어 동작 원리

auth 옵션은 내부적으로 jwtVerification 미들웨어를 통해 처리됩니다. 라우트가 등록될 때 프레임워크가 자동으로 이 미들웨어를 삽입합니다.

// packages/router/src/express/router.ts (248행) this.expressRouter[method](path, jwtVerification(auth), ...middleware, Wrapper(excute));

jwtVerification의 요청 처리 흐름:

요청 수신 ├─ Authorization 헤더 없음 │ ├─ auth === true → 403 "NO Token Provided" │ └─ auth === false → next() (req.user 미설정) └─ Authorization 헤더 있음 → Bearer 토큰 추출 → jwt.verify() ├─ 검증 성공 → req.user = decoded → next() └─ 검증 실패 ├─ auth === true │ ├─ invalid signature → 403 │ └─ expired/기타 → 401 └─ auth === false → next() (req.user 미설정)

에러 응답 형식

상황HTTP 상태응답
토큰 없음 + auth: true403{ status: 403, message: "NO Token Provided" }
잘못된 서명 + auth: true403{ error: true, message: "invaild signature..." }
만료된 토큰 + auth: true401{ error: true, message: "Unauthorized access..." }
검증 실패 + auth: false요청 계속 진행, req.userundefined

클라이언트에서 토큰 사용하기

로그인 요청

curl -X POST http://localhost:3000/api/users/login \ -H "Content-Type: application/json" \ -d '{"email": "alice@example.com", "password": "secret123"}'

응답:

{ "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "eyJhbGciOiJIUzI1NiIs..." }

보호 라우트 요청

발급받은 accessTokenAuthorization 헤더에 Bearer 스킴으로 첨부합니다.

curl http://localhost:3000/api/users/me \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

응답:

{ "id": 1, "email": "alice@example.com", "name": "Alice" }

auth 옵션 패턴 정리

// 공개 라우트 — 누구나 접근 가능 @Get('/posts', { title: '게시글 목록', auth: false }) // 보호 라우트 — 로그인 필수 @Post('/posts', { title: '게시글 작성', auth: true }) // 선택적 인증 — auth 생략 (기본값 false) // 토큰이 있으면 req.user에 디코딩, 없어도 에러 없이 진행 @Get('/posts/:id', { title: '게시글 조회' })

auth를 생략한 경우에도 클라이언트가 Authorization 헤더를 보내면 토큰을 디코딩하여 req.user에 설정합니다. 인증이 “선택적”인 라우트(로그인한 사용자에게 추가 정보를 보여주는 경우 등)에 활용할 수 있습니다.


관련 문서

Last updated on