2023. 6. 26. 20:41ㆍ개발 문서 번역/NestJS
이 포스팅은 「NestJS 기초실무 가이드 : 강력하고 쉬운 Node.js 웹 프레임워크로 웹사이트 만들기」
(서명: NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式)
책이 출간되었습니다.
앞전의 포스팅에서 회원가입과 로그인 처리에 대한 부분을 완성하였습니다.
하지만 로그인 처리가 끝난 후의 신원확인(인증)와 관련된 부분도 반드시 처리해주어야합니다.
왜 이런 처리가 필요한걸까요? 만약 웹사이트가 회원가입과 로그인 기능만을 제공한다면
사용자가 로그인한 후 회원과 관련 있는 기능을 사용한다고 하면 서버는 어떻게 해당 사용자가 누군지 알수 있을까요?
이런 사용자 식별 방법에는 여러가지가 있지만 Token을 사용한 인가방식이 가장 많이 채택되어 사용되고 있습니다.
Token의 개념
Token은 신분을 표현해주는 매개체와 같습니다.
사용자가 로그인에 성공했다면 서버는 값이 유일한 Token을 하나 발행하고 해당 Token을사용자에게 반환해주어야 합니다.
Token이 유효하다면 사용자의 요청엔 Token이 포함되어 전송되며, 서버는 현재 유저가 누구인지 분별할 수 있게 됩니다.
요 몇년사이 Token 기술은 굉장히 많이 쓰이고 있으며, 정식 명칭은 Json Web Token (약칭: JWT)
본 포스팅에선 JWT를 통해 인가를 구현해보겠습니다.
JWT란 무엇인가요?
JWT란 Token의 설계방법중 하나입니다. 가장 큰 특징으로는 Token 안에는 민감하지 않은 사용자의 정보(ex: 사용자 이름, 성별 등...) JWT는 Base64를 통해 인코딩되는데 이 사용자 정보들을 Base64를 통해 누구나 디코딩할 수 있기 때문입니다.
Token 방식을 사용하신다면 반드시 주의해야합니다.
일반적인 JWT의 양식은 아래와 같은 모습입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6IjY0OTZmZTQ5MzA3ZTQ3NmM5OWEzMjY3OSIsInVzZXJuYW1lIjoid3RoMjA1MiIsImlhdCI6MTY4Nzc3NDE0NCwiZXhwIjoxNjg3Nzc0MjA0fQ.
6Y_obQN2x2S6YWU5MMgXK5PI5AudJMW-UWEKaq1sOZE
"."을 통해 총 3개의 단락으로 구분되어 있는것을 확인할 수 있습니다.
이 3개의 단락 모두 Base64를 통해 디코딩할 수 있으며, 각각 다른 내용들을 담고 있습니다.
헤더(Header, 表頭)
헤더는 JWT의 첫 단락이며, 안에는 "암호화 알고리즘" 과 "Token 유형"이 포함되어있습니다.
위의 JWT 헤더는 다음과 같은 정보들을 포함하고 있습니다.
{
"alg": "HS256",
"typ": "JWT"
}
페이로드(Payload, 內容)
JWT의 두번째 단락입니다. 이곳에는 일반적으로 간단한 사용자 정보가 들어가있으며 위에 JWT가 디코딩되었을 경우 아래와 같은 정보를 포함하고 있을것입니다.
ㅇ
{
"id": "6496fe49307e476c99a32679",
"username": "wth2052",
"iat": 1687774144,
"exp": 1687774204
}
시그니처(Signature, 簽章)
JWT의 세번째 단락입니다. Token의 변조를 방지하기 위해 쓰이며, 백엔드단에서는 시크릿 키를 통해 JWT에 서명을 진행합니다. 시크릿 키는 절대로 절대로 다른 사람에게 알려져서는 안되는 중요한 존재입니다.
JWT 설치
시작하기 전에 먼저 npm을 통해 JWT와 관련있는 패키지들을 설치하겠습니다.
Nest가 패키징한 모듈인 passport-jwt와 타입이 정의된 파일들이 필요합니다.
$ npm install @nestjs/jwt passport-jwt
$ npm install @types/passport-jwt -D
JWT 검증 구현
먼저 Token에 서명을 위해서는 시크릿 키가 필요한데요. 해당 시크릿 키를 .env파일에 보관하도록 하겠습니다.
JWT_SECRET=YOUR_SECRET
src/config 폴더 아래 secret.config.ts 파일을 생성하여 시크릿 키를 secrets이라는 환경변수에 등록하도록 하겠습니다.
import { registerAs } from '@nestjs/config';
export default registerAs('secrets', () => {
const jwt = process.env.JWT_SECRET;
return { jwt };
});
app.module.ts에 적용시켜봅시다.
import { APP_PIPE } from '@nestjs/core';
import { Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './features/user/user.module';
import { AuthModule } from './features/auth/auth.module';
import MongoConfigFactory from './config/mongo.config';
import SecretConfigFactory from './config/secret.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [MongoConfigFactory, SecretConfigFactory], // ConfigModule 적용
isGlobal: true,
}),
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri: config.get<string>('mongo.uri'),
}),
}),
UserModule,
AuthModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
JWT 생성
시크릿 키 설정이 완료되었다면 JWT를 생성해봅시다! AuthModule에 JwtModule을 가져와 registerAsync 메서드를 통해 JWT를 설정을 구성해보겠습니다. 가장 중요한것은 시크릿 키를 전달하는 것입니다.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '../user';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
PassportModule,
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const secret = config.get('secrets.jwt');
return {
secret,
signOptions: {
expiresIn: '60s',
},
};
},
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
주의: 이 포스팅에선 간단한 신원 확인 기능만 구현하겠습니다. 자세한 JwtModule설정과 관련된 사항은
공식 문서 혹은 node-jsonwebtoken 페이지를 참고해주세요.
앞전 포스팅에서는 사용자가 로그인 한 후에 사용자에 대한 데이터를 받게 했지만
이번 포스팅에서는 JWT를 반환하도록 수정하고 사용자가 해당 JWT를 통해 회원만이 사용할 수 있는 기능들을 사용할 수 있게 해보겠습니다.
AuthService에 generateJwt 메서드를 통해 JwtService의 sign 메서드를 적용해 JWT를 생성하고
해당 메서드에 '페이로드'에 담을 자료들을 정의하겠습니다. 이번 포스팅에선 사용자의 id와 username을 정의하겠습니다.
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CommonUtility } from '../../core/utils/common.utility';
import { UserDocument } from '../../common/models/user.model';
import { UserService } from '../user';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UserService,
) {}
async validateUser(username: string, password: string) {
const user = await this.userService.findUser({ username });
const { hash } = CommonUtility.encryptBySalt(
password,
user?.password?.salt,
);
if (!user || hash !== user?.password?.hash) {
return null;
}
return user;
}
generateJwt(user: UserDocument) {
const { _id: id, username } = user;
const payload = { id, username };
return {
access_token: this.jwtService.sign(payload),
};
}
}
전 포스팅에서 설정했던 LocalStrategy를 통해 반환하는 값은 username과 email 뿐이었습니다.
하지만 이는 JWT를 생성하기 위한 자료와 맞지 않기때문에
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string) {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
사용자 전체를 반환하도록 하겠습니다.
주의: validate 메서드는 주요 데이터들만 반환하는 것이 가장 좋습니다.
AuthController의 signin 메서드엔 generateJwt의 결과를 반환하겠습니다.
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
import { UserDocument } from '../../common/models/user.model';
import { CreateUserDto, UserService } from '../user';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}
@Post('/signup')
signup(@Body() user: CreateUserDto) {
return this.userService.createUser(user);
}
@UseGuards(AuthGuard('local'))
@Post('/signin')
signin(@Req() request: Request) {
return this.authService.generateJwt(request.user as UserDocument);
}
}
Postman으로 테스트하여 로그인에 성공한다면 아래와 같이 access_token이 발급된 모습을 확인할 수 있습니다.
JWT 검증
이어서 JwtStrategy와 passport를 연결해보겠습니다. LocalStrategy를 구현하는 방식은 비슷합니다.
반드시 passport-jwt의 strategy를 상속받아야 하는건 같으며, 조금 다른건 super에 파라미터를 몇개 입력해야 하는데
src/features/auth/stragegies 폴더 아래 jwt.strategy.ts를 새로 만들어보겠습니다.
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('secrets.jwt'),
});
}
validate(payload: any) {
const { id, username } = payload;
return { id, username };
}
}
super에는 3개의 파라미터가 입력되있음을 확인할 수 있습니다.
1. jwtFromRequest: JWT을 요청의 어디에서 추출할껀지 정합니다. 여기서는 ExtractJwt를 통해 설정을 도울 수 있습니다.
2. ignoreExpiration: 유효기간이 지난 JWT를 무시할 것인지 설정할 수 있습니다. 기본값은 false입니다.
3. secretOrKey: JWT 시그니처가 사용하는 시크릿 키를 입력할 수 있습니다.
주의: 더 많은 파라미터에 대한 내용은 공식 홈페이지를 참고해주세요.
validate 이 메서드를 주의해서 볼 필요가 있습니다.
JWT는 유효성과 Token 만료 여부를 확인하는 과정을 거치기 때문에 여기서 추가적인 검증을 수행할 필요는 없습니다.
하지만 필요하다면 데이터베이스에서 더 많은 사용자 정보를 가져오는것도 가능합니다.
JwtStrategy를 완성한 후엔 AuthModule의 providers안에 추가해주도록 합시다.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '../user';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
PassportModule,
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const secret = config.get('secrets.jwt');
return {
secret,
signOptions: {
expiresIn: '60s'
}
};
},
})
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}
마지막으로 JWT 인증을 적용하기 위해선 사용자 데이터를 가져오는 API를 설계해야 합니다.
CLI를 통해 UserController를 생성하겠습니다.
$ nest generate controller features/user
user.controller.ts를 아래와 같이 수정하겠습니다.
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@UseGuards(AuthGuard('jwt'))
@Get(':id')
async getUser(@Param('id') id: string) {
const user = await this.userService.findUser({ _id: id });
const { password, ...others } = user.toJSON();
return others;
}
}
getUser 메서드에 AuthGuard를 적용시키고 jwt 전략을 사용하여 id로 데이터베이스에서 검색을 진행하여 UserDocument를 받아온 후 먼저 JSON 양식으로 바꾸겠습니다.
바꾼 후엔 passport를 제외한 모든 속성들을 프론트단에 반환해주겠습니다.
먼저 Postman을 통해 access_token을 발급받아
Bearer token을 함께 사용해 사용자 데이터를 가져오는 API를 테스트해보겠습니다.
만약 Token이 일치하지 않는경우엔 다음과 같은 화면이 나타납니다.
마치며
이틀간 간단한 인증/인가 기능에 대해 배워보았습니다.
이젠 passport에 대한 기본 개념과 사용 방법이 익숙해지셨으리라 믿습니다.
관심있는 독자분들은 Facebook과 Google 로그인 방식에 대해서도 알아보시기 바랍니다.
아래는 오늘 학습의 요약본입니다.
1. JWT는 Token의 설계 방법 중 하나입니다.
가장 큰 특징은 Token 내에 사용자 정보를 담을 수 있다는 것 입니다.
2. JWT는 Base64를 통해 인코딩됩니다.
3. JWT는 "."을 통해 총 3개의 단락으로 나뉘며 헤더, 페이로드, 시그니처로 나뉩니다.
4. JWT는 시크릿 키가 필요합니다. 해당 시크릿 키는 무슨일이 있어도 유출되면 안됩니다.
5. JwtModule은 시크릿 키 설정, 유효기간 등 파라미터, JWT를 생성하는데 쓰입니다.
6. JwtStrategy의 super는 파라미터들을 지정하여 JWT Token에 대한 인증 방식을 정의하곤 합니다.
'개발 문서 번역 > NestJS' 카테고리의 다른 글
NestJS 帶你飛! 시리즈 번역 26# Swagger(상) (0) | 2023.07.01 |
---|---|
NestJS 帶你飛! 시리즈 번역 25# Authentication & RBAC (0) | 2023.06.29 |
NestJS 帶你飛! 시리즈 번역 23# Authentication(상) (0) | 2023.06.25 |
NestJS 帶你飛! 시리즈 번역 22# MongoDB (0) | 2023.06.20 |
NestJS 帶你飛! 시리즈 번역 21# HTTP Module (0) | 2023.06.18 |