반응형

AWS의 Route53에서 서브 도메인을 생성하는 방법입니다.

기존에 www.mycodegear.com이라는 도메인이 있을 경우, blog.mycodegear.com과 같은 서브 도메인을 추가할 수 있습니다.

 

- AWS 콘솔에 접속 후 Route53으로 이동합니다.

- 호스팅영역을 클릭합니다.

- 서브도메인을 추가할 도메인 이름을 클릭합니다.

- 레코드 생성을 클릭합니다.

- 레코드 이름에 서브도메인의 이름을 입력합니다. (blog.mycodegear.com의 경우 blog만 입력)

- 레코드 유형은 CNAME을 선택합니다.

- 값은 AWS 리소스의 Alias값을 입력합니다.

- TTL은 default 값인 300초로 합니다.

- 라우팅 정책은 default 값인 단순 라우팅으로 합니다.

 

이렇게 하고 레코드 생성을 클릭하면 작성한 레코드가 추가되고,

생성된 domain을 브라우저에 입력하면 연결을 확인할 수 있습니다.

반응형
반응형

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

 

반응형

+ Recent posts