반응형

이 글의 동영상 강의입니다.

https://youtu.be/zjkaDU3syO8

 

완성 소스는 github 링크를 참조하세요.

https://github.com/CodeGearGit/nest-sample

 

GitHub - CodeGearGit/nest-sample: nestjs sample program

nestjs sample program. Contribute to CodeGearGit/nest-sample development by creating an account on GitHub.

github.com

 

환경 설정(Configuration)

Nestjs 프로젝트 실행시에 미리 정의된 환경설정을 읽어와서 실행합니다.

이때 환경변수를 통해서 가져오는 것과 설정파일을 통해 가져오는 것이 있습니다.

  • 환경변수
    • 보안상 중요한 내용을 코드에 노출하지 않기 위해 사용.
    • 개발시에는 .env 파일을 이용해서 관리.
    • .env는 .gitignore에 추가해서 서버에 배포되지 않게 합니다.
    • 서버에서는 프로파일에 export로 정의.
    • AWS의 ECS 같은 경우는 작업정의에서 정의.
  • 설정파일
    • 개발/운영등의 환경에 맞는 설정을 다르게 사용하기 위해 사용.
    • yaml 형식의 파일을 사용.

 

Nestjs Config

Nestjs에서는 설정 파일을 쉽게 관리할 수 있도록 @nestjs/config 패키지를 제공합니다.

@nestjs/config 패키지는 dotenv를 포함하고 있으므로 따로 설치하지 않아도 됩니다.

다음 명령을 사용해 패키지를 설치합니다.

npm i @nestjs/config

다음은 AppModule에서 config를 사용할 수 있도록 ConfigModule을 import 합니다.

  • isGlobal option은 다른 모듈에서 config를 사용할 수 있도록 global로 설정을 합니다.

src/app.module.ts

import { ConfigModule } from '@nestjs/config';
...

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true
    }),
    ....]
...

 

환경 변수 정의

프로젝트 루트 폴더에 .env를 만들고 다음과 같이 작성을 합니다.

NODE_ENV=development
NODE_SERVER_PORT=3000

main.ts 파일에서 다음과 같이 사용할 수 있습니다.

const port = process.env.NODE_SERVER_PORT
...
await app.listen(port);
logger.log(`Application listening on port ${port}`);

 

환경변수 테스트

서버를 실행해서 port를 읽어오는지 확인합니다.

npm run start

 

ConfigService 사용하기

이제 ConfigService를 사용하도록 main.ts 를 변경합니다.

  • @nestjs/config의 ConfigService를 사용합니다.
  • configService.get('NODE_SERVER_PORT')를 사용하여 port를 읽어옵니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { ConfigService } from "@nestjs/config";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());
  const configService = app.get(ConfigService);
  const port = configService.get('NODE_SERVER_PORT');
  await app.listen(port);
  console.log(`Application listening on port ${port}`);
}
bootstrap();

서버를 실행해보면 이전과 동일하게 서버가 올라갑니다.

 

ConfigService를 다른 곳에서 사용하시려면 construtor 추가하면 됩니다.

import { ConfigService } from '@nestjs/config';
...
constructor(private configService: ConfigService);
...
configService.get('...');

 

Custom Configuration - 개발 / 운영 설정 파일 분리

yaml 파일 처리를 위해 js-yaml 패키지를 설치합니다.

npm i js-yaml @types/js-yaml

config 폴더를 만들고 다음과 같이 파일을 추가합니다.

- 테스트를 위해 개발/운영 각각 포트를 다르게 주었습니다.

  • src/config/production.yaml
server:
  port: 3001
  • src/config/development.yaml
server:
  port: 3002

 

config/config.ts 파일을 다음과 같이 만듭니다.

- 환경변수의 NODE_ENV가 'production'일 경우는 production.yaml을 읽고

- 그외의 경우는 development.yaml을 읽게 합니다.

  • src/config/config.ts
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';

const YAML_CONFIG_PROD = 'production.yaml';
const YAML_CONFIG_DEV = 'development.yaml';

export default () => {
  return yaml.load(
      (process.env.NODE_ENV==='production')?
        readFileSync(join(__dirname, YAML_CONFIG_PROD), 'utf8')
      : readFileSync(join(__dirname, YAML_CONFIG_DEV), 'utf8'),
  ) as Record<string, any>;
};

AppModule에서 config.ts 정보를 가져옵니다.

import config from './config/config';
...
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [config],
      isGlobal: true
    }),...

main.ts에서 config를 이용해 포트를 읽어오도록 변경합니다.

const configService = app.get(ConfigService);
const port = configService.get<string>('server.port');

yaml은 컴파일시에 dist 폴더로 copy가 되지 않으므로 파일을 찾을 수 없습니다.

package.json에서 script를 이용해 yaml을 copy하기 위해 다음 패키지를 설치합니다.

npm i cpx

package.json을 다음과 같이 파일 복사 스크립트를 추가합니다.

- scripts에 "copy-files"를 아래와 같이 추가합니다.

- "copy-files": "cpx \"src/config/*.yaml\" dist/config/",

- start 및 build 스크립트에 "npm run copy-files" 를 추가합니다.

...
"scripts": {
    "copy-files": "cpx \"src/config/*.yaml\" dist/config/",
    "prebuild": "rimraf dist",
    "build": "npm run copy-files && nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "npm run copy-files && nest start",
    "start:dev": "npm run copy-files && nest start --watch",
    "start:debug": "npm run copy-files && nest start --debug --watch",
    "start:prod": "npm run copy-files && node dist/main",
...

프로젝트를 실행합니다.

npm run start:dev

.env의 "NODE_ENV=development"일 경우에는 3002번 포트로 서버가 실행됩니다.

.env의 "NODE_ENV=production"일 경우에는 3001번 포트로 서버가 실행됩니다.

이렇게 환경 설정파일을 분리해서 개발과 운영의 환경을 다르게 설정할 수 있습니다.

 

반응형
반응형

 

이 글의 동영상 강의입니다.

https://youtu.be/Unkbarfk1-M

 

로그인시 jwt token을 client의 쿠키에 저장하고, 로그아웃시에 삭제하는 방법을 알아보겠습니다.

 

우선 기존 login 처리 controller에서 쿠키를 저장할 수 있도록 소스를 수정합니다.

- res.cookie를 이용해서 jwt 토큰 값을 전달합니다.

- res.cookie의 인자는 (key, value, option) 으로 구성됩니다.

- option에는 httpOnly는 브라우저에서 cookie를 이용할 수 없게 합니다. 따라서 XSS 등의 보안을 강화시킬 수 있습니다.

- maxAge는 쿠키의 유효기간을 설정할 수 있습니다.

 

src/auth/auth.controller.ts

@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);
    res.cookie('jwt',jwt.accessToken,{
        httpOnly: true,
        maxAge: 24 * 60 * 60 * 1000 //1 day
    });
    return res.send({
        message: 'success'
    });
}

postman에서 login을 호출하면 다음과 같은 결과가 나옵니다.

- url = http://localhost:3000/auth/login

하단 결과 탭에 Cookie를 선택하면 다음과 같이 쿠키값이 들어있는 것을 확인할 수 있습니다.

이제 클라이언트에서 요청이 올때 이 쿠키 정보도 같이 보내게 됩니다.

이를 서버에서 처리하기 위해 cookie-parser 라는 패키지를 설치합니다.

npm i cookie-parser @types/cookie-parser

이제 다음과 같이 쿠키분석을 할 수 있도록 main.ts에 쿠키 파서를 추가합니다.

- app.use(cookieParser())를 추가함으로서 쿠키파서를 사용할 수 있습니다.

src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(cookieParser());
  await app.listen(3000);
}
bootstrap();

쿠키값을 잘 읽어 오는지 확인하기 위해 컨트롤러를 하나 만듭니다.

- 여기서는 단순히 쿠키의 jwt 값을 읽어서 리턴해줍니다.

src/auth/auth.controller.ts

@Get('/cookies')
getCookies(@Req() req: Request, @Res() res: Response): any {
    const jwt =  req.cookies['jwt'];
    return res.send(jwt);
}

postman으로 호출하면 다음과 같은 결과를 확인할 수 있습니다.

- url = http://localhost:3000/auth/cookies

이제 로그아웃시에 쿠키를 삭제하는 컨트롤러를 추가합니다.

- 쿠키의 'jwt'값을 삭제합니다.

- 유효기간을 '0'으로 설정합니다.

src/auth/auth.controller.ts

@Post('/logout')
logout(@Req() req: Request, @Res() res: Response): any {
    res.cookie('jwt', '', {
        maxAge: 0
    })
    return res.send({
        message: 'success'
    })
}

postman에서 테스트하면 결과는 다음과 같습니다.

- url = http://localhost:3000/auth/logout

다음과 같이 Cookies 탭에 값이 없어진것을 확인할 수 있습니다.

이상으로 Nestjs에서 쿠키를 다루는 방법이었습니다.

반응형
반응형

Nestjs에서 기본적으로 제공하는 ValidationPipe를 이용하면 요청 Parameter의 유효성을 쉽게 체크하고 처리할 수 있습니다.

예를 들면 필수 입력 값의 경우 @IsNotEmpty를 사용하는 것만으로 유효성 체크가 끝이 납니다.

 

사용자 등록시 아이디와 패스워드를 필수 값으로 처리하는 방법에 대해 알아보겠습니다.

이전 예제에서 사용자 등록시 Input Parameter는 아래의 UserDTO를 사용하였습니다.

src/auth/dto/user.dto.ts

export class UserDTO {
    username: string;
    password: string;
}

이 경우에 username을 빈값으로 보내게 되어도 다음과 같이 회원 가입이 됩니다.

db에는 다음과 같이 id 3번 값이 공백으로 들어가게 됩니다.

공백이 입력되는 것을 막는 ValidationPipe를 사용해 보겠습니다.

 

우선 class-validator 패키지를 설치합니다.

npm i --save class-validator

UserDTO에서 "@IsNotEmpty()" 데코레이터를 추가합니다.

src/auth/dto/user.dto.ts

import { IsNotEmpty } from "class-validator";

export class UserDTO {
    @IsNotEmpty()
    username: string;
    @IsNotEmpty()
    password: string;
}

 

회원가입 라우팅 메소드에 "@UsePipes(ValidationPipe)"를 추가합니다.

src/auth/auth.controller.ts

import { Body, Controller, Get, Post, Req, Res, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
...
@Post('/register')
@UsePipes(ValidationPipe)
async registerAccount(@Req() req: Request, @Body() userDTO: UserDTO): Promise<any> {
    return await this.authService.registerUser(userDTO);
}
...

 

이제 다시 위와 동일하게 username을 공백으로 API를 호출합니다.

* 이때 Validation 체크가 되지 않을 경우 "npm run build"를 한번 실행 후에 하시면 됩니다.

결과는 400 에러가 나오고 , username은 반드시 입력해야 한다는 메시지가 나옵니다.

 

class-validator의 데코레이터는 다음 사이트에가면 확인하실 수 있습니다.

https://github.com/typestack/class-validator#manual-validation

 

GitHub - typestack/class-validator: Decorator-based property validation for classes.

Decorator-based property validation for classes. Contribute to typestack/class-validator development by creating an account on GitHub.

github.com

 

반응형
반응형

이 글의 동영상 강의입니다.

https://youtu.be/hgilx-qB76o

 

AppModule에서 설정했던 TypeORM 설정을 외부 파일로 분리하면 관리가 좀 더 편해집니다.

그리고 Entity 파일도 동적으로 읽어오게 하면 Entity가 생길때마다 추가해야하는 일을 줄일 수 있습니다.

위 2가지 방법으로 기존 소스를 변경해 보겠습니다.

 

기존 AppModule에 TypeOrm은 아래와 같이 설정하였습니다.

src/app.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { Cat } from './cats/entity/cats.entity';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [Cat],
      synchronize: true,
    }),
    CatsModule,
    AuthModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

이를 다음과 같이 외부 설정 파일로 분리할 수 있습니다.

src/orm.config.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

function ormConfig(): TypeOrmModuleOptions {
	const commonConf = {
        SYNCRONIZE: false,
        ENTITIES: [__dirname + '/domain/*.entity{.ts,.js}'],
        MIGRATIONS: [__dirname + '/migrations/**/*{.ts,.js}'],
        CLI: {
            migrationsDir: 'src/migrations',
        },
        MIGRATIONS_RUN: false,
    };

    const ormconfig: TypeOrmModuleOptions = {
        name: 'default',
        type: 'mysql',
        database: 'test',
        host: 'localhost',
        port: 13306,
        username: 'root',
        password: 'root',
        logging: true,
        synchronize: commonConf.SYNCRONIZE,
        entities: commonConf.ENTITIES,
        migrations: commonConf.MIGRATIONS,
        cli: commonConf.CLI,
        migrationsRun: commonConf.MIGRATIONS_RUN,
    };
    
    return ormconfig;
}

export { ormConfig };

Entity 파일들은 동적으로 읽어오기 위해 src/domain 폴더로 옮겨줍니다.

 

이렇게 설정 파일이 만들어지면 AppModule에서는 다음과 같이 변경하면 됩니다.

src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
import { ormConfig } from './orm.config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({ useFactory: ormConfig }),
    CatsModule,
    AuthModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

이렇게 해서 좀 더 깔끔한 소스가 되었습니다.^^

반응형
반응형

다음은 이글의 동영상 강의입니다.

https://youtu.be/hJMxGJoNsjQ

https://youtu.be/QBTgOnKMo9s

https://youtu.be/e6HpJqXj1mk

https://youtu.be/d_-Q98-hKs8

 

사이트에서 특정 권한자만 접근이 가능하도록 제어해야할 필요가 있습니다.

예를 들면 회원관리 메뉴의 경우는 모든 사람이 접근하면 안되고, 관리자만 접근 가능해야 하는 경우입니다.

이를 구현하기 위해서는 사용자의 권한(Role)을 관리해야 합니다.

 

권한 관리를 위해서는 다음 사항들이 수행되어야 합니다.

- 권한 구분 : admin, user등으로 권한을 설정합니다.

- 사용자 별로 어떤 권한이 있는지를 db에 저장하고 있어야 합니다.

- 메뉴 접근시 사용자의 권한을 체크하여 사용 가능 여부에 따라 접근을 허가 또는 거부합니다.

 

db에 사용자의 권한을 관리하기 위한 테이블 user_autority의 구조는 다음과 같습니다.

다음을 SQL을 이용하여 user_authority 테이블을 만듭니다.

- user_id는 user 테이블의 id 값에 매핑되는 값입니다.

- authority_name은 다음 두가지 type을 갖도록 ENUM으로 만듭니다.

- authority_name : USER, ADMIN

- 초기 데이터를 아래와 같이 입력합니다.

CREATE TABLE IF NOT EXISTS `test`.`user_authority` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `authority_name` ENUM('ROLE_USER', 'ROLE_ADMIN') NOT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB

insert into `test`.`user_authority` (user_id, authority_name) values (1,'ROLE_USER');
insert into `test`.`user_authority` (user_id, authority_name) values (1,'ROLE_ADMIN');
insert into `test`.`user_authority` (user_id, authority_name) values (2,'ROLE_USER');

- user 테이블에 아래와 같이 id에 값이 있어야 합니다.

 

user-authority entity를 만듭니다.

- 한 사용자가 여러개의 authority를 갖을 수 있으므로 user_authority 테이블에서는 ManyToOne으로 Join을 합니다.

- auth/entity/user-authority.entity.ts

import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
import { User } from "./user.entity";

@Entity('user_authority')
export class UserAuthority {
    @PrimaryColumn()
    id: number;

    @Column('int',{name: 'user_id'})
    userId: number;

    @Column('varchar',{name: 'authority_name'})
    authorityName: string;

    @ManyToOne(type=>User, user=>user.authorities)
    @JoinColumn({name: 'user_id'})
    user: User;
}

user-entity를 다음과 같이 수정합니다.

- 한 사용자가 여러개의 authority를 갖을 수 있으므로 user 테이블에서는 OneToMany Join을 합니다.

- auth/entity/user.entity.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { UserAuthority } from "./user-authority.entity";

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

    @Column()
    username: string;

    @Column()
    password: string;

    @OneToMany(()=>UserAuthority, userAuthority => userAuthority.user, {eager: true})
    authorities?: any[];
}

authority repository를 만듭니다.

- auth/repository/user-autority.repository.ts

import { EntityRepository, Repository } from "typeorm";
import { UserAuthority } from "../entity/user-authority.entity";

@EntityRepository(UserAuthority)
export class UserAuthorityRepository extends Repository<UserAuthority> {}

App Module의 TypeOrmModule.forRoot에 UserAuthority를 추가합니다.

- app.module.ts

TypeOrmModule.forRoot({
  type: 'mysql',
  host: 'localhost',
  port: 13306,
  username: 'root',
  password: 'root',
  database: 'test',
  entities: [Cat, User, Authority],
  synchronize: true,
}),

Auth Module에 UserAuthorityRepository를 추가합니다.

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 './security/passport.jwt.strategy';
import { UserRepository } from './repository/user.repository';
import { UserService } from './user.service';
import { UserAuthorityRepository } from './repository/user-authority.repository';

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

payload에도 UserAutyrity를 추가합니다.

- auth/security/payload.interface.ts

export interface Payload {
    id: number;
    username: string;
    authorities?: any[];
}

 

Role Type을 다음과 같이 만듭니다.

- auth/role-type.ts

export enum RoleType {
    USER = 'ROLE_USER',
    ADMIN = 'ROLE_ADMIN',
    ANONYMOUS = 'ROLE_ANONYMOUS',
}

Role Decorator를 다음과 같이 만듭니다.

- auth/decorator/role.decorator.ts

import { SetMetadata } from '@nestjs/common';
import { RoleType } from '../role-type';

export const Roles = (...roles: RoleType[]): any => SetMetadata('roles', roles);

 

Role Guard를 다음과 같이 만듭니다.

- auth/security/roles.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { User } from '../entity/user.entity';

@Injectable()
export class RolesGuard implements CanActivate {
    constructor(private readonly reflector: Reflector) {}

    canActivate(context: ExecutionContext): boolean {
        const roles = this.reflector.get<string[]>('roles', context.getHandler());

        if (!roles) {
            return true;
        }

        const request = context.switchToHttp().getRequest();
        const user = request.user as User;

        return user && user.authorities && user.authorities.some(role => roles.includes(role));
    }
}

RolesGuard에서 사용된 Reflector는 Application Runtime시에 정의된 Metadata를 이용해 type을 알아내는데 사용합니다.

 

AuthService의 payload에 authority를 추가합니다.

- authorities는 배열 형태로 RoleType이 들어갑니다.

- auth/auth.service.ts

async validateUser(userDTO: UserDTO): Promise<{accessToken: string} | undefined> {
    let userFind: User = await this.userService.findByFields({
        where: { username: userDTO.username }
    });
    if(!userFind) {
        throw new UnauthorizedException();
    }
    const validatePassword = await bcrypt.compare(userDTO.password, userFind.password);
    if(!validatePassword) {
        throw new UnauthorizedException();
    }

    this.convertInAuthorities(userFind);

    const payload: Payload = { id: userFind.id, username: userFind.username, authorities: userFind.authorities };
    return {
        accessToken: this.jwtService.sign(payload)
    };
}

async tokenValidateUser(payload: Payload): Promise<User| undefined> {
    const userFind = await this.userService.findByFields({
        where: { id: payload.id }
    });
    this.flatAuthorities(userFind);
    return userFind;
}

private flatAuthorities(user: any): User {
    if (user && user.authorities) {
        const authorities: string[] = [];
        user.authorities.forEach(authority => authorities.push(authority.authorityName));
        user.authorities = authorities;
    }
    return user;
}

private convertInAuthorities(user: any): User {
    if (user && user.authorities) {
        const authorities: any[] = [];
        user.authorities.forEach(authority => authorities.push({ name: authority.authorityName }));
        user.authorities = authorities;
    }
    return user;
}

테스트를 위해 Controller에 핸들러를 하나 추가합니다.

- auth/auth.controller.ts

@Get('/admin-role')
@UseGuards(AuthGuard, RolesGuard)
@Roles(RoleType.ADMIN)
adminRole(@Req() req: Request): any {
    const user: any = req.user;
    return user;
}

Application을 실행합니다.

npm run start:dev

 

ADMIN 권한이 있는 아이디로 테스트 

- 관리자 계정으로 로그인 후 토큰값을 받아 옵니다.

accessToken을 이용해서 관리자 접근 권한을 체크합니다.

ADMIN 권한이 없는 아이디로 테스트

권한이 없을 경우 아래와 가티 에러가 발생됩니다.

이상으로 Nest에서 권한을 체크하는 방법에 대해서 알아보았습니다.

반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/1upOqWW2zyU

 

Nestjs에서 JWT 토큰을 이용한 인증시에는 Guard를 사용합니다.

Guard는 라우팅 전에 작동하는 일종의 미들웨어입니다.

 

이전까지의 코드가 들어있는 샘플소스는 다음 github 저장소에서 받으실 수 있습니다.

https://github.com/CodeGearGit/nestjs-05-jwt

 

GitHub - CodeGearGit/nestjs-05-jwt

Contribute to CodeGearGit/nestjs-05-jwt development by creating an account on GitHub.

github.com

 

패키지 설치

프로젝트에 다음과 같이 패키지를 설치합니다.

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

 

코드 작성

JWT 검증을 위해 JwtStrategy를 만듭니다.

auth/security/passport.jwt.strategy.ts

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt';
import { PassportStrategy } from "@nestjs/passport";
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: 'SECRET_KEY',
        })
    }

    async validate(payload: Payload, done: VerifiedCallback): Promise<any> {
        const user = await this.authService.tokenValidateUser(payload);
        if (!user) {
            return done(new UnauthorizedException({ message: 'user does not exist' }), false);
        }

        return done(null, user);
    }
}

auth/auth.service.ts 에 tokenValidateUser를 추가합니다.

async tokenValidateUser(payload: Payload): Promise<UserDTO | undefined> {
    return await this.userService.findByFields({
        where: { id: payload.id }
    });
}

auth/auth.module.ts

- imports에 PassportModule 을 추가하고,

- providers에 JwtStrategy 를 추가합니다.

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 './security/passport.jwt.strategy';
import { UserRepository } from './user.repository';
import { UserService } from './user.service';

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

auth/security/auth.guard.ts를 만듭니다.

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard as NestAuthGuard } from '@nestjs/passport';

@Injectable()
export class AuthGuard extends NestAuthGuard('jwt') {
    canActivate(context: ExecutionContext): any {
        return super.canActivate(context);
    }
}

auth/auth.controller.ts에 아래와 같이 라우터를 하나 추가합니다.

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

 

테스트

jwt 인증 모듈의 개발이 완료되었으므로 Postman을 이용해서 테스트를 해봅니다.

토큰이 없을 경우 다음과 같이 Unauthorized 값이 리턴됩니다.

로그인 호출을 통해 토큰 값을 받아옵니다.

토큰을 이용해 authenticate를 다시 호출합니다.

- Headers 에 다음 값을 입력합니다.

   KEY: Authorization

   VALUE : 로그인에서 받아온 토큰값

다음과 같이 결과가 나오는걸 보실 수 있습니다.

이상으로 JWT 인증하는 법에 대해 알아보았습니다.

 

최종 완료된 소스는 아래 URL에서 확인하시면 됩니다.

https://github.com/CodeGearGit/nestjs-06-jwt-auth

 

GitHub - CodeGearGit/nestjs-06-jwt-auth

Contribute to CodeGearGit/nestjs-06-jwt-auth development by creating an account on GitHub.

github.com

 

반응형
반응형

다음은 이글의 동영상 강의 입니다.

https://youtu.be/H4VS-Osylvo

 

토큰을 이용한 API 호출 방법

REST API 호출시 토큰을 이용하면 사용자 인증과 권한을 체크할 수 있습니다.

다음 그림은 토큰의 생성과  호출을 나타내는 그림입니다.

  1. Client에서 로그인을 요청합니다.
  2. Server에서 로그인을 체크한 후 토큰을 생성하고 accessToken을 전달합니다.
  3. API를 호출합니다. 이때 호출 Header에 accessToken을 담아서 호출합니다.
  4. accessToken을 확인하여 인증여부, 권한등을 체크한 후 결과 값을 return합니다.

 

JWT 구조

JWT 토큰은 다음과 같이 "."을 구분자로 하여 다음과 같이 3개의 값이 들어있는 형태를 갖고 있습니다.

  1. header
  2. payload
  3. verify signature
{
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
     eyJpZCI6MSwidXNlcm5hbWUiOiJjb2RlZ2VhciIsImlhdC
     I6MTY0MzUxOTI2MCwiZXhwIjoxNjQzNTE5NTYwfQ.
     jxsk2FtHsRRhoAZrsUDgHaHOLCxI9IlSMKTrkZ0zUl4"
}

이 값은 www.jwt.io  사이트에 가면 다음과 같이 확인이 가능합니다.

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

샘플 소스

지난시간까지의 소스는 아래 URL에서 받으시고, 아래 코드를 작성하시면 됩니다.

https://github.com/CodeGearGit/nestjs-04-login

 

GitHub - CodeGearGit/nestjs-04-login

Contribute to CodeGearGit/nestjs-04-login development by creating an account on GitHub.

github.com

 

JwtModule Import

우선 로그인시에 토큰을 생성해 보는 방법에 대해 설명합니다.

 

Nestjs에서 jwt 토큰을 사용하기 위해서는 @nestjs/jwt 라는 패키지를 설치합니다.

npm i --save @nestjs/jwt

JWT를 사용하고자 하는 Module에 다음과 같이 Jwt.Module을 등록합니다.

auth.module.ts

import { JwtModule } from '@nestjs/jwt';
imports: [
    TypeOrmModule.forFeature([UserRepository]),
    JwtModule.register({
      secret: 'SECRET',
      signOptions: { expiresIn: '300s' },
    }),
 ],

여기서 secret에는 verify signature에서 사용할 secret key 값을 입력합니다.

 

Payload Interface

다음은 payload에 보여 줄 값을 위해 interface를 생성합니다.

auth/security/payload.interface.ts

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

 

 

JWT토큰 생성

로그인 서비스를 다음과 같이 수정합니다.

auth/auth.service.ts의 validateUser

  • payload 값 지정
  • Json의 token 값으로 return
async validateUser(userDTO: UserDTO): Promise<{accessToken: string} | undefined> {
    let userFind: User = await this.userService.findByFields({
        where: { username: userDTO.username }
    });
    const validatePassword = await bcrypt.compare(userDTO.password, userFind.password);
    if(!userFind || !validatePassword) {
        throw new UnauthorizedException();
    }

    const payload: Payload = { id: userFind.id, username: userFind.username };

    return {
        accessToken: this.jwtService.sign(payload),
    };
}

 

로그인 컨트롤러를 다음과 같이 수정합니다.

@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);
}

서버를 실행하고 Postman을 이용해 다음과 같이 로그인을 확인합니다.

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

결과

결과 값에서 Headers 탭을 클릭하면 다음과 같이 Bearer + 토큰 값을 확인할 수 있습니다.

 

이상으로 Nestjs에서 JWT 토큰을 생성하는 법을 알아보았습니다.

반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/LbyfF6ALMo0

 

비밀번호 암호와의 필요성

회원 가입시 비밀번호를 그대로 저장하면 보안사고의 위험이 있습니다.
만약 고객의 정보가 노출되었을 경우 해당 비밀번호를 이용하여 다른 사이트를 해킹할 경우 많은 피해가 발생할 수 있습니다.
따라서 실제 프로젝트에서 비밀번호는 반드시 암호화하여 저장해야만 합니다.
이번 글에서는 비밀번호를 암호화하는 방법에 대해 알아보겠습니다.
 

BCrypt

여기서는 nodejs에서 비밀번호 암호화시 많이 사용하는 bcrypt라는 패키지를 사용합니다.
다음은 npmjs.com의 bcrypt 설명페이지입니다.
 
bcrypt는 비밀번호를 암호화하는 키방식의 암호화 함수 입니다.

 

예제 소스 다운로드

여기서 사용하는 예제 소스는 아래 URL에서 받으실 수 있습니다.

https://github.com/CodeGearGit/nest-sample

 

GitHub - CodeGearGit/nest-sample: nestjs sample program

nestjs sample program. Contribute to CodeGearGit/nest-sample development by creating an account on GitHub.

github.com

 

bcrypt  추가

기존 프로젝트에 bcrypt 패키지를 설치합니다.
nestjs가 typescript 기반이므로 @types/bcrypt를 사용합니다.
npm install --save bcrypt @types/bcrypt
 

회원 가입 시 비밀번호 암호화

비밀번호 암호화 소스를 아래와 같이 작성합니다.
auth/user.service.ts
import * as bcrypt from 'bcrypt';

async transformPassword(user: UserDTO): Promise<void> {
	user.password = await bcrypt.hash(
		user.password, 10,
	);
	return Promise.resolve();
}

 

회원 정보를 저장하기 전에 패스워드 암호화 로직을 추가합니다.
auth/user.service.ts
async save(userDTO: UserDTO): Promise<UserDTO | undefined> {
	await this.transformPassword(userDTO);
	console.log(userDTO);
	return await this.userRepository.save(userDTO);
}

 

포스트맨을 이용해 사용자를 생성하는 API를 호출합니다.

결과

Database에는 다음과 같이 들어가 있는 것을 볼 수 있습니다.

 

 

로그인시 패스워드 체크 로직 수정

로그인시 패스워드 확인하는 부분을 수정합니다.
이때 bcrypt의 compare를 사용하여 입력받은 password와 저장된 password를 비교합니다.
auth.service.ts
import * as bcrypt from 'bcrypt';

async validateUser(userDTO: UserDTO): Promise<string | undefined> {
	let userFind: UserDTO = await this.userService.findByFields({
		where: { username: userDTO.username }
	});
	const validatePassword = await bcrypt.compare(userDTO.password, userFind.password)
	if(!userFind || !validatePassword) {
		throw new UnauthorizedException();
	}
	return "Login Success!";
}
포스트맨을 이용해 로그인을 테스트합니다.
 

결과

로그인 정보를 잘못 입력한 경우의 결과
이상으로 비밀번호 암호화 하는 방법에 대해 알아보았습니다.
반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/u39nqCfjYx4

 

가장 기본적인 아이디 / 패스워드를 체크하는 수준의 로그인을 만들어 봅니다.

이전 글 회원가입의 소스는 아래 url을 통해 받으실 수 있습니다.

https://github.com/CodeGearGit/nest-register-sample

 

GitHub - CodeGearGit/nest-register-sample

Contribute to CodeGearGit/nest-register-sample development by creating an account on GitHub.

github.com

 

auth/auth.service.ts에 아래와 같이 로그인 체크 로직을 추가합니다.

async validateUser(user: UserDTO): Promise<UserDTO | undefined> {
    let userFind: UserDTO = await this.userService.findByFields({ 
        where: { username: user.username }
    });
    if(!userFind || user.password !== userFind.password) {
        throw new UnauthorizedException();
    }
    return userFind;
}

auth/auth.controller.ts에 라우팅 메소드를 추가합니다.

@Post('/login')
async login(@Body() userDTO: UserDTO): Promise<any> {
    return await this.authService.validateUser(userDTO);
}

Postman을 이용해서 아래와 같이 login 테스트를 합니다.

결과 값은 다음과 같습니다.

password를 '1234' 입력하면 결과는 다음과 같습니다.

 

이상으로 가장 기초적인 로그인 체크에 대해 알아보았습니다.

반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/4mjd5P7cZIA

https://youtu.be/NCqCfyQoNz8

 

Nest로 로그인 기능을 구현하기 위해 우선적으로 회원 가입을 만들어봅니다.

 

이 예제의 소스는 아래 사이트에서 받으실 수 있습니다.

https://github.com/CodeGearGit/nest-register-sample

 

GitHub - CodeGearGit/nest-register-sample

Contribute to CodeGearGit/nest-register-sample development by creating an account on GitHub.

github.com

 

USER 테이블의 구조는 다음과 같습니다.

auth 모듈을 만듭니다.

nest g module auth

auth/entity/user.entity.ts 를 만듭니다.

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

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

    @Column()
    username: string;
    
    @Column()
    password: string;
}

auth/user.repository.ts 를 만듭니다.

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

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

auth/dto/user.dto.ts 를 만듭니다.

export class UserDTO {
    username: string;
    password: string;
}

auth/user.servict.ts 를만듭니다.

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { FindOneOptions } from "typeorm";
import { UserDTO } from "./dto/user.dto";
import { UserRepository } from "./user.repository";

@Injectable()
export class UserService{
    constructor(@InjectRepository(UserRepository) private userRepository: UserRepository){}

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

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

auth/auth.service.ts 를 만듭니다.

(nest g service auth)

import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { UserDTO } from "./dto/user.dto";
import { UserService } from "./user.service";

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

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

auth/auth.controller.ts를 만듭니다.

(nest g controller auth)

import { Body, Controller, Post, Req } from "@nestjs/common";
import { Response, Request } from 'express';
import { AuthService } from "./auth.service";
import { UserDTO } from "./dto/user.dto";

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

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

auth/auth.module.ts를 아래와 같이 수정합니다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UserRepository } from './user.repository';
import { UserService } from './user.service';

@Module({
    imports: [TypeOrmModule.forFeature([UserRepository])],
    exports: [TypeOrmModule],
    controllers: [AuthController],
    providers: [AuthService, UserService]
  })
export class AuthModule {}

서버를 start 합니다.

npm run start:dev

postman에서 다음과 같이 테스트를 합니다.

POST http://localhost:3000/api/register
Body-raw-JSON
{
    "username": "codegear",
    "password": "1111"
}

실행 결과는 다음과 같습니다.

user 테이블에서도 다음과 같이 확인하실 수 있습니다.

 

반응형

+ Recent posts