JWT
JWT(JSON Web Token)는 당사자간의 정보를 JSON 개체로 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준(RFC 7519). 이 정보는 디지털 서명이되어 있으므로 확인하고 신뢰할 수 있다.
정보를 안전하게 전할 때 혹은 유저의 권한 등을 체크하기 위해 사용하는데 유용한 모듈.

. 을 기준으로 앞에부터 순서대로 HEADER, PAYLOAD, VERIFY SIGNATURE 로 구성

HEADER
PAYLOAD
VERIFY SIGNATURE
유저 로그인 -> 토큰 생성 -> 토큰 보관
위와 같이 사용 흐름이 되며
토큰 생성 과정에서는 유저 이름, 롤, 생성일, 만료일 등의 정보와 시크릿 텍스트를 합친 값을 해싱 알고리즘을 사용하여 토큰을 생성한다.
보관한 토큰을 요청을 보낼때 해더에 담아서 같이 보내준다.
그러면 클라이언트에서 온 Headers와 Payload 를 서버에서 가지고 있는 Secret Text를 사용하여 Signature 부분을 다시 생성하여 비교하여 일치하면 올바른 토큰인지 판별한다.
npm i @nestjs/jwt @nestjs/passport passport passport-jwt
@nestjs/jwt:
@nestjs/passport:
passport:
passport-jwt:
auth 모듈 imports에 넣어주기
// AuthModule
imports: [
JwtModule.register({
secret: 'Secret1234',
signOptions: {
expiresIn: 3600,
},
}),
secret: 토큰을 만들 때 이용하는 secret text
expiresIn: 정해진 시간 이후에는 토큰이 유효하지 않게 됨. 여기서 3600ms은 한시간 이후에 토큰이 유효하지 않도록 설정.
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }), // passport 모듈 추가
JwtModule.register({
secret: 'Secret1234',
signOptions: {
expiresIn: 3600,
},
}),
TypeOrmModule.forFeature([User]),
],
controllers: [AuthController],
providers: [AuthService, UserRepository]
})
export class AuthModule {}
auth service에서 signIn 함수에 토큰을 생성하는 로직을 추가하고 accessToken이라는 이름의 객체로 내려주자.
// auth service
...
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(UserRepository) private userRepository: UserRepository,
private jwtService: JwtService, // jwt service를 사용하기 위해 추가
) {}
...
async signIn(
authCredentialDto: AuthCredentialDto,
): Promise<{ accessToken: string }> {
const { username, password } = authCredentialDto;
const user = await this.userRepository.findOneBy({ username });
if (user && (await bcrypt.compare(password, user.password))) {
// 유저 토큰 생성 (Secret + Payload)
const payload = { username };
const accessToken = await this.jwtService.sign(payload);
return { accessToken };
} else {
throw new UnauthorizedException('login failed');
}
}
서버 실행 후 올바른 정보로 로그인하면 { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InF3ZTEiLCJpYXQiOjE2ODE3NDAyMjMsImV4cCI6MTY4MTc0MzgyM30.vpSvLmb-hxs4s0hh1KQlXqwxMCBBz0F6iA2SrMp-Bc0" } 다음과 같은 값을 받을 수 있다.
이 값을 https://jwt.io/ 로 가서 살펴볼 수 있었다.

npm i @types/passport-jwt// auth/jwt-strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
@Injectable() // 해당 내용을 다른 서비스에서 사용가능하도록 데코레이터 추가
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(UserRepository)
private userRepository: UserRepository,
) {
super({
secretOrKey: 'Secret1234',
// 토큰이 유효한지 체크할 것임
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// 토큰을 어떤 타입의 옵션으로 가져올 것인지 정의
});
}
async validate(payload) { // 유효한 유저정보인지 검증
const { username } = payload;
const user: User = await this.userRepository.findOneBy({ username }); // payload의 username을 통해 유저정보를 가져온다.
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: 'Secret1234',
signOptions: {
expiresIn: 3600,
},
}),
TypeOrmModule.forFeature([User]),
],
controllers: [AuthController],
providers: [AuthService, UserRepository, JwtStrategy], // JwtStrategy를 이 Auth 모듈에서 사용할 수 있도록 등록
exports: [JwtStrategy, PassportModule], // 다른 모듈에서 사용할 수 있도록 등록
})
export class AuthModule {}
NestJS 미들웨어
UseGuards를 통해 구현해보자.
UseGuards안에 @nestjs/passport에서 가져온 AuthGuard()를 이용하면 요청안에 유저 정보를 넣어줄 수 있다.
요청을 직접 확인해보자.
// auth Controller
@Post('/test')
@UseGuards(AuthGuard())
test(@Req() req) {
console.log('req: ', req);
}
authController에 test 메소드를 만들고 아래와 같이 요청을 보내면

콘솔에서 엄청 많은 req 객체안의 내용을 확인할 수 있다.
그중 살펴보면 중간에 다음과 같이 유저정보가 담겨있는 것을 확인할 수 있다.

이를 활용하여 req.user가 아니라 user라는 객체로 보내주도록 하기 위해 커스텀 데코레이터를 활용하자.
get-user.decorator.ts 작성하기
// auth/get-user.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { User } from './user.entity';
export const GetUser = createParamDecorator(
(data, ctx: ExecutionContext): User => {
const req = ctx.switchToHttp().getRequest(); // 요청 정보를 가져옴
return req.user; // req.user 반환
},
);
// auth controller
@Post('/test')
@UseGuards(AuthGuard())
test(@GetUser() user: User) { // createParamDecorator 를 사용했기 때문에 parameter 위치에서 사용
console.log('user: ', user);
}
다시 테스트하면 다음과 같이 user 라는 객체에 user 정보만 담아서 가져올 수 있는 것을 확인할 수 있다.

이렇게 커스텀 데코레이터도 이용해보고 nestjs의 미들웨어도 활용하여 요청안에 유저정보를 담아서 보낼 수 있게 되었다.
이제 다음에는 이를 활용하여 인증을 통해서 권한을 주는 부분을 작성해보자.