05) 개인 프로젝트) Auth인증 구현 Part 1

Leo·2021년 2월 11일
3

Project-01

목록 보기
5/12
post-thumbnail

참조 사이트

Auth 인증 구현

API를 만들기 위해서는 인증의 구현을 필수 사항입니다.
node.js의 인증 라이브러리인 Passport를 이용합니다.

Passport의 인증 절차

  • 사용자의 자격증명(id/pw, JWT 등)을 확인하여 인증합니다.
  • 인증 상태 관리(JWT와 같은 휴대용 토큰을 발행)
  • Request 경로 처리기에서 나중에 사용할 수 있도록 인증 된 사용자에 대한 정보를 첨부합니다.

이름, 암호 인증 구현

사용자의 이름과 암호로 인증을 합니다.
인증이 되면 서버는 인증을 증명하기 위해 후속 요청에서 인증 헤더에 전달자 토큰으로 전송할 수 있는 JWT(Json Web Token)를 발행합니다. 또한 유효한 JWT가 포함 된 요청에만 엑세스 할 수 있는 보호 경로를 생성합니다.

인증 구현을 위한 패키지 다운로드

사용자이름과 암호를 통해 인증을 구현하기 위해 [passport-local] 패키지를 설치해야합니다.

@types/passport-local을 설치하는 이유는 Typescript 작성에 도움을 주기 때문입니다.

$ npm i --save @nestjs/passport passport passport-local
$ npm i --save-dev @types/passport-local

Auth구현을 위한 Module과 Service 생성

auth의 내용을 작성하기 위하여 auth module,service를 만들어 줍니다. 쉽게 만들기 위하여 nest cli를 사용합니다.

$ nest g module auth
$ nest g service auth

인증 절차에 필요한 컬럼 생성

인증을 하기 위해서는 사용자이름과 암호를 사용합니다.
그렇기 때문에 이전에 사용하던 user entity의 코드를 수정해야합니다.
username으로 검색을 하기 때문에 username은 유일성을 가지도록합니다.

/src/users/user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column()
  password: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column()
  isActive: boolean;
}

users.service에 username 검색 추가

현재 작성 중인 예제는 사용자이름을 통해 검색하기 때문에 전에 만든 findOne을 사용하지 못합니다.
그래서 해당 요청에 대한 함수를 새로 만들어 줘야합니다.

/src/users/users.service.ts

...생략

@Injectable()
export class UsersService {
  ...생략
  async find(username: string): Promise<User | undefined> {
    return this.usersRepository.findOne({ username: username });
  }
}

username 컬럼에서 username 변수의 값으로 검색할 수 있습니다.

auth에서 사용하기 위한 export

users.service.ts 의 find 메소드를 auth에서 사용하기 위해서 모듈 외부에서 볼 수 있도록합니다.

/src/users/users.module.ts

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

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

exports 배열에 내보내고 싶은 user.service.ts를 넣어 주시면 됩니다.

사용자 검색, 암호 확인을 위한 AuthService

AuthService에서는 사용자를 검색하고 암호를 확인합니다.

/src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async vaildateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.find(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

validateUser는 username을 통해 UserService에서 user 정보를 가져와 password가 맞는지 확인합니다.
일치하는 경우 user의 password와 분리한 나머지 부분을 반환합니다.

Passport-local의 인증 전략 구현

인증 전략을 구현하기 위하여 [local.strategy.ts]에 만들어줍니다.

/src/auth/strategies/local.strategy.ts

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 authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.vaildateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

현재 passport-local의 사용 사례에는 구성 옵션이 없으므로 생성자 super()는 옵션 객체없이 단순히를 호출 합니다.
passport 전략의 동작을 사용자화 하기 위해 옵션 개체를 전달 할 수 있습니다.

validate 메소드는 passport에서 validate라는 이름을 찾아 호출합니다. 즉, 이름이 다를 경우 동작이 제대로 되지 않습니다. validate에서는 사용자가 존재하고 유효한지 확인합니다. 만약 유효하지 않을 경우 예외를 던집니다.

구현한 Passport를 사용하기 위한 AuthModule 설정

passport기능을 사용할 수 있도록 모듈에 등록해줘야합니다.

/src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

app.controller에서 구현

/auth/login 경로에 해당하는 것을 구현합니다.

/src/app.controller.ts

import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LocalAuthGuard } from './auth/local-auth.guard';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
}

passport 로컬 전략의 기본 이름은 'local'입니다. @UseGuards()에서 해당 이름을 참조하여 passport-local 패키지에서 제공하는 코드와 연결해줍니다. AuthGuard 안에 현재 한개의 전략이 있지만 추가될 수 있습니다.

해당 전략이 여러개가 될 수 있기 때문에 따로 클래스를 만들어서 관리하는 편이 좋습니다.

/src/auth/guards/local-auth.guard.ts

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

수정된 코드에 맞게 AppController도 수정해줍니다.

/src/app.controller.ts

import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/guards/local-auth.guard';

@Controller()
export class AppController {
  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
}

Request, response

일단 테스트로 요청을 보내기 위하여 전에 추가했던 데이터를 전부 삭제해줍니다. 간단하게 DB user테이블을 지운후 다시 실행해주면 됩니다.

새로운 데이터를 추가합니다.

POST http://localhost:3000/users/

Request

[
    {
        "username": "test1",
        "password": "qwer1234@",
        "firstName": "a",
        "lastName": "bc",
        "isActive": true
    },
    {
        "username": "test2",
        "password": "qwer",
        "firstName": "b",
        "lastName": "cd",
        "isActive": false
    },
    {
        "username": "test3",
        "password": "qwer12",
        "firstName": "c",
        "lastName": "de",
        "isActive": true
    }
]

POST http://localhost:3000/auth/login

Request

{
    "username": "test1",
    "password": "qwer1234@"
}

Response

{
    "id": 1,
    "username": "test1",
    "firstName": "a",
    "lastName": "bc",
    "isActive": true
}

Request

{
    "username": "test2",
    "password": "qwer1234@"
}

Response

{
    "statusCode": 401,
    "message": "Unauthorized"
}

정상적인 요청에 대한 응답이 올 경우 비밀번호를 제외한 나머지 정보를 보내줍니다.

profile
개발자

0개의 댓글