본문으로 건너뛰기

실제 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의 "애플리케이션"에서 새 React 애플리케이션을 생성하세요
    • 애플리케이션 이름: Content Management System
    • 애플리케이션 유형: 전통적인 웹 애플리케이션
    • Redirect URI: http://localhost:5173/callback

CMS React application

API 리소스 및 권한 설정하기

  1. 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

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의 "사용자 관리" 섹션에서 사용자를 생성하세요.

사용자 상세 정보의 "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 구현을 테스트해봅시다.

노트:

"사용자 관리"에서 생성한 사용자로 로그인할 수 없다면, 먼저 적절한 로그인 방식을 활성화해야 합니다. Logto Console의 "로그인 경험"에서 원하는 인증 (Authentication) 방식을 활성화하세요 (예: 이메일 + 비밀번호 또는 사용자명 + 비밀번호).

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

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

CMS dashboard - Alex

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

CMS dashboard - Charles - Article list

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

CMS dashboard - Bob

결론

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

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

행복한 코딩 되세요! 🚀