DevBoi

[NestJS] Guard로, 인증 로직 구현 본문

Develop/[NestJs]

[NestJS] Guard로, 인증 로직 구현

HiSmith 2023. 6. 4. 00:15
반응형

Guard는 허용된 유저가 아니면, 요청 자체를 막아버리는 것이다.

서버의 리소스를 허용된 유저가 아니면 사용할 수 없도록 하는 것이고, 서버의 자원 낭비를 막을수 있게 된다.

 

decorator 문법을 사용하고, ts에서 실험기능에 포험되어있다고한다.

 

일단 가이드로 통용되고있는 구현체를 구현해보자

 

Guard의 기본로직은 아래와 같다.

 

1. 회원가입

2. 사용자 정보와 대응하는 jwt생성

3. 사용자가 서버에 요청을 보낼때 Header에 jwt를 담아서 보낸다.

4. Guard에서 확인

5. 유효한 값이면 통과, 아니면 에러를 일으킨다.

 

 

 

1. Authorization guard

사용자 인증은 가드의 대표적인 예시이다.

AuthGuard는 사용자의 헤더에 특정 토근이 제대로 들어있는지를 확인하는 것이다.

Guard는 canActivate함수를 구현해야한다. 그리고, 해당 함수는 true, false를 반환한다.

이는 비동기,동기로 구현할 수 있다. 

 

@Injectable()
export class AuthGuard implements CanActivate{
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();
        return validateRequest(request);
    }
    
}

 

위와 같이 코드를 하고 CanActivate에 대한 부분을 보자

파라미터로 가지는 ExecutionContext , 뭔가 실행 내용 메소드 정보 request 내용들을 보여주는 것같다. 

 

무튼 아래와 같이 생겼다.

import { Type } from '../index';
import { ArgumentsHost } from './arguments-host.interface';
/**
 * Interface describing details about the current request pipeline.
 *
 * @see [Execution Context](https://docs.nestjs.com/guards#execution-context)
 *
 * @publicApi
 */
export interface ExecutionContext extends ArgumentsHost {
    /**
     * Returns the *type* of the controller class which the current handler belongs to.
     */
    getClass<T = any>(): Type<T>;
    /**
     * Returns a reference to the handler (method) that will be invoked next in the
     * request pipeline.
     */
    getHandler(): Function;
}

 

상속 대상인 ArgumentHost는 아래와 같다.

export declare type ContextType = 'http' | 'ws' | 'rpc';
/**
 * Methods to obtain request and response objects.
 *
 * @publicApi
 */
export interface HttpArgumentsHost {
    /**
     * Returns the in-flight `request` object.
     */
    getRequest<T = any>(): T;
    /**
     * Returns the in-flight `response` object.
     */
    getResponse<T = any>(): T;
    getNext<T = any>(): T;
}
/**
 * Methods to obtain WebSocket data and client objects.
 *
 * @publicApi
 */
export interface WsArgumentsHost {
    /**
     * Returns the data object.
     */
    getData<T = any>(): T;
    /**
     * Returns the client object.
     */
    getClient<T = any>(): T;
}
/**
 * Methods to obtain RPC data object.
 *
 * @publicApi
 */
export interface RpcArgumentsHost {
    /**
     * Returns the data object.
     */
    getData<T = any>(): T;
    /**
     * Returns the context object.
     */
    getContext<T = any>(): T;
}
/**
 * Provides methods for retrieving the arguments being passed to a handler.
 * Allows choosing the appropriate execution context (e.g., Http, RPC, or
 * WebSockets) to retrieve the arguments from.
 *
 * @publicApi
 */
export interface ArgumentsHost {
    /**
     * Returns the array of arguments being passed to the handler.
     */
    getArgs<T extends Array<any> = any[]>(): T;
    /**
     * Returns a particular argument by index.
     * @param index index of argument to retrieve
     */
    getArgByIndex<T = any>(index: number): T;
    /**
     * Switch context to RPC.
     * @returns interface with methods to retrieve RPC arguments
     */
    switchToRpc(): RpcArgumentsHost;
    /**
     * Switch context to HTTP.
     * @returns interface with methods to retrieve HTTP arguments
     */
    switchToHttp(): HttpArgumentsHost;
    /**
     * Switch context to WebSockets.
     * @returns interface with methods to retrieve WebSockets arguments
     */
    switchToWs(): WsArgumentsHost;
    /**
     * Returns the current execution context type (string)
     */
    getType<TContext extends string = ContextType>(): TContext;
}

 

사용할때 더 상세히 보겠지만, Rpc나 Http 등 통신 중의 내용을 가지고 있다

즉 Request에 대한 전반적인 info들을 볼수 있을 것이다.

 

일단 임시로 사용중인 컨트롤러에 해당 가드를 붙여보자

  @UseGuards(AuthGuard)
  @Get()
  findAll() {
    return this.foodService.findAll();
  }

그리고 아까 가드에서 request내용을 콘솔로그로 찍어보자

@Injectable()
export class AuthGuard implements CanActivate{
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();
        console.log(request)
        return validateRequest(request);
    }
    
}

 

적용한 컨트롤러에 정상적으로 적용이 잘된것을 확인 할 수있다.

이제 그러면, 해당 인증 모듈을 좀 더 상세하게 구현해보자.

이걸 메소드 단위로 할지 컨트롤러 단위로 할지 글로벌 단위로 할지는 생각보다 간단하다.

아래 중에 선택해서 진행하면 된다.

 

 

좀 더 상세화된 기능을 구현하기 위해, passport를 설치해보자

npm i --save @nestjs/passport @types/passport-jwt

 

그리고 비밀번호 암호화 모듈, jwt모듈을 설치해준다.

npm install --save bcrypt @types/bcrypt
npm install --save @nestjs/jwt

 

일단 소스에 대한 개발이 완료가 되어 정리를 한다. Strategy에 대한 등록을 하지않아서, 헤맸던 것도 많지만

결국은 정리가되어서, 공유를 한다.

 

1.passport.jwt.strategy.ts

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy,ExtractJwt,VerifiedCallback } from "passport-jwt";
import { AuthService } from "./auth.service";
import { Payload } from "./payload.interface";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy){
    constructor(private authService : AuthService ){
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration : true,
            secretOrKey: 'sadfasdfasfasdfdasfasfdads'
        })
    }
    async validate(payload: Payload, done: VerifiedCallback): Promise<any> {
        console.log(payload)
        const user = await this.authService.tokenValidateUser(payload);
        console.log('JwtStrategy.validate')
        if(!user) {
            return new UnauthorizedException({message: 'user doew not exist'});
        }
        return user;
    }

}

 

2.authService.ts

import { HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { UserDTO } from '../user/dto/auth-user.dto';
import { User } from '../user/entities/user.entity';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';
import { Payload } from './payload.interface';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
    constructor(
        private userService: UserService,
        private jwtService: JwtService
    ){}

    async registerUser(newUser: UserDTO): Promise<UserDTO> {
        let userFind: UserDTO = await this.userService.findByFields({ 
            where: { username: newUser.username }
        });
        if(userFind) {
            throw new HttpException('Username aleady used!', HttpStatus.BAD_REQUEST);
        }
        return await this.userService.save(newUser);
    }

    async validateUser(userDTO: UserDTO): Promise<{accessToken: string} | undefined> {        
        let userFind: User = await this.userService.findByFields({
            where: { username: userDTO.username }
        });        
        console.log(userDTO.password)
        console.log(userFind.password)
        
        const validatePassword = (userDTO.password === userFind.password) ? true : false
        console.log(validatePassword)
        if(!userFind || !validatePassword) {
            throw new UnauthorizedException();
        }
        const payload: Payload = { id: userFind.id, username: userFind.username };
        return {
            accessToken: this.jwtService.sign(payload)
        };
    }
    async tokenValidateUser(payload: Payload): Promise<User| undefined> {
        console.log('tokenValidateUser')
        console.log(payload)
        return await this.userService.findByUserId(
           payload
        );
    }
}

 

3.auth-module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './passport.jwt.strategy';
import { User } from '../user/entities/user.entity';
import { UserService } from '../user/user.service';

@Module({
    imports: [
      PassportModule.register({defaultStrategy: 'jwt'}),
      TypeOrmModule.forFeature([User]),
      JwtModule.register({
        secret: 'sadfasdfasfasdfdasfasfdads',
        signOptions: {expiresIn: '300s'},
      }),
      PassportModule
    ],
    exports: [TypeOrmModule],
    controllers: [AuthController],
    providers: [AuthService, UserService,JwtStrategy]
  })
  export class AuthModule {}

 

4.auth.controller.ts

import { Body, Controller, Get, Post, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { AuthService } from './auth.service';
import { UserDTO } from '../user/dto/auth-user.dto';
import { JwtAuthGuard } from './auth-guard';

@Controller('auth')
export class AuthController {
    constructor(private authService: AuthService){}

    @Post('/register')
    async registerAccount(@Req() req: Request, @Body() userDTO: UserDTO): Promise<any> {
        return await this.authService.registerUser(userDTO);
    }

    @Post('/login')
    async login(@Body() userDTO: UserDTO, @Res() res: Response): Promise<any> {
        const jwt = await this.authService.validateUser(userDTO);
        res.setHeader('Authorization', 'Bearer '+jwt.accessToken);
        return res.json(jwt);
    }

    @Post('/authenticate')
    @UseGuards(JwtAuthGuard)
    isAuthenticated(@Req() req: Request): any {    
        const user: any = req.user;
        return user;
    }
}

 

5.payload.interface.ts

export interface Payload {
    id: number;
    username: string;
}

 

6.app-moudle.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { type } from 'os';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { FoodModule } from './food/food.module';
import { UserModule } from './user/user.module';
import { UserRepository } from './user/user.repository';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
import { JwtStrategy } from './auth/passport.jwt.strategy';
import { UserService } from './user/user.service';
import { AuthController } from './auth/auth.controller';
import { AuthModule } from './auth/auth.module';
import { User } from './user/entities/user.entity'
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/auth-guard';
const settings = require('../ormconfig.json');

@Module({
  imports: [FoodModule,
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'admin',
      password: 'admin',
      database: 'test',
      entities: [User],
      synchronize: true,
    }),    
    AuthModule,
    UserModule,
    
],
  controllers: [AppController],
  providers: [AppService],

})
export class AppModule {}

 

 

user.repository.ts

import { EntityRepository, Repository } from "typeorm";
import { User } from "./entities/user.entity";

@EntityRepository(User)
export class UserRepository extends Repository<User>{}

 

user-service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserDTO } from './dto/auth-user.dto';
import { FindOneOptions } from 'typeorm';
import {User} from './entities/user.entity'
import * as bcrypt from 'bcrypt';
import { UserRepository } from './user.repository';
import { InjectRepository } from "@nestjs/typeorm";
import { Payload } from 'src/auth/payload.interface';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
        private userRepository: UserRepository
  ){}
  
  create(createUserDto: CreateUserDto) {
    return 'This action adds a new user';
  }

  findAll() {
    return `This action returns all user`;
  }

  findOne(id: number) {
    return `This action returns a #${id} user`;
  }

  update(id: number, updateUserDto: UpdateUserDto) {
    return `This action updates a #${id} user`;
  }

  remove(id: number) {
    return `This action removes a #${id} user`;
  }


  async findByFields(options: FindOneOptions<UserDTO>): Promise<User | undefined> {
    return await this.userRepository.findOne(options);
}

  async save(userDTO: UserDTO): Promise<UserDTO | undefined> {
      return await this.userRepository.save(userDTO);
  }
  async findByUserId(payload: Payload): Promise<UserDTO | undefined> {

    return await this.userRepository.findOne(payload.id);
  }
}

 

위의 소스들을 수정하고, 빌드를 하면 정상적으로 실행이 된다.

크게 Guard와 JwtStrategy  관련 내용은 다음 포스팅에서 정리를 해서 올릴 예정이다.

 

반응형

'Develop > [NestJs]' 카테고리의 다른 글

[NestJs] Post , 생성  (0) 2023.06.18
[NestJS] Repository Pattern  (0) 2023.06.16
[NestJs] TypeOrm 변경 및 Repository  (0) 2023.05.30
[NestJS] MariaDB , TypeOrm 세팅  (0) 2023.05.29
[NestJS] 커스텀 파이프 개발  (0) 2023.05.27