Node.js 애플리케이션에 역할 기반 접근 제어(RBAC) 구현하기
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
현대 웹 애플리케이션의 상호 연결성이 점점 높아짐에 따라 데이터의 보안 및 무결성을 보장하는 것이 무엇보다 중요합니다. 애플리케이션이 복잡해지고 사용자 기반이 확대됨에 따라, 누가 무엇을 할 수 있는지 관리하는 것이 중대한 과제가 됩니다. 모든 사용자가 역할에 관계없이 민감한 관리 기능에 접근하거나 중요한 비즈니스 데이터를 수정할 수 있는 시나리오를 상상해 보세요. 이러한 세분화된 제어 부족은 심각한 보안 위험을 초래할 뿐만 아니라 애플리케이션의 신뢰성과 신뢰도를 저하시킵니다. 바로 이 지점에서 강력한 권한 관리의 중요성이 부각되며, 역할 기반 접근 제어(RBAC)는 강력하고 널리 채택된 솔루션으로 등장합니다. RBAC는 사용자를 역할로 그룹화하여 권한 할당의 복잡한 작업을 단순화하여 관리를 더 체계적이고 확장 가능하게 만듭니다. 이 글에서는 기본 인증을 넘어 안전하고 유연한 권한 부여 계층을 제공하기 위해 Node.js 애플리케이션 내에서 RBAC를 효과적으로 구현하는 방법을 탐구할 것입니다.
RBAC 기본 사항 이해
구현 세부 사항으로 들어가기 전에 RBAC와 관련된 몇 가지 핵심 개념을 명확히 해보겠습니다.
- 사용자(User): 애플리케이션과 상호 작용하는 개인 또는 엔티티.
- 역할(Role): 사용자에게 할당된 권한의 컬렉션입니다. 권한을 사용자에게 직접 할당하는 대신, 사용자에게 하나 이상의 역할이 할당됩니다. 예로는 "관리자(Admin)", "편집자(Editor)", "뷰어(Viewer)", "모더레이터(Moderator)" 등이 있습니다.
- 권한(Permission): 애플리케이션 내의 원자적 권리 또는 기능입니다. 이는 승인된 사용자가 실제로 무엇을 할 수 있는지 정의합니다. 예로는 "게시물 생성(create_post)", "자신의 게시물 편집(edit_own_post)", "모든 사용자 삭제(delete_any_user)", "대시보드 보기(view_dashboard)"가 있습니다.
- 리소스(Resource): 권한이 적용되는 엔티티 또는 데이터입니다. API 엔드포인트, 데이터베이스 레코드, 파일 또는 기타 애플리케이션 구성 요소가 될 수 있습니다.
- 행동(Action): 리소스에 대해 수행될 수 있는 작업(예: 읽기, 쓰기, 업데이트, 삭제)입니다. 종종 권한은 행동과 리소스를 결합합니다(예: "게시물 읽기(read
)", "사용자 업데이트(update )").
RBAC의 기본 원칙은 권한을 역할에 할당한 다음, 역할을 사용자에게 할당하는 것입니다. 이 간접적인 방식은 역할의 권한 변경이 해당 역할에 할당된 모든 사용자에게 자동으로 적용되므로 권한 관리를 크게 단순화합니다.
RBAC 시스템 설계
Node.js에서 RBAC를 구현하려면 일반적으로 몇 가지 핵심 구성 요소가 필요합니다.
- 데이터 모델(Data Model): 사용자와 역할, 권한 및 이들의 연관성을 저장할 방법이 필요합니다. 일반적인 접근 방식은 관계형 데이터베이스를 사용하는 것이지만, NoSQL 데이터베이스도 사용할 수 있습니다.
- 권한 부여 미들웨어(Authorization Middleware): API 요청을 가로채고, 사용자의 역할을 확인하고, 요청된 작업을 수행할 수 있는 필요한 권한이 있는지 결정하는 로직 조각입니다.
- 권한 정의(Permission Definitions): 애플리케이션 내에서 사용 가능한 권한을 정의하고 관리하는 명확한 방법입니다.
코드 예제를 통한 실질적인 구현
Express.js와 가상 경로 세트를 사용하여 간단한 예제를 살펴보겠습니다. 단순성을 위해 역할과 권한은 메모리에 저장하지만, 실제 애플리케이션에서는 이러한 데이터가 데이터베이스에 상주합니다.
먼저 역할과 해당 권한을 정의해 봅시다.
// permissions.js const PERMISSIONS = { VIEW_DASHBOARD: 'view_dashboard', CREATE_POST: 'create_post', EDIT_OWN_POST: 'edit_own_post', EDIT_ANY_POST: 'edit_any_post', DELETE_POST: 'delete_post', DELETE_USER: 'delete_user', VIEW_USERS: 'view_users', }; const ROLES = { ADMIN: 'admin', EDITOR: 'editor', VIEWER: 'viewer', }; const rolePermissions = { [ROLES.ADMIN]: [ PERMISSIONS.VIEW_DASHBOARD, PERMISSIONS.CREATE_POST, PERMISSIONS.EDIT_ANY_POST, PERMISSIONS.DELETE_POST, PERMISSIONS.DELETE_USER, PERMISSIONS.VIEW_USERS, ], [ROLES.EDITOR]: [ PERMISSIONS.VIEW_DASHBOARD, PERMISSIONS.CREATE_POST, PERMISSIONS.EDIT_OWN_POST, ], [ROLES.VIEWER]: [ PERMISSIONS.VIEW_DASHBOARD, ], }; module.exports = { PERMISSIONS, ROLES, rolePermissions, };
다음으로 권한 부여 미들웨어를 생성합니다. 이 미들웨어는 일반적으로 인증 후에 실행되며, req.user
에는 역할이 포함된 인증된 사용자의 정보가 포함됩니다.
// authMiddleware.js const { rolePermissions } = require('./permissions'); function authorize(requiredPermissions) { return (req, res, next) => { // 실제 애플리케이션에서는 req.user가 인증 미들웨어에 의해 채워집니다. // 이 예제에서는 사용자를 시뮬레이션해 보겠습니다. const user = req.user || { id: 'someUserId', roles: [ 'editor' ] // 예: 이 사용자는 편집자입니다. }; if (!user || user.roles.length === 0) { return res.status(401).send('Authentication required.'); } const userPermissions = new Set(); user.roles.forEach(role => { if (rolePermissions[role]) { rolePermissions[role].forEach(permission => userPermissions.add(permission)); } }); const hasAllRequired = requiredPermissions.every(perm => userPermissions.has(perm)); if (hasAllRequired) { next(); // 사용자는 필요한 모든 권한을 가지고 있습니다. 라우트 핸들러로 진행합니다. } else { console.log(`User with roles ${user.roles.join(', ')} missing permissions: ${requiredPermissions.filter(perm => !userPermissions.has(perm)).join(', ')}`); return res.status(403).send('Forbidden: Insufficient permissions.'); } }; } module.exports = authorize;
이제 이를 Express 애플리케이션에 통합해 보겠습니다.
// app.js const express = require('express'); const authorize = require('./authMiddleware'); const { PERMISSIONS, ROLES } = require('./permissions'); const app = express(); const port = 3000; app.use(express.json()); // --- 인증 시뮬레이션 --- // 실제 앱에서는 Passport.js 또는 유사한 미들웨어가 될 것입니다. app.use((req, res, next) => { // 시연을 위해 헤더를 기반으로 다른 사용자를 모방해 보겠습니다. const userRoleHeader = req.headers['x-user-role']; if (userRoleHeader) { req.user = { id: 'mockUser123', roles: userRoleHeader.split(',').map(r => r.trim()) }; } else { req.user = { id: 'anonymous', roles: [] }; // 비인가된 사용자는 역할 없음 } next(); }); // --- 인증 시뮬레이션 종료 --- // 공개 경로 app.get('/', (req, res) => { res.send('Welcome to the application!'); }); // 관리자 대시보드 - 'view_dashboard' 권한 필요 app.get('/admin/dashboard', authorize([PERMISSIONS.VIEW_DASHBOARD]), (req, res) => { res.send(`Admin Dashboard accessed by user: ${req.user.id} with roles: ${req.user.roles.join(', ')}`); }); // 게시물 생성 - 'create_post' 권한 필요 app.post('/posts', authorize([PERMISSIONS.CREATE_POST]), (req, res) => { res.status(201).send(`Post created by user: ${req.user.id}`); }); // 게시물 수정 - 'edit_any_post' (관리자용) 또는 'edit_own_post' (편집자용) 권한 필요 // 참고: 세분화된 'edit_own_post'는 req.user.id와 게시물의 작성자 ID를 비교해야 합니다. // 이 예제에서는 일반 권한 확인만 수행합니다. app.put('/posts/:id', authorize([PERMISSIONS.EDIT_ANY_POST]), (req, res) => { res.send(`Post ${req.params.id} updated by user: ${req.user.id}`); }); // 사용자 삭제 - 'delete_user' 권한 필요 app.delete('/users/:id', authorize([PERMISSIONS.DELETE_USER]), (req, res) => { res.send(`User ${req.params.id} deleted by user: ${req.user.id}`); }); app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); console.log('Test with headers:'); console.log(` x-user-role: ${ROLES.ADMIN}`); console.log(` x-user-role: ${ROLES.EDITOR}`); console.log(` x-user-role: ${ROLES.VIEWER}`); console.log(' (Or no header for anonymous access)'); });
이를 테스트하려면 다음 단계를 따르세요.
node app.js
를 실행합니다.- cURL 또는 Postman과 같은 도구를 사용합니다.
- GET /admin/dashboard
x-user-role: admin
으로: 200 OKx-user-role: editor
로: 200 OKx-user-role: viewer
로: 200 OK- 헤더 없음: 401 Authentication required.
- POST /posts
x-user-role: admin
으로: 201 Createdx-user-role: editor
로: 201 Createdx-user-role: viewer
로: 403 Forbidden
- DELETE /users/123
x-user-role: admin
으로: 200 OKx-user-role: editor
로: 403 Forbidden
- GET /admin/dashboard
고급 고려 사항 및 애플리케이션 시나리오
위의 예는 기본적인 RBAC 설정을 제공합니다. 실제 애플리케이션은 종종 더 정교한 기능이 필요합니다.
- 동적 권한(Dynamic Permissions): 데이터베이스에 권한을 저장하면 관리자가 코드 변경 없이 역할-권한 매핑을 수정할 수 있습니다.
- 리소스 기반 권한(ABAC 하이브리드): 예를 들어,
edit_own_post
대edit_any_post
입니다. 이는 종종 RBAC와 속성 기반 접근 제어(ABAC)를 혼합하여 역할뿐만 아니라 사용자 및 리소스의 속성(예:req.user.id === post.authorId
)을 확인합니다. - 권한 상속(Permission Inheritance): 역할은 다른 역할로부터 권한을 상속받을 수 있습니다(예: "편집자"는 "뷰어"의 모든 권한을 상속받을 수 있습니다).
- 캐싱(Caching): 성능을 위해 미리 계산된 사용자 권한을 메모리 또는 Redis에 캐시할 수 있습니다.
- 전용 라이브러리(Dedicated Libraries): 복잡한 RBAC 요구 사항의 경우
casl
,accesscontrol
또는rbac-a
와 같은 라이브러리를 고려해 보세요. 이 라이브러리들은 권한 정의, 규칙 엔진 및 쿼리 빌더와 같은 더 강력한 기능을 제공합니다. - 프런트엔드 통합(Frontend Integration): 프런트엔드는 적절한 UI 요소(예: "편집" 버튼 표시/숨기기)를 표시하기 위해 사용자가 수행할 수 있는 작업을 알아야 하는 경우가 많습니다. 이는 전용 API 엔드포인트를 통해 사용자 권한을 노출하거나 초기 사용자 객체에 포함하여 달성할 수 있습니다.
RBAC는 사용자 권한을 미리 정의된 역할 집합으로 깔끔하게 분류할 수 있고 권한의 세분성이 주로 이러한 역할과 일치하는 애플리케이션에 가장 적합합니다. 예로는 CMS 플랫폼, 별도의 부서가 있는 내부 도구 또는 고객, 판매자 및 관리자 역할이 있는 전자 상거래 사이트가 있습니다.
결론
RBAC를 구현하는 것은 Node.js 애플리케이션을 보안하는 데 필수적인 단계로, 간단한 인증에서 벗어나 더 세분화되고 관리하기 쉬운 권한 부여 방식으로 나아갑니다. 역할을 신중하게 정의하고, 관련 권한을 할당하고, 효과적인 미들웨어를 사용하여 개발자는 허가된 사용자만이 특정 리소스에 대한 특정 작업을 수행할 수 있도록 할 수 있습니다. 이 구조화된 접근 방식은 보안을 강화할 뿐만 아니라 사용자 권한의 관리를 단순화하여 애플리케이션을 더욱 강력하고 확장 가능하게 만듭니다.