역할 기반 접근 제어 (RBAC) 구현
역할 기반 접근 제어(RBAC)는 사용자의 조직 내 역할에 따라 시스템 리소스에 대한 접근을 관리하는 방법입니다.
NestJS에서 RBAC를 구현하면 애플리케이션의 보안성과 유연성을 크게 향상시킬 수 있습니다.
RBAC의 개념과 필요성
RBAC는 다음과 같은 이점을 제공합니다.
- 접근 권한의 중앙 집중화된 관리
- 역할 기반의 권한 할당으로 관리 용이성 증가
- 최소 권한 원칙 준수 용이
- 규정 준수 및 감사 지원
NestJS에 RBAC를 적용하면 모듈화된 구조와 결합하여 더욱 체계적이고 확장 가능한 접근 제어 시스템을 구축할 수 있습니다.
RBAC 시스템 설계 및 구현
- 엔티티 모델링
// 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;
}
- 커스텀 데코레이터 생성
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
- 역할 확인 가드 구현
// 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));
}
}
- 컨트롤러에 적용
@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();
}
}
성능 최적화 전략
- 캐싱 : 역할 및 권한 정보를 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;
}
}
- 데이터베이스 인덱싱 : 역할 및 권한 테이블에 적절한 인덱스 추가
- 권한 체크 로직 최적화 : 복잡한 권한 체크는 배치 처리 또는 비동기 처리
Best Practices 및 확장 전략
- 세분화된 권한 설계 : 너무 많은 역할보다는 세분화된 권한을 조합하여 역할 정의
- 역할 상속 구현 : 역할 간 계층 구조 설정으로 관리 용이성 증가
- attribute-based access control (ABAC) 고려 : 더 복잡한 접근 제어가 필요한 경우 ABAC 도입
- 정기적인 접근 권한 검토 : 사용자의 역할과 권한을 주기적으로 검토 및 업데이트
- 감사 로깅 : 중요한 접근 제어 결정에 대한 로그 기록
- 테스트 자동화 : 역할 및 권한 변경에 따른 영향을 검증하는 자동화된 테스트 구현
- 사용자 인터페이스 통합 : 역할에 따른 UI 요소 표시/숨김 처리
@Injectable()
export class RoleBasedUiService {
canAccessFeature(user: User, feature: string): boolean {
return user.roles.some(role => role.permissions.includes(feature));
}
}
- 동적 정책 업데이트 : 런타임에 정책을 업데이트할 수 있는 메커니즘 구현
@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;
}
}
- 역할 기반 라우트 보호 : 라우트 가드를 사용하여 전체 라우트 그룹에 대한 접근 제어
@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);
}
}
- 마이크로서비스 아키텍처 고려 : 대규모 시스템의 경우 인증/인가를 별도의 마이크로서비스로 분리
성능 최적화는 RBAC 구현의 중요한 측면입니다.
캐싱, 효율적인 데이터베이스 쿼리, 그리고 비동기 처리 등의 기술을 활용하여 권한 체크로 인한 지연을 최소화해야 합니다.