실제 RBAC 적용: 애플리케이션을 위한 안전한 인가 (Authorization) 구현
애플리케이션에 안전하고 확장 가능한 인가 (Authorization) 시스템을 구현하는 데 어려움을 겪고 계신가요? 역할 기반 접근 제어 (RBAC)는 사용자 권한 관리를 위한 업계 표준이지만, 올바르게 구현하는 것은 쉽지 않습니다. 이 튜토리얼에서는 실제 콘텐츠 관리 시스템 (CMS) 예제를 통해 견고한 RBAC 시스템을 구축하는 방법을 안내합니다.
이 가이드를 따라가면 다음을 배울 수 있습니다:
- ✨ 세밀한 권한을 설계하고 구현하여 정밀한 제어를 하는 방법
- 🔒 의미 있는 역할로 권한을 체계적으로 구성하는 모범 사례
- 👤 리소스 소유권을 효과적으로 처리하는 기법
- 🚀 인가 (Authorization) 시스템을 확장 가능하고 유지보수하기 쉽게 만드는 방법
- 💡 실제 CMS 예제를 통한 실전 구현
이 튜토리얼의 전체 소스 코드는 GitHub 에서 확인할 수 있습니다.
RBAC 기본 원리 이해하기
역할 기반 접근 제어 (RBAC)는 단순히 사용자에게 권한을 할당하는 것 이상입니다. 보안과 유지보수성을 균형 있게 고려한 구조적인 인가 (Authorization) 접근 방식을 만드는 것이 핵심입니다.
RBAC이란 무엇인가 에 대해 Auth Wiki에서 더 자세히 알아볼 수 있습니다.
우리의 구현에서 따를 주요 원칙은 다음과 같습니다:
세밀한 권한 설계
세밀한 권한은 사용자가 시스템에서 무엇을 할 수 있는지에 대해 정밀하게 제어할 수 있게 해줍니다. "admin"이나 "user"와 같은 광범위한 접근 수준 대신, 리소스에 대해 사용자가 수행할 수 있는 구체적인 동작을 정의합니다. 예를 들어:
read:articles
- 시스템 내 모든 기사 보기create:articles
- 새 기사 생성update:articles
- 기존 기사 수정publish:articles
- 기사 게시 상태 변경
리소스 소유권 및 접근 제어
리소스 소유권은 CMS 인가 (Authorization) 설계의 핵심 개념입니다. RBAC이 역할별로 수행 가능한 작업을 정의하는 반면, 소유권은 접근 제어에 개인적인 차원을 추가합니다:
- 작성자는 자신이 생성한 기사에 자동으로 접근할 수 있습니다
- 이 자연스러운 소유권 모델 덕분에 작성자는 항상 자신의 콘텐츠를 보고 편집할 수 있습니다
- 시스템은 기사 작업 처리 시 역할 권한 또는 소유권을 모두 확인합니다
- 예를 들어,
update:articles
권한이 없어도 작성자는 자신의 기사를 편집할 수 있습니다 - 이 설계는 추가적인 역할 권한 필요성을 줄이면서도 보안을 유지합니다
이중 레이어 접근 방식(역할 + 소유권)은 더 직관적이고 안전한 시스템을 만듭니다. 퍼블리셔와 관리자는 역할 권한을 통해 모든 콘텐츠를 관리할 수 있고, 작성자는 자신의 작업을 직접 통제할 수 있습니다.
안전한 API 설계하기
이제 CMS의 핵심 기능을 API 엔드포인트로 설계해봅시다:
GET /api/articles # 모든 기사 목록 조회
GET /api/articles/:id # 특정 기사 조회
POST /api/articles # 새 기사 생성
PATCH /api/articles/:id # 기사 수정
DELETE /api/articles/:id # 기사 삭제
PATCH /api/articles/:id/published # 게시 상태 변경
API에 접근 제어 구현하기
각 엔드포인트마다 접근 제어의 두 가지 측면을 고려해야 합니다:
- 리소스 소유권 - 사용자가 이 리소스의 소유자인가?
- 역할 기반 권한 - 사용자의 역할이 이 작업을 허용하는가?
각 엔드포인트별 접근 제어 방식은 다음과 같습니다:
엔드포인트 | 접근 제어 로직 |
---|---|
GET /api/articles | - list:articles 권한이 있거나, 작성자는 자신의 기사만 볼 수 있음 |
GET /api/articles/:id | - read:articles 권한이 있거나, 해당 기사의 작성자 |
POST /api/articles | - create:articles 권한이 있는 사용자 |
PATCH /api/articles/:id | - update:articles 권한이 있거나, 해당 기사의 작성자 |
DELETE /api/articles/:id | - delete:articles 권한이 있거나, 해당 기사의 작성자 |
PATCH /api/articles/:id/published | - publish:articles 권한이 있는 사용자만 가능 |
확장 가능한 권한 시스템 만들기
API 접근 요구사항을 바탕으로 다음과 같은 권한을 정의할 수 있습니다:
권한 | 설명 |
---|---|
list:articles | 시스템 내 모든 기사 목록 보기 |
read:articles | 모든 기사의 전체 내용 읽기 |
create:articles | 새 기사 생성 |
update:articles | 모든 기사 수정 |
delete:articles | 모든 기사 삭제 |
publish:articles | 게시 상태 변경 |
이 권한들은 자신이 소유하지 않은 리소스에 접근할 때만 필요합니다. 기사 소유자는 자동으로:
- 자신의 기사 보기 (
read:articles
불필요) - 자신의 기사 편집 (
update:articles
불필요) - 자신의 기사 삭제 (
delete:articles
불필요)
효과적인 역할 만들기
API와 권한을 정의했으니, 이제 논리적으로 권한을 묶는 역할을 만들 수 있습니다:
권한/역할 | 👑 관리자(Admin) | 📝 퍼블리셔(Publisher) | ✍️ 작성자(Author) |
---|---|---|---|
설명 | 전체 콘텐츠 관리 권한 | 모든 기사 보기 및 게시 상태 관리 | 시스템 내 새 기사 작성 |
list:articles | ✅ | ✅ | ❌ |
read:articles | ✅ | ✅ | ❌ |
create:articles | ✅ | ❌ | ✅ |
update:articles | ✅ | ❌ | ❌ |
delete:articles | ✅ | ❌ | ❌ |
publish:articles | ✅ | ✅ | ❌ |
참고: 작성자는 역할 권한과 상관없이 자신의 기사에 대해 읽기/수정/삭제 권한을 자동으로 가집니다.
각 역할은 특정 책임에 맞게 설계되었습니다:
- 관리자(Admin): CMS의 모든 기사 작업을 포함한 전체 제어권 보유
- 퍼블리셔(Publisher): 콘텐츠 검토 및 게시 관리에 집중
- 작성자(Author): 콘텐츠 생성에 특화
이 역할 구조는 명확한 역할 분리를 만듭니다:
- 작성자는 콘텐츠 생성에 집중
- 퍼블리셔는 콘텐츠 품질 및 노출 관리
- 관리자는 전체 시스템 제어
Logto에서 RBAC 설정하기
시작하기 전에 Logto Cloud 에서 계정을 생성하거나, Logto OSS 버전 을 사용하여 자체 호스팅 Logto 인스턴스를 사용할 수 있습니다.
이 튜토리얼에서는 간편하게 Logto Cloud를 사용합니다.
애플리케이션 설정하기
- Logto Console의 "애플리케이션"에서 새 React 애플리케이션을 생성하세요
- 애플리케이션 이름: Content Management System
- 애플리케이션 유형: 전통적인 웹 애플리케이션
- Redirect URI: http://localhost:5173/callback
API 리소스 및 권한 설정하기
- Logto Console의 "API 리소스"에서 새 API 리소스를 생성하세요
- API 이름: CMS API
- API 식별자: https://api.cms.com
- API 리소스에 권한 추가
list:articles
read:articles
create:articles
update:articles
publish:articles
delete:articles
역할 생성하기
Logto Console의 "Roles"에서 CMS를 위한 다음 역할을 생성하세요
- Admin
- 모든 권한 포함
- Publisher
read:articles
,list:articles
,publish:articles
권한 포함
- Author
create:articles
권한 포함
사용자에게 역할 할당하기
Logto Console의 "사용자 관리" 섹션에서 사용자를 생성하세요.
사용자 상세 정보의 "Roles" 탭에서 사용자에게 역할을 할당할 수 있습니다.
예시에서는 다음과 같이 3명의 사용자를 생성합니다:
- Alex: Admin
- Bob: Publisher
- Charlie: Author
데모 목적상, Logto Console을 통해 이러한 리소스와 구성을 생성합니다. 실제 프로젝트에서는 Logto에서 제공하는 Management API 를 사용해 프로그래밍적으로 리소스와 구성을 생성할 수 있습니다.
프론트엔드에 Logto RBAC 연동하기
이제 Logto에서 RBAC를 설정했으니, 프론트엔드에 연동할 수 있습니다.
먼저 Logto 빠른 시작 을 따라 애플리케이션에 Logto를 연동하세요.
예시에서는 React 를 사용합니다.
애플리케이션에 Logto를 설정한 후, Logto가 동작할 수 있도록 RBAC 구성을 추가해야 합니다.
// frontend/src/App.tsx
const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // Logto Console에서 생성한 앱 ID
endpoint: LOGTO_ENDPOINT, // Logto Console에서 생성한 엔드포인트
resources: [API_RESOURCE], // Logto Console에서 생성한 API 리소스 식별자, 예: https://api.cms.com
// 프론트엔드에서 API 리소스에 요청할 모든 스코프
scopes: [
'list:articles',
'create:articles',
'read:articles',
'update:articles',
'delete:articles',
'publish:articles',
],
};
이미 로그인된 상태라면, 변경 사항을 적용하려면 반드시 로그아웃 후 다시 로그인하세요.
사용자가 Logto로 로그인하고 위에서 지정한 API 리소스에 대한 액세스 토큰을 요청하면, Logto는 사용자의 역할에 따라 관련 스코프(권한)를 액세스 토큰에 추가합니다.
useLogto
훅의 getAccessTokenClaims
를 사용하여 액세스 토큰에서 스코프를 가져올 수 있습니다.
// frontend/src/hooks/use-user-data.ts
import { useLogto } from '@logto/react';
import { API_RESOURCE } from '../config';
import { useState, useEffect } from 'react';
export const useUserData = () => {
const { getAccessTokenClaims } = useLogto();
const [userScopes, setUserScopes] = useState<string[]>([]);
const [userId, setUserId] = useState<string>();
useEffect(() => {
const fetchScopes = async () => {
const token = await getAccessTokenClaims(API_RESOURCE);
setUserScopes(token?.scope?.split(' ') ?? []);
setUserId(token?.sub);
};
fetchScopes();
}, [getAccessTokenClaims]);
return { userId, userScopes };
};
그리고 userScopes
를 사용하여 사용자가 리소스에 접근할 권한이 있는지 확인할 수 있습니다.
// frontend/src/pages/Dashboard.tsx
const Dashboard = () => {
const { userId, userScopes } = useUserData();
// ...
return (
<div>
{/* ... */}
{(userScopes.includes('delete:articles') || article.ownerId === userId) && (
<button
onClick={() => handleDelete(article.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
)}
</div>
);
};
백엔드에 Logto RBAC 연동하기
이제 Logto RBAC를 백엔드에 연동할 차례입니다.
백엔드 인가 (Authorization) 미들웨어
먼저, 백엔드에 미들웨어를 추가하여 사용자의 권한을 확인하고, 로그인 여부 및 특정 API 접근 권한이 있는지 검증해야 합니다.
// backend/src/middleware/auth.js
const { createRemoteJWKSet, jwtVerify } = require('jose');
const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';
if (!authorization) {
throw new Error('Authorization header missing');
}
if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}
return authorization.slice(bearerTokenIdentifier.length + 1);
};
const hasScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const verifyJwt = async (token) => {
const JWKS = createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL));
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.LOGTO_ISSUER,
audience: process.env.LOGTO_API_RESOURCE,
});
return payload;
};
const requireAuth = (requiredScopes = []) => {
return async (req, res, next) => {
try {
// 토큰 추출
const token = getTokenFromHeader(req.headers);
// 토큰 검증
const payload = await verifyJwt(token);
// 사용자 정보 요청에 추가
req.user = {
id: payload.sub,
scopes: payload.scope?.split(' ') || [],
};
// 필수 스코프 검증
if (!hasScopes(req.user.scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
hasScopes,
};
이 미들웨어에서는 프론트엔드 요청에 유효한 액세스 토큰이 포함되어 있는지, 그리고 액세스 토큰의 audience가 Logto Console에서 생성한 API 리소스와 일치하는지 확인합니다.
API 리소스를 검증하는 이유는, 이 API 리소스가 실제로 CMS 백엔드의 리소스를 대표하며, 모든 CMS 권한이 이 API 리소스와 연결되어 있기 때문입니다.
이 API 리소스가 Logto에서 CMS 리소스를 대표하므로, 프론트엔드 코드에서는 백엔드에 API 요청을 보낼 때 해당 액세스 토큰을 함께 전송합니다:
// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();
return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// API 리소스용 액세스 토큰 가져오기
const token = await getAccessToken(API_RESOURCE);
if (!token) {
throw new ApiRequestError('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
// 요청 헤더에 액세스 토큰 추가
Authorization: `Bearer ${token}`,
...options.headers,
},
});
// ... 응답 처리
return await response.json();
} catch (error) {
// ... 에러 처리
}
},
[getAccessToken]
);
};
이제 requireAuth
미들웨어를 사용해 API 엔드포인트를 보호할 수 있습니다.
API 엔드포인트 보호하기
특정 권한이 있는 사용자만 접근할 수 있는 API에는 미들웨어에서 직접 제한을 추가할 수 있습니다. 예를 들어, 기사 생성 API는 create:articles
권한이 있는 사용자만 접근할 수 있습니다:
// backend/src/routes/articles.js
const { requireAuth } = require('../middleware/auth');
router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});
권한과 리소스 소유권을 모두 확인해야 하는 API의 경우, hasScopes
함수를 사용할 수 있습니다. 예를 들어, 기사 목록 API에서는 list:articles
권한이 있는 사용자는 모든 기사를 볼 수 있고, 작성자는 자신이 작성한 기사만 볼 수 있습니다:
// backend/src/routes/articles.js
const { requireAuth, hasScopes } = require('../middleware/auth');
router.get('/articles', requireAuth(), async (req, res) => {
try {
// list:articles 스코프가 있으면 모든 기사 반환
if (hasScopes(req.user.scopes, ['list:articles'])) {
const articles = await articleDB.list();
return res.json(articles);
}
// 그렇지 않으면 사용자의 기사만 반환
const articles = await articleDB.listByOwner(req.user.id);
res.json(articles);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch articles' });
}
});
이제 RBAC 구현이 완료되었습니다. 전체 소스 코드 를 참고하여 전체 구현을 확인할 수 있습니다.
CMS RBAC 구현 테스트하기
이제, 방금 생성한 세 명의 사용자를 이용해 CMS RBAC 구현을 테스트해봅시다.
"사용자 관리"에서 생성한 사용자로 로그인할 수 없다면, 먼저 적절한 로그인 방식을 활성화해야 합니다. Logto Console의 "로그인 경험"에서 원하는 인증 (Authentication) 방식을 활성화하세요 (예: 이메일 + 비밀번호 또는 사용자명 + 비밀번호).
먼저, Alex와 Charles로 각각 로그인하여 기사를 생성해봅시다.
Alex는 Admin 역할이므로, 기사 생성, 삭제, 수정, 게시, 모든 기사 보기 모두 가능합니다.
Charles는 Author 역할이므로, 자신의 기사만 생성/조회/수정/삭제할 수 있습니다.
Bob은 Publisher 역할로, 모든 기사를 조회 및 게시할 수 있지만 생성, 수정, 삭제는 할 수 없습니다.
결론
축하합니다! 애플리케이션에 견고한 RBAC 시스템을 구현하는 방법을 배웠습니다.
멀티 테넌트 애플리케이션 구축 등 더 복잡한 시나리오가 필요하다면, Logto는 포괄적인 조직(Organization) 지원을 제공합니다. 조직 단위 접근 제어 구현에 대해 더 알고 싶다면 멀티 테넌트 SaaS 애플리케이션 구축: 설계부터 구현까지 완벽 가이드 를 참고하세요.
행복한 코딩 되세요! 🚀