nest 에서 authentication 흐름 살펴보기

With·2022년 8월 9일
0

nest.js

목록 보기
5/5

노마드코더 우버 잇츠 5.10 ~ 11 강의에 대한 정리 내용입니다.

authentication 정리

part 1

(1) 클라이언트에서 header를 통해 토큰을 서버로 보냄
(2) req.headers의 값을 사용해서 db 내에 있는 user의 정보를 얻기 위해 미들웨어를 생성함
(3) 생성한 미들웨어를 사용하기 위해서 app.module.ts에서 등록함

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

(3) 미들웨어에서는 jwtService를 이용해서 토큰을 verify()

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

@Injectable()
export class JwtMiddleware implements NestMiddleware {
  constructor(
    private readonly jwtService: JwtService,
    private readonly userService: UserService,
  ) {}
  async use(req: Request, res: Response, next: NextFunction) {
    if ('x-jwt' in req.headers) {
      const token = req.headers['x-jwt'];
      const decoded = this.jwtService.verify(token.toString());
      if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
        try {
          const user = await this.userService.findById(decoded['id']);
          req['user'] = user; // request 안에 user 라는 property를 새롭게 만들어 준 것
        } catch (e) {
          console.log(e);
        }
      }
    }

    // 동작이 끝나면 next()가 실행되도록 구현
    next();
  }
}

(4) verify()를 통해 payload를 알수있고, 거기에서 얻은 아이디를 통해 유저를 조회함. 유저를 조회할때는 userService를 이용함 (미들웨어 전체 코드 중에서 일부)
findById라는 메서드는 useService내에서 생성됐고, Repository를 통해 typeOrm의 findOne을 통해 db를 조회했다.

if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
  try {
    const user = await this.userService.findById(decoded['id']);
    req['user'] = user; // request 안에 user 라는 property를 새롭게 만들어 준 것
  } catch (e) {
    console.log(e);
  }

(5) 유저를 찾으면, req['user'] = user 를 통해서 request 객체에 user의 정보를 붙인다.
미들웨어의 역할은 여기에서 끝난다. req 이후에 미들웨어를 가장 먼저 만나기때문에 request 객체를 이렇게 다룰 수 있는 것이다. 만약 user의 정보가 없었다면 request객체에는 아무것도 붙는게 없게 될 것 이다. 아무튼 이렇게 미들웨어에서 변경된 request객체는 모든 resolver에서 사용할 수 있게 된다.

const user = await this.userService.findById(decoded['id']);
req['user'] = user; // request 안에 user 라는 property를 새롭게 만들어 준 것

part 2

(1) 개요
part1에서는 클라이언트로부터 받은 request.headers의 토큰을 조회하고, verify, 그리고 request객체에 추가하기 까지 했다. part2에서는 request객체에 추가한 user 정보를 바탕으로 다음 request 요청에 대한 후속 작업을 진행한다.

(2) GraphQLModule.forRoot() 를 통해 context를 조회할 수 있는데, 여기에서 request객체를 조회할 수 있다. 이 context는 apollo server의 context다. 그리고 여기에서 request에 우리가 원하는 정보를 추가할 수 있고, 여기에서 추가한 정보는 모든 Resolver에서 조회할 수 있다. 그리고 canActivatecontext에서도 조회할 수 있다.

GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
      context: ({ req }) => ({ user: req['user'] }), // 이렇게
    }),

(3) 한편 가드를 사용했는데, 가드의 역할도 미들웨어와 비슷하다. (차이점은 아래에서 설명)
가드안에서 implements CanActivate 하고, canActivate를 구현한다. canActivate는 true또는 false를 반환한다. 이 반환값에 따라 request의 프로세스가 이어지거나 또는 멈춘다. canActivate의 인자에서는 context를 받아올 수 있는데, 이것은 app.module.tsGrapthQLModule.forRoor({context}) 의 값이다. 안에 들어있는 데이터는 같지만, 형태가 다르다. app.module.ts는 gql context이고, canActivate에서 나온건 http context이다. 그래서 모양을 바꾸는 과정이 필요하다. (http ➡️ gql), (1) 에서 우리가 추가한 {user: req['user]}를 여기에서 확인할 수 있고, 이것을 활용해서 다음 로직을 구현할 수 있다.

여기에서 구현할 로직은 ExcutionContext에서 user 정보가 있으면, true를 반환한고 없으면 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;
    } else {
      return true;
    }
  }
}

(4) 가드를 거쳐 마지막 Resolver다. Resolver 에서는 한번더 데코레이터의 힘을 이용하는데 이것은 우리가 만든 커스텀 데코레이터이다. createParamDecorator를 이용해서 데코레이터를 만들 수 있다. 팩토리 function 에서는 datacontext를 가져올 수 있다. 단, context가 http context이기 때문에 gql context로 변환하는 과정이 필요하고, 유저가 있다면 해당 최종적으로 유저의 정보를 반환한다.

export const AuthUser = createParamDecorator(
  // 안에석 구현되는 함수를 factory function 이라고 한다.
  (data: unknown, context: ExecutionContextHost) => {
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const user = gqlContext['user'];
    return user;
  },
);

// 데코레이터가 사용될 때

@Query(() => User) // resolver
@UseGuards(AuthGuard) // 미들웨어 이후로 실행됨 ➡️ 유저의 정보가 있다면 req 진행, 없다면 block
me(@Authuser() authUser: User) // 데코레이터에서 반환하는 값을 변수로 잡아 사용
  return authUser;
}

이렇게 데코레이터를 통해서 최종적으로 user의 값을 반환받아 resolver에서 해당 값을 반환하면 클라이언트에게 그 값이 반환된다. 끝!!

기타 개념

  • 가드와 미들웨어의 차이
    가드와 미들웨어는 실행시기가 다르다.
    미들웨어는 next() 함수를 호출한 후 어떤 핸들러가 실행될지 알 수 없다. 반면에 가드는 excutionContext 인스턴스에 액세스 할 수 있으므로 다음에 실행될 작업을 정확히 알 고 있다. 그리고 가드는 미들웨어 바로 이후에 실행되어 2차적으로 인증을 할 수 있게 한다.

  • 모든 가드는 cacActivate 를 구현해야 한다. (implements CanActivate

profile
주니어 프론트엔드 개발자 입니다.

0개의 댓글