본문으로 건너뛰기

실전 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 접근 제어 구현하기

각 엔드포인트마다 접근 제어의 두 가지 측면을 고려해야 합니다:

  1. 리소스 소유권 - 사용자가 이 리소스의 소유자인가?
  2. 역할 기반 권한 - 사용자의 역할이 이 작업을 허용하는가?

각 엔드포인트별 접근 방식은 다음과 같습니다:

엔드포인트접근 제어 로직
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를 사용하겠습니다.

애플리케이션 설정하기

  1. Logto Console의 "Applications"에서 새 React 애플리케이션을 생성하세요.
    • 애플리케이션 이름: Content Management System
    • 애플리케이션 유형: Traditional Web Application
    • Redirect URIs: http://localhost:5173/callback

CMS React application

API 리소스 및 권한 설정하기

  1. Logto Console의 "API Resources"에서 새 API 리소스를 생성하세요.
    • API 이름: CMS API
    • API 식별자: https://api.cms.com
    • API 리소스에 권한 추가
      • list:articles
      • read:articles
      • create:articles
      • update:articles
      • publish:articles
      • delete:articles

CMS API resource details

역할 생성하기

Logto Console의 Roles에서 CMS를 위한 다음 역할을 생성하세요.

  • Admin
    • 모든 권한 포함
  • Publisher
    • read:articles, list:articles, publish:articles 권한 포함
  • Author
    • create:articles 권한 포함

Admin role

Publisher role

Author role

사용자에게 역할 할당하기

Logto Console의 "User management" 섹션에서 사용자를 생성하세요.

사용자 상세 정보의 "Roles" 탭에서 역할을 할당할 수 있습니다.

예시에서는 다음과 같이 3명의 사용자를 생성합니다:

  • Alex: Admin
  • Bob: Publisher
  • Charlie: Author

User management

User details - Alex

노트:

데모 목적상 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 구현을 테스트해봅시다.

노트:

"User Management"에서 생성한 사용자로 로그인할 수 없다면, 먼저 적절한 로그인 방식을 활성화해야 합니다. Logto Console의 "Sign-in & account > Sign-up and sign-in"에서 원하는 인증 (Authentication) 방식을 활성화하세요 (예: Email + Password 또는 Username + Password).

먼저, Alex와 Charles로 각각 로그인하여 기사를 생성해봅시다.

Alex는 Admin 역할이므로, 모든 기사 생성, 삭제, 수정, 게시, 조회가 가능합니다.

CMS dashboard - Alex

Charles는 Author 역할로, 자신의 기사만 생성할 수 있고, 자신이 소유한 기사만 조회, 수정, 삭제할 수 있습니다.

CMS dashboard - Charles - Article list

Bob은 Publisher 역할로, 모든 기사 조회 및 게시가 가능하지만, 생성, 수정, 삭제는 할 수 없습니다.

CMS dashboard - Bob

결론

축하합니다! 애플리케이션에 견고한 RBAC 시스템을 구현하는 방법을 배웠습니다.

멀티 테넌트 애플리케이션 구축 등 더 복잡한 시나리오의 경우, Logto는 포괄적인 조직(Organization) 지원을 제공합니다. 조직 단위 접근 제어 구현에 대해 더 알고 싶다면 멀티 테넌트 SaaS 애플리케이션 구축: 설계부터 구현까지 완벽 가이드를 참고하세요.

즐거운 코딩 되세요! 🚀