NestJS JWT MIDDLEWARE

LeeJaeHoon·2021년 12월 16일
post-thumbnail

MIDDLEWARE

먼저 jwt폴더 안에 jwt.middleware.ts파일을 만듭니다.

middleware를 class로 정의하거나 함수로도 정의할 수 있습니다.

nestjs의 미들웨어는 express와 같기때문에 next()를 해주어야 합니다.

import { NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

//implements는 해당 클래스가 interface로 행동하도록 한다.
// export class JwtMiddleware implements NestMiddleware {
//   use(req: Request, res: Response, next: NextFunction) {
//     console.log(req.headers);
//     next();
//   }
// }

export function jwtMiddleware(req: Request, res: Response, next: NextFunction) {
  console.log(req.headers);
  next();
}

사용

Applying middleware

@Module() 데코레이터에는 미들웨어를 위한 위치가 없습니다. 대신 모듈 클래스의 configure() 메소드를 사용하여 설정합니다. 미들웨어를 포함하는 모듈은 NestModule 인터페이스를 구현해야 합니다. AppModule 레벨에서 jwtMiddleware를 설정해 보겠습니다.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { jwtMiddleware } from './jwt/jwt.middleware';

@Module({
  ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(jwtMiddleware)
      .forRoutes({ path: '/graphql', method: RequestMethod.ALL });
  }
}

path로 지정된 url로 유저가 요청을 보낼때 미들웨어가 작동합니다.

Global middleware

미들웨어를 등록된 모든 경로에 한번에 바인딩하려면 INestApplication 인스턴스에서 제공하는 use() 메서드를 사용할 수 있습니다.

app.use에서는 argument로 들어간 값이 function일때만 가능합니다.

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { jwtMiddleware } from './jwt/jwt.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.use(jwtMiddleware);
  await app.listen(3000);
}
bootstrap();

jwt middleware 만들기

users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateAccountInput } from './dtos/create-account.dto';
import { LoginInput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { ConfigService } from '@nestjs/config';
import { JwtService } from 'src/jwt/jwt.service';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly users: Repository<User>,
    private readonly config: ConfigService,
    private readonly jwtService: JwtService,
  ) {}

  //create User
	 ...
  //Login User
	...
  async findById(id: number): Promise<User> {
    return this.users.findOne({ id });
  }
}

먼저 id를 통해 user를 찾는 메서드를 만듭니다.

users.module.ts

UsersService를 다른곳에서 의존성 주입을 하기위해서 exports해줍니다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsesrResolver } from './users.resolver';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsesrResolver, UsersService],
  exports: [UsersService],
})
export class UsersModule {}

jwt/jwt.service.ts

사용자는 서버에게서 토큰을 받은 후, 서버에게 요청을 보낼 때, request.Header에 토큰을 포함하여 요청을 보냅니다.

그러면 서버는 사용자에게서 받은 토큰이 유효한 것인지 확인해야합니다.

jwt.verify()함수를 이용하여 토큰 유효성을 확인할 수 있습니다.

jwt.verify() 함수에 들어가는 매개변수는 다음과 같습니다.

첫번째로 token : client에게서 받은 token

두번째로 secretkey : token 생성 시 사용했던 secretkey

import { Inject, Injectable } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';
import { JwtModuleOptions } from './jwt.interfaces';
import { CONFIG_OPTIONS } from './jwt.constants';

@Injectable()
export class JwtService {
  constructor(
    @Inject(CONFIG_OPTIONS)
    private readonly options: JwtModuleOptions,
  ) {
    console.log(this.options);
  }
  sign(userId: number): string {
    return jwt.sign({ id: userId }, this.options.privateKey);
  }
  verify(token: string) {
    return jwt.verify(token, this.options.privateKey);
  }
}

jwt/jwt.middleware.ts

if('x-jwt' in req.headers) 를 통해 header에 클라이언트에서 준 x-jwt이 있는지 확인후 있다면

jwtService에있는 verify메서드를 통해 jwt.sign으로 넘겨주었던 token값을 얻을 수 있습니다.

decoded를 콘솔에 찍어보면 { id: 3, iat: 1639162850 } 을 얻을 수 있습니다.

decoded에 id라는 프로퍼티를 가지고 있다면 userService의 findById메서드를 통해 해당 유저를 찾습니다.

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { UsersService } from 'src/users/users.service';
import { JwtService } from './jwt.service';

// implements는 해당 클래스가 interface로 행동하도록 한다.
@Injectable()
export class JwtMiddleware implements NestMiddleware {
  constructor(
    private readonly jwtService: JwtService,
    private readonly userService: UsersService,
  ) {}
  async use(req: Request, res: Response, next: NextFunction) {
    if ('x-jwt' in req.headers) {
      const token = req.headers['x-jwt'];
      try {
        const decoded = this.jwtService.verify(token.toString());
        if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
          const user = await this.userService.findById(decoded['id']);
          // 이 request는 HTTP request같은건데 이걸 graphql resolver에 전달해 줘야한다.
          req['user'] = user;
        }
      } catch (e) {}
    }
    next();
  }
}

req['user'] = user 이 request는 HTTP request같은건데 이걸 graphql resolver에 전달해 줘야합니다.

req.user = user이 아닌 req['user'] = user로 사용하는 이유 : 동적으로 object key를 활용할때는 obj[key]로 쓴다고 합니다. 또한 req에는 원래 user이라는 프로퍼티가 없기 때문에 타입스크립트에서 오류를 표시합니다.

GraphQLModule context

GraphQLModule.forRoot에는 context를 사용할 수 있습니다

context가 힘수로 정의되면 매 request마다 호출됩니다.

context는 req property를 포함한 object를 express로부터 받습니다.

즉 우리가 미들웨어로 넘겨준 req['user'] = user의 값을 context.user의 값으로 넘겨줍니다.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { jwtMiddleware } from './jwt/jwt.middleware';

@Module({
	...
  GraphQLModule.forRoot({
      autoSchemaFile: true, //메모리에 저장
      //request context는 각 request에서 사용이 가능하다
      //context기 힘수로 정의되면 매 request마다 호출된다.
      //이것은 req property를 포함한 object를 express로부터 받는다.
      context: ({ req }) => ({ user: req['user'] }),
   }),
	...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(jwtMiddleware)
      .forRoutes({ path: '/graphql', method: RequestMethod.ALL });
  }
}

Guard

nest g mo auth를 통해 auth모듈을 만듭니다.

auth/auth.guard.ts 파일 만들기

canActivate는 함수인데 true를 리턴하면 request를 진행시키고 false면 request를 멈추게합니다.

canActivate(context: ExecutionContext)의 context를 통해 request의 context에 접근할 수 있습니다.

하지만 문제는 context가 http로 되어 있다는 겁니다. 그래서 이것을 graphql로 바꿔줘야합니다.

GqlExecutionContext.create(context).getContext()을 통해 graphql context값을 가져올 수 있습니다.

user기 있을시 return true로 해줌으로써 request를 진행시키고 user가 없을시 리턴값을 false로 해줌으로써 request를 멈추게합니다.

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

// guard는 함수인데 request를 다음 단계로 진행할지 말지 결정한다.
// CanActivate는 함수인데 true를 리턴하면 request를 진핼시키고 false면 request를 멈추게한다
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const user = gqlContext['user'];
    if (!user) {
      return false;
    }
    return true;
  }
}

Guard사용

@UseGuards의 argument로 우리가 미들웨어로 만든 AuthGuard를 넣어줌으로 써 사용할 수 있습니다.

이제 Query로 me를 요청할때 토큰값이 있으면 request를 진행시키고 없으면 request를 멈추게합니다.

import { UseGuards } from '@nestjs/common';
import { Resolver, Query, Mutation, Args, Context } from '@nestjs/graphql';
import { AuthGuard } from 'src/auth/auth.guard';
import {
  CreateAccountInput,
  CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';

@Resolver((of) => User)
export class UsesrResolver {
  constructor(private readonly usesrService: UsersService) {}
	...
  @Query((returns) => User)
  @UseGuards(AuthGuard)
  me() {}
}

AuthUser Decorator 만들기

auth/auth-user.decorator.ts 파일을 만듭니다.

createParamDecorator은 factory function이 필요합니다.

factory function에는 항상 unknown value인 data와 context가 있습니다

이전에 Guard만들 때 사용한 context를 이용해 user을 불러오고 리턴해줍니다.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const AuthUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const user = gqlContext['user'];
    return user;
  },
);

AuthUser Decorator 사용

우리가 만든 AuthUser을 함수의 인자에 넣어 사용할 수 있습니다.

AuthUser에서 return한 값이 authUser에 들어갑니다.

import { UseGuards } from '@nestjs/common';
import { Resolver, Query, Mutation, Args, Context } from '@nestjs/graphql';
import { AuthGuard } from 'src/auth/auth.guard';
import {
  CreateAccountInput,
  CreateAccountOutput,
} from './dtos/create-account.dto';
import { LoginInput, LoginOutput } from './dtos/login.dto';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';

@Resolver((of) => User)
export class UsesrResolver {
  constructor(private readonly usesrService: UsersService) {}
	...
  @Query((returns) => User)
  @UseGuards(AuthGuard)
  me(@AuthUser() authUser: User) {
    //AuthUser에서 return한 값이 authUser에 들어간다.
    console.log(authUser);
    return authUser;
  }
}

0개의 댓글