인증 (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: true | 403 | { status: 403, message: "NO Token Provided" } |
잘못된 서명 + auth: true | 403 | { error: true, message: "invaild signature..." } |
만료된 토큰 + auth: true | 401 | { error: true, message: "Unauthorized access..." } |
검증 실패 + auth: false | — | 요청 계속 진행, req.user는 undefined |
클라이언트에서 토큰 사용하기
로그인 요청
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..."
}보호 라우트 요청
발급받은 accessToken을 Authorization 헤더에 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에 설정합니다. 인증이 “선택적”인 라우트(로그인한 사용자에게 추가 정보를 보여주는 경우 등)에 활용할 수 있습니다.
관련 문서
- Routing — HTTP 메서드 데코레이터와
IOptions - 사용자 CRUD — 회원가입부터 내 정보 조회까지 전체 구현
- Layered Architecture — Controller → Application → Entity 패턴