콘텐츠로 이동

역할 기반 권한 RBAC

11/9(월)

한 줄 요약: 로그인은 "누구인지"만 알려 준다. "관리자인지"는 앱이 규칙으로 정한다. 그리고 그 확인은 화면이 아니라 서버 API가 요청마다 해야 한다(로그인 안 함 401, 권한 없음 403).

📍 만난 상황

  • 2026-11-09 17:42 — 가짜 로그인 서버로 SSO 흐름 전체를 시험하는 방법(②안)을 들은 직후였다. → 그날 일지
  • 물었다: "일단 마이페이지 같은 걸 만들어야 하는 거 아니야? 그리고 관리자와 일반 사용자는 어떻게 구분하지?"
  • 답 1: 마이페이지는 "내 즐겨찾기"라서, "나"가 누구인지 알려 줄 로그인이 먼저다.
  • 답 2: 로그인은 신원만 준다. 관리자 여부는 우리 앱이 관리자 목록으로 정하고, 서버 API에서 막는다.

🧩 쉽게 말하면

회사 출입증과 서버실 출입 명단에 빗대면 된다. 출입증(로그인)은 "이 사람은 직원 ○○이다"만 보여 준다. 서버실에 들어가도 되는지는 출입증에 적혀 있지 않다.

서버실 앞 경비원이 명단(역할 규칙)을 보고 들여보낸다. 안내판에서 "서버실" 글자를 지워도(버튼 숨기기) 길을 아는 사람은 찾아온다. 그래서 문 앞 경비가 진짜 보안이다.

이게 없으면 로그인만 하면 누구나 승인 버튼을 누를 수 있다. 또는 사람마다 권한을 하나하나 따로 적어야 해서 관리가 안 된다.

📖 제대로 알기

인증과 인가

  • 인증(Authentication: "누구인가" 확인). 로그인이 하는 일이다. 결과는 사용자 ID(sub), 이름, 이메일, 그룹 정도다.
  • 인가(Authorization: "이걸 해도 되나" 판정). 앱이 규칙으로 정한다.
  • 영어로 둘 다 Auth로 시작해서 자주 섞인다. 줄여서 AuthN(인증), AuthZ(인가)라고 쓰기도 한다.

RBAC

  • RBAC(Role-Based Access Control: 역할 기반 접근 제어). 사람에게 권한을 직접 주지 않고, 역할에 권한을 묶은 뒤 사람에게 역할을 준다.
  • 역할(role): admin, user 같은 이름표.
  • 권한(permission): "제안 승인", "문의 답변" 같은 행동 하나.
  • 사람 100명에 권한 10개면 직접 주는 조합이 1,000개다. 역할 2개로 묶으면 "누가 어느 역할인가" 100개만 관리하면 된다.

역할을 정하는 방법

방법 예 맞는 경우
관리자 목록(설정 값) ADMIN_EMAILS 환경 변수 처음, 관리자 몇 명
그룹 ↔ 역할 표 로그인 groups → group_role_map 조직 그룹으로 관리할 때
사용자 표의 열 app_user.role 기록·표시용(판정의 원본은 위 둘)

세션과 상태 코드

  • 세션 쿠키(session cookie: 로그인 상태를 담아 브라우저가 매번 보내는 작은 값). 역할을 여기 적되, 서버가 서명(위조 방지 표시)한다. 이용자가 user를 admin으로 고치면 서명이 깨져 거절된다.
  • 401 Unauthorized: 누구인지 모름. 로그인 안 했거나 세션이 만료됨.
  • 403 Forbidden: 누구인지는 앎. 하지만 이 행동은 안 됨.

⚙️ 어떻게 동작하나

  1. 로그인(인증): 로그인 서버가 "이 사람은 sub=…, 이메일=…"을 알려 준다.
  2. 역할 정하기(인가 규칙): 앱이 관리자 목록이나 그룹 대응표를 본다. 있으면 admin, 없으면 user.
  3. 기록하기: 서명한 세션 쿠키에 역할을 적는다. app_user.role에도 남긴다(누가 승인했는지 이력에 쓰려고).
  4. 요청마다 검사: 관리자 API(/api/admin/...)는 맨 앞에서 세션을 읽는다. 없으면 401, admin이 아니면 403.
  5. 화면은 편의: 관리자에게만 "관리자 메뉴", "승인" 버튼을 보여 준다. 숨기기일 뿐 보안이 아니다.

sequenceDiagram
  participant B as 브라우저
  participant A as 서버 API
  participant D as 대시보드 DB
  B->>A: POST 승인 요청 (세션 쿠키)
  A->>A: 세션 서명 확인
  alt 세션 없음
    A-->>B: 401 로그인 필요
  else role이 user
    A-->>B: 403 권한 없음
  else role이 admin
    A->>D: 제안 승인 + 변경 기록
    A-->>B: 200 완료
  end
검사는 DB를 건드리기 전에 서버에서 끝난다. 버튼을 숨겼든 아니든, 주소를 직접 불러도 같은 검사를 거친다.

🧪 예시

  • 예시 1 · 일상에 빗대어: 직원 50명 회사에 서버실 담당이 3명이다. 출입증은 50명 모두 같다. 경비원 명단에 3명만 있다. 47명 중 누가 서버실 문을 두드리면 경비원이 "직원은 맞지만 명단에 없습니다"라고 돌려보낸다(403). 출입증 없는 외부인은 "누구신지 먼저 확인할게요"(401)다.
  • 예시 2 · 실제로는: 관리자 API 맨 앞에 넣는 검사다.

// src/app/api/admin/suggestions/[id]/approve/route.ts
export async function POST(req: Request) {
  const me = await getSessionUser();      // ① 서명된 세션 읽기
  if (!me) return json(401, "로그인 필요");  // ② 누구인지 모름
  if (me.role !== "admin")
    return json(403, "권한 없음");          // ③ 누구인지 알지만 안 됨
  // ④ 여기부터 실제 승인 처리
  return approveSuggestion(req, me.id);
}
- ① 쿠키를 읽고 서명을 확인한다. 고쳐진 쿠키는 여기서 버려진다. - ② 세션이 없으면 401. 로그인 화면으로 보낼 수 있다. - ③ 역할이 admin이 아니면 403. 버튼이 없던 사람이 주소를 직접 불러도 여기서 막힌다. - ④ me.id를 넘겨 "누가 승인했는지"를 변경 기록에 남긴다.

역할을 정하는 쪽은 이렇게 생긴다.

const admins = (process.env.ADMIN_EMAILS ?? "").split(",");
const role = admins.includes(profile.email) ? "admin" : "user";
- 관리자를 더하거나 빼려면 이 환경 변수만 고친다. 운영에서는 배포 설정에 둔다.

⚖️ 비교 · 흔한 오해

401과 403

코드 뜻 이용자가 할 일
401 누구인지 모름 로그인한다
403 누구인지 알지만 안 됨 권한을 요청한다

오해 1: "로그인하면 역할도 알려 준다"

  • 실제: 로그인 서버는 신원과 그룹 정도만 준다. "이 그룹이면 대시보드 관리자"라는 규칙은 대시보드가 정한다. 같은 사람이 A 앱에선 관리자, B 앱에선 일반 사용자일 수 있다.
  • 왜 그렇게 생각하기 쉬운가: 회사 시스템에 로그인하면 바로 내 메뉴가 보이는 경험 때문이다. 로그인 직후 일어나는 역할 판정이 눈에 보이지 않아서 로그인이 다 해 준 것처럼 느껴진다.

오해 2: "버튼을 숨기면 안전하다"

  • 실제: 버튼은 화면일 뿐이다. 개발자 도구나 curl로 같은 주소를 직접 부를 수 있다. 서버가 막아야 한다.

오해 3: "역할을 쿠키에 적으면 된다"

  • 실제: 그냥 적으면 이용자가 고친다. 서명된 쿠키여야 한다. 또는 쿠키엔 세션 번호만 두고 역할은 서버에서 찾는다.

비교: 플랫폼 권한과는 층이 다르다

dhub2 플랫폼은 "누가 어떤 데이터셋을 읽을 수 있나"를 따로 판정한다. 대시보드 RBAC는 "누가 대시보드의 승인·관리 기능을 쓰나"만 정한다. 둘은 겹치지 않고 각자 자기 것을 지킨다.

🛠️ 실무에서는

  • 기본은 거부: 역할을 모르면 user로, 규칙에 없으면 막는다. "일단 열고 나중에 막기"는 사고가 난다.
  • 체크리스트
  • 관리자 API마다 맨 앞에 401·403 검사가 있다(공통 함수로 묶기).
  • 세션 쿠키가 서명돼 있다.
  • 역할 판정 원본이 한 곳이다(관리자 목록 또는 그룹 대응표).
  • 승인 같은 행동에 "누가 했는지"를 기록한다.
  • 개발용 페르소나 전환(관리자로 보기/일반으로 보기)은 운영에서 꺼져 있다.
  • 역할 변경 반영 시점: 역할을 세션에 적었다면, 목록에서 빼도 다음 로그인까지 옛 역할이 남는다. 바로 막아야 하면 세션을 짧게 하거나 요청 때 다시 확인한다.
  • 문제가 생기면 먼저 볼 것
  • 관리자인데 403 → 이메일 대소문자·공백, 환경 변수 반영 여부, 다시 로그인했는지.
  • 다들 401 → 쿠키 이름·도메인, 서명 키가 서버마다 다른지.

❓ 자주 묻는 질문

  • Q. 이메일로 관리자를 정해도 되나? A. 처음 몇 명이면 간단해서 괜찮다. 다만 이메일은 바뀔 수 있어서, 사람이 늘면 sub나 그룹 기준으로 옮기는 편이 안전하다.
  • Q. 403 대신 404를 돌려주는 곳도 있던데? A. "그런 기능이 있다는 것 자체"를 숨기고 싶을 때 쓴다. 관리자 화면 정도면 403이 디버깅에 더 편하다.
  • Q. 화면 숨기기는 아예 안 해도 되나? A. 해야 한다. 보안이 아니라 사용성이다. 못 누르는 버튼을 보여 주면 헷갈린다.
  • Q. 역할이 더 잘게 필요해지면? A. "데이터셋 담당자" 같은 역할을 더하거나, 대상별 권한(누가 어떤 데이터셋을)을 따로 둔다. 후자는 관계 기반 권한(ReBAC)이라 부른다.

🔭 더 깊이 보기

  • 세션 쿠키와 JWT는 어떻게 다른가? — 역할을 어디에 담고 어떻게 위조를 막는지가 갈린다.
  • ABAC·ReBAC는 RBAC와 무엇이 다른가? — 플랫폼 권한 모델처럼 "대상별 권한"이 필요할 때의 다음 단계다.
  • OWASP의 접근 제어 취약점은 무엇이 있나? — 서버 검사를 빠뜨린 실제 사고 유형을 익힐 수 있다.
  • 관련 주제: OIDC 로그인 흐름 · 프론트엔드와 백엔드 서버 구조 · ERD 관계와 카디널리티

📚 참고 자료

  • HTTP 인증 (MDN 한국어) — 401 응답과 인증 흐름의 기본 · 링크
  • 403 Forbidden (MDN 한국어) — 401과 403을 언제 쓰는지 · 링크
  • Authorization Cheat Sheet (OWASP, 영문) — 기본 거부, 서버 쪽 검사 같은 인가 원칙 · 링크

🗓️ 이력

  • 2026-11-09: 마이페이지보다 로그인이 먼저인지, 관리자와 일반 사용자를 어떻게 구분하는지 물음. 인증·인가 구분과 서버 API의 401·403 검사를 정리 → 일지

🕸️ 연결된 주제