icon

역할 기반 접근 제어 (RBAC) 구현


 역할 기반 접근 제어(RBAC)는 사용자의 조직 내 역할에 따라 시스템 리소스에 대한 접근을 관리하는 방법입니다.

 NestJS에서 RBAC를 구현하면 애플리케이션의 보안성과 유연성을 크게 향상시킬 수 있습니다.

RBAC의 개념과 필요성

 RBAC는 다음과 같은 이점을 제공합니다.

  1. 접근 권한의 중앙 집중화된 관리
  2. 역할 기반의 권한 할당으로 관리 용이성 증가
  3. 최소 권한 원칙 준수 용이
  4. 규정 준수 및 감사 지원

 NestJS에 RBAC를 적용하면 모듈화된 구조와 결합하여 더욱 체계적이고 확장 가능한 접근 제어 시스템을 구축할 수 있습니다.

RBAC 시스템 설계 및 구현

  1. 엔티티 모델링
// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  username: string;
 
  @ManyToMany(() => Role)
  @JoinTable()
  roles: Role[];
}
 
// role.entity.ts
@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  name: string;
 
  @ManyToMany(() => Permission)
  @JoinTable()
  permissions: Permission[];
}
 
// permission.entity.ts
@Entity()
export class Permission {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  name: string;
}
  1. 커스텀 데코레이터 생성
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
 
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
  1. 역할 확인 가드 구현
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
 
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
 
  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}
  1. 컨트롤러에 적용
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  @Get()
  @Roles('ADMIN')
  findAll() {
    return this.usersService.findAll();
  }
}

동적 역할 및 권한 할당

 동적으로 역할과 권한을 관리하기 위해 서비스를 구현합니다.

@Injectable()
export class RoleService {
  constructor(
    @InjectRepository(Role)
    private roleRepository: Repository<Role>,
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
 
  async assignRoleToUser(userId: number, roleName: string): Promise<User> {
    const user = await this.userRepository.findOne(userId, { relations: ['roles'] });
    const role = await this.roleRepository.findOne({ where: { name: roleName } });
    
    if (!user || !role) {
      throw new NotFoundException('User or Role not found');
    }
 
    user.roles.push(role);
    return this.userRepository.save(user);
  }
 
  // 추가 메서드: 역할 생성, 권한 할당 등
}

RBAC와 JWT 결합

 JWT 페이로드에 역할 정보를 포함시키는 전략

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}
 
  async login(user: User) {
    const payload = { 
      username: user.username, 
      sub: user.id,
      roles: user.roles.map(role => role.name)
    };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

데이터베이스 수준의 접근 제어 통합

 TypeORM의 쿼리 빌더를 사용하여 데이터베이스 수준에서 RBAC를 구현할 수 있습니다.

@Injectable()
export class PostService {
  constructor(
    @InjectRepository(Post)
    private postRepository: Repository<Post>,
  ) {}
 
  async findAll(user: User): Promise<Post[]> {
    const query = this.postRepository.createQueryBuilder('post');
 
    if (!user.roles.includes('ADMIN')) {
      query.where('post.authorId = :userId', { userId: user.id });
    }
 
    return query.getMany();
  }
}

성능 최적화 전략

  1. 캐싱 : 역할 및 권한 정보를 Redis와 같은 인메모리 캐시에 저장
@Injectable()
export class RoleCacheService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    private roleService: RoleService,
  ) {}
 
  async getUserRoles(userId: number): Promise<string[]> {
    const cachedRoles = await this.cacheManager.get<string[]>(`user:${userId}:roles`);
    if (cachedRoles) {
      return cachedRoles;
    }
 
    const roles = await this.roleService.getUserRoles(userId);
    await this.cacheManager.set(`user:${userId}:roles`, roles, { ttl: 3600 });
    return roles;
  }
}
  1. 데이터베이스 인덱싱 : 역할 및 권한 테이블에 적절한 인덱스 추가
  2. 권한 체크 로직 최적화 : 복잡한 권한 체크는 배치 처리 또는 비동기 처리

Best Practices 및 확장 전략

  1. 세분화된 권한 설계 : 너무 많은 역할보다는 세분화된 권한을 조합하여 역할 정의
  2. 역할 상속 구현 : 역할 간 계층 구조 설정으로 관리 용이성 증가
  3. attribute-based access control (ABAC) 고려 : 더 복잡한 접근 제어가 필요한 경우 ABAC 도입
  4. 정기적인 접근 권한 검토 : 사용자의 역할과 권한을 주기적으로 검토 및 업데이트
  5. 감사 로깅 : 중요한 접근 제어 결정에 대한 로그 기록
  6. 테스트 자동화 : 역할 및 권한 변경에 따른 영향을 검증하는 자동화된 테스트 구현
  7. 사용자 인터페이스 통합 : 역할에 따른 UI 요소 표시/숨김 처리
@Injectable()
export class RoleBasedUiService {
  canAccessFeature(user: User, feature: string): boolean {
    return user.roles.some(role => role.permissions.includes(feature));
  }
}
  1. 동적 정책 업데이트 : 런타임에 정책을 업데이트할 수 있는 메커니즘 구현
@Injectable()
export class DynamicPolicyService {
  private policies: Map<string, (user: User, resource: any) => boolean> = new Map();
 
  addPolicy(name: string, policy: (user: User, resource: any) => boolean) {
    this.policies.set(name, policy);
  }
 
  checkPolicy(name: string, user: User, resource: any): boolean {
    const policy = this.policies.get(name);
    return policy ? policy(user, resource) : false;
  }
}
  1. 역할 기반 라우트 보호 : 라우트 가드를 사용하여 전체 라우트 그룹에 대한 접근 제어
@Injectable()
export class RoleBasedRouteGuard implements CanActivate {
  constructor(private reflector: Reflector, private roleService: RoleService) {}
 
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return this.roleService.userHasRole(user, roles);
  }
}
  1. 마이크로서비스 아키텍처 고려 : 대규모 시스템의 경우 인증/인가를 별도의 마이크로서비스로 분리

 성능 최적화는 RBAC 구현의 중요한 측면입니다.

 캐싱, 효율적인 데이터베이스 쿼리, 그리고 비동기 처리 등의 기술을 활용하여 권한 체크로 인한 지연을 최소화해야 합니다.