반응형

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

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에서 권한을 체크하는 방법에 대해서 알아보았습니다.

반응형

+ Recent posts