인증
ASAPJS는 라우트 수준 인증에 JSON Web Token(JWT)을 사용합니다. jwtVerification 미들웨어는 데코레이터 시스템을 통해 등록된 모든 라우트에 자동으로 적용됩니다. 엄격성은 각 라우트 데코레이터의 auth 옵션으로 제어합니다.
jwtVerification
jwtVerification은 @asapjs/router에서 내보내는 Express 미들웨어 팩토리입니다. 일반적인 사용에서는 직접 호출할 필요가 없습니다 — RouterController가 등록된 모든 라우트에 대해 내부적으로 호출합니다. IOptions의 auth 필드가 동작을 제어하는 공개 API입니다.
import { jwtVerification } from '@asapjs/router';시그니처
const jwtVerification = (auth?: boolean) =>
(req: Request, res: Response, next: NextFunction) => void| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
auth | boolean | false | true이면 유효한 Bearer 토큰이 필요합니다. false이면 토큰이 있을 경우 수락하지만 필수는 아닙니다. |
동작 방식
미들웨어는 모든 요청에서 다음 로직을 실행합니다:
- 수신된 요청에서
Authorization헤더를 읽습니다. - 헤더가 없는 경우:
auth === true—HttpException(403, 'NO Token Provided')를 throw합니다. 이 에러는 Express 에러 처리 파이프라인으로 전달됩니다.auth === false— 즉시next()를 호출합니다;req.user는 설정되지 않습니다.
- 헤더가 있는 경우,
Bearer <token>형식에서 토큰을 추출하고config.auth.jwt_access_token_secret을 사용하여 검증합니다. - 검증이 성공하면 — 디코딩된 페이로드를
req.user에 저장하고next()를 호출합니다. - 검증이 실패하고
auth === true인 경우 — 에러 유형에 따라 직접 응답을 전송합니다 (아래 응답 형식 참고). - 검증이 실패하고
auth === false인 경우 —req.user를 설정하지 않고next()를 호출합니다.
에러 응답 형식
jwtVerification은 에러 케이스에 따라 서로 다른 방식으로 응답합니다:
| 시나리오 | 상태 | 응답 경로 | 바디 |
|---|---|---|---|
토큰 없음, auth: true | 403 | throw HttpException → Wrapper errorToResponse() | { status: 403, errorCode: 'LEGACY_HTTP_EXCEPTION', message: 'NO Token Provided' } |
유효하지 않은 서명, auth: true | 403 | res.json() 직접 응답 | { error: true, message: 'invaild signature. please use vaild endpoint' } |
만료/유효하지 않은 토큰, auth: true | 401 | res.json() 직접 응답 | { error: true, message: 'Unauthorized access. Please Refresh Token' } |
실패, auth: false | — | 요청 계속 진행 | req.user는 undefined |
참고: 토큰 없음 케이스는
throw를 통해 에러 파이프라인(errorToResponse)으로 처리되어{ status, errorCode, message }형식이지만, JWT 검증 실패 케이스는res.json()으로 직접 응답하여{ error, message }형식입니다. 클라이언트 에러 처리 시 두 가지 형식을 모두 고려해야 합니다.
코드 내 오타: 현재 코드에
'invaild signature'(invalid의 오타)와'vaild endpoint'(valid의 오타)가 포함되어 있습니다. 이 문자열은 코드 수정 전까지 그대로 반환됩니다.
필수 설정
jwtVerification은 전역 설정 객체에서 시크릿을 읽습니다. 서버가 시작되기 전에 애플리케이션 설정에 auth.jwt_access_token_secret을 반드시 설정해야 합니다.
// 애플리케이션 설정 (Application.run()에 전달)
{
auth: {
jwt_access_token_secret: 'your-secret-key'
}
}설정은 런타임에 @asapjs/core의 getConfig()를 통해 접근합니다. 키가 없으면 첫 번째 인증 요청 시점에 JWT 검증이 오류를 던집니다.
라우트 데코레이터의 auth 옵션
모든 HTTP 데코레이터(@Get, @Post, @Put, @Delete)는 IOptions 인수에 auth 필드를 받습니다. 이것이 인증을 제어하는 주요 방법입니다:
import { Get, Post } from '@asapjs/router';
// 공개 라우트 — 토큰 불필요
@Get('/posts', {
title: 'List posts',
auth: false, // 기본값; 생략 가능
})
async getPosts({ paging }: ExecuteArgs) { ... }
// 보호된 라우트 — 유효한 Bearer 토큰 필요
@Post('/posts', {
title: 'Create post',
auth: true,
body: CreatePostDto,
})
async createPost({ body, user }: ExecuteArgs) { ... }auth를 생략하면 기본값은 false입니다.
JWT 토큰 형식
클라이언트는 Bearer 방식을 사용하여 Authorization 요청 헤더에 토큰을 전달해야 합니다:
Authorization: Bearer <jwt-token>미들웨어는 공백 문자를 기준으로 분리하여 두 번째 세그먼트를 토큰 문자열로 사용합니다. 다른 형식(예: Bearer 접두사 누락)은 서명 검증에 실패하는 인식되지 않는 토큰을 생성합니다.
HttpException
HttpException은 라우트 핸들러와 미들웨어 내에서 HTTP 오류 응답을 알리는 에러 클래스입니다.
import { HttpException } from '@asapjs/router';생성자
class HttpException extends Error {
public status: number;
public message: string;
constructor(status?: number, message?: string);
}| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
status | number | 500 | 클라이언트에 전송할 HTTP 상태 코드. |
message | string | '알 수 없는 서버 오류가 발생했습니다.' | 응답 바디에 포함되는 사람이 읽을 수 있는 오류 메시지. |
HttpException 던지기
라우트 핸들러나 서비스 메서드 내에서 HttpException을 던집니다. Wrapper 유틸리티가 이를 잡아 errorToResponse()를 통해 구조화된 JSON 응답을 전송합니다:
import { HttpException } from '@asapjs/router';
async login({ body }: ExecuteArgs) {
const user = await this.userService.findByEmail(body.email);
if (!user) {
throw new HttpException(404, 'User not found');
}
const valid = await user.verifyPassword(body.password);
if (!valid) {
throw new HttpException(401, 'Invalid credentials');
}
return this.userService.issueToken(user);
}오류 응답 형식
HttpException이 Wrapper에 의해 잡히면 errorToResponse()를 통해 처리됩니다. HttpException에는 errorCode가 없으므로 레거시 호환 코드가 사용됩니다:
{
"status": 404,
"errorCode": "LEGACY_HTTP_EXCEPTION",
"message": "User not found"
}타입 안전한 에러 코드와 데이터가 필요한 경우 @asapjs/error의 error() 팩토리를 사용하세요. 자세한 내용은 에러 처리 가이드를 참고하세요.
전체 컨트롤러 예제
import { RouterController, Get, Post, ExecuteArgs } from '@asapjs/router';
import { HttpException } from '@asapjs/router';
import UserApplication from '../application/UserApplication';
import CreateUserDto from '../dto/CreateUserDto';
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();
}
// 공개 — 토큰 불필요
@Post('/register', {
title: 'Register',
description: 'Creates a new user account.',
body: CreateUserDto,
response: UserInfoDto,
})
async register({ body }: ExecuteArgs) {
return await this.userService.register(body as CreateUserDto);
}
// 공개 — 토큰 불필요
@Post('/login', {
title: 'Login',
description: 'Authenticates with email and password, returns a JWT.',
body: LoginRequestDto,
response: LoginResponseDto,
})
async login({ body }: ExecuteArgs) {
return await this.userService.login(body as LoginRequestDto);
}
// 보호된 라우트 — 유효한 Bearer 토큰 필요
@Get('/me', {
title: 'Get current user',
description: 'Returns the profile of the authenticated user.',
auth: true,
response: UserInfoDto,
})
async getMe({ user }: ExecuteArgs) {
if (!user?.id) {
throw new HttpException(401, 'Token payload missing user id');
}
return await this.userService.getUserInfo(user);
}
}