개발 노트/Backend,CMS,API

[Backend] 웹 관리자 CMS 인증과 모바일 앱 토큰 인증 확실하게 분리하기

pposooj 2026. 5. 21. 13:10

웹 관리자 화면(CMS)과 모바일 앱 API를 한 서버 안에서 같이 개발하는 경우가 많습니다. 관리자 화면은 운영자가 브라우저로 로그인해서 데이터를 등록하고 수정하는 공간이고, 앱 API는 모바일 앱이 공개 데이터나 사용자별 데이터를 조회하는 인터페이스입니다.

둘 다 인증이 필요하긴 한데, 목적과 방식이 완전히 다릅니다. 이걸 대충 섞어서 개발하면 나중에 보안 경계가 흐려지고, 에러 처리할 때 앱이 오작동하는 큰 문제가 생깁니다.

처음에는 같은 서버에서 같은 유저 테이블(User Table)을 쓰니까 인증도 비슷하게 처리하면 되겠지 싶지만, 운영하다 보면 기능과 오류 처리 방식이 완전히 달라서 Route 단계부터 철저하게 분리해야 안전합니다. 저의 경우에도 예전에 이 경계를 모호하게 잡았다가 고생했던 기억이 있어서, 이번 기회에 CMS와 API 인증 구조를 설계할 때 놓치기 쉬운 기준들을 정리해 둡니다.

인증 경계가 흐려질 때 생기는 문제들

인증 경계가 명확하게 분리되어 있지 않으면 실무에서 다음과 같은 골치 아픈 문제들이 터집니다.

  • 관리자 세션 Middleware가 앱 API 영역에 어설프게 섞입니다.
  • 앱 토큰을 가지고 관리자 Route에 접근할 수 있는 보안 구멍이 생깁니다.
  • 인증이 실패했을 때 HTML 로그인 페이지가 리다이렉트되어 반환되는 바람에, 앱에서 에러 처리를 못 하고 터집니다.
  • 401 Unauthorized와 403 Forbidden이 뒤섞여서 앱에서 재로그인을 시켜야 하는지, 권한 부족 안내를 띄워야 하는지 구분하기 어렵습니다.
  • 토큰이나 Secret 값이 로그, 예제 코드, 오류 응답에 그대로 노출될 위험이 있습니다.
  • 관리자 권한 Scope와 앱 사용자 Scope가 같은 이름으로 뒤섞여 관리하기 힘들어집니다.
  • API Route Prefix가 명확하지 않아 보안 검수를 할 때 어디까지 막아야 하는지 범위가 흐려집니다.

인증은 처음에 그냥 "로그인 잘 되네?" 하고 넘어가기 쉽지만, 실제 운영에서는 "누가 어디에 접근할 수 있고, 실패했을 때 어떤 응답을 주느냐"가 훨씬 중요합니다.

관리자 인증 vs 앱 토큰 인증 차이점

두 인증은 태생부터 목적이 다릅니다. 관리자는 화면과 운영 권한 중심이고, 앱 토큰은 API 호출과 사용자 권한 중심입니다.

구분 관리자 인증 앱 토큰 인증
주체 운영자, 슈퍼 관리자 계정 모바일 앱 사용자 또는 앱 클라이언트
인증 방식 세션(Session), 쿠키(Cookie), 관리자 Guard Bearer Token, API Token (JWT 등)
접근 대상 CMS 웹 화면, 백오피스 등록/수정/삭제 앱 API 조회, 사용자별 개인 기능
실패 응답 웹 로그인 화면 리다이렉트 또는 관리자 전용 에러 웹페이지 규격화된 JSON 오류 응답
권한 기준 관리자 역할(Role), 메뉴별 접근 권한 사용자 Scope, API 기능별 Scope
로그 주의점 관리자 ID 및 행위 보호 Token 원문 절대 기록 금지

같은 DB 테이블을 바라보더라도 Middleware와 응답 형식은 무조건 따로 가야 합니다. 특히 앱 API 쪽에서는 절대로 HTML 로그인 페이지가 아니라, 앱이 바로 파싱해서 처리할 수 있는 JSON 응답을 유지해야 합니다.

1. Route Prefix부터 확실하게 쪼개기

가장 기본은 주소창(Route Prefix)부터 명확하게 분리하는 것입니다. 주소가 섞이면 Middleware, 에러 응답, Rate Limit 기준까지 다 같이 스파게티처럼 꼬이게 됩니다.

Plaintext
 
/admin/* -> 웹 관리자 CMS 전용 Route
/api/v1/* -> 모바일 앱 API 전용 Route
/internal/* -> 내부 서버 간 통신(MSA 등) 전용 Route

Laravel을 기준으로 예시를 들면, 아래처럼 Route 파일과 Middleware Group을 완전히 분리해서 관리하는 것이 좋습니다.

PHP
 
// routes/web.php (관리자 전용)
Route::prefix('admin')
    ->middleware(['web', 'auth:admin']) // 관리자 전용 guard 사용
    ->group(function () {
        Route::get('/notices', [AdminNoticeController::class, 'index']);
    });

// routes/api.php (모바일 앱 API 전용)
Route::prefix('v1')
    ->middleware(['api', 'auth:app-token']) // 앱 토큰 전용 guard 사용
    ->group(function () {
        Route::get('/me', [AppUserController::class, 'me']);
    });

여기서 핵심은 서로 다른 인증 Middleware와 Guard를 써야 한다는 점입니다. 주소창 prefix만 나눠놓고 정작 인증 guard를 같이 써버리면 경계를 나눈 의미가 퇴색되니 주의해야 합니다.

2. 401(인증 실패)과 403(권한 부족) 구분하기

앱 API 개발할 때 진짜 중요한 부분입니다. 에러 귀찮다고 전부 401이나 500으로 퉁쳐버리면 앱 프론트엔드 개발자가 분기 처리를 할 수 없습니다. 앱이 재로그인을 시켜야 하는 상황과, "권한이 없습니다" 안내만 띄우면 되는 상황을 구분해 줘야 합니다.

상황 HTTP Status Error Code 앱에서의 처리 방식
토큰이 아예 없음 401 AUTH_REQUIRED 로그인 화면으로 강제 이동
토큰이 만료됨 401 AUTH_EXPIRED 토큰 재발급(Refresh) 시도 또는 재로그인
토큰은 유효하나 권한 부족 403 FORBIDDEN "접근 권한이 없는 기능입니다" 팝업
앱 토큰으로 관리자 기능 접근 403 또는 404 FORBIDDEN 접근 원천 차단 및 무효화

이렇게 HTTP Status와 내부 Custom Error Code를 세트로 고정해서 전달해야 앱 쪽에서 깔끔하게 예외 처리를 먹일 수 있습니다.

3. 앱 토큰 Scope 설계 패턴

앱 토큰에는 딱 필요한 만큼의 권한(Scope)만 쥐여줘야 합니다. 당연한 소리겠지만 관리자 권한을 나타내는 Scope가 앱 토큰에 들어가면 절대 안 됩니다. 이름 자체에서 영역이 확실히 드러나도록 Prefix를 붙여 설계하는 편이 나중에 덜 헷갈립니다.

앱 사용자용 Scope 예시:

  • app:profile:read
  • app:notice:read
  • app:favorite:write
  • app:comment:write

웹 관리자용 Scope 예시:

  • admin:notice:create
  • admin:notice:update
  • admin:user:manage

만약 앱 토큰 권한을 검사하는데 admin:* 형태의 Scope가 들어갈 여지가 있다면 설계 단계에서부터 잘못된 것이니 구조를 다시 뜯어봐야 합니다.

4. Middleware에서 Scope 검증하기

아래는 API 요청이 들어왔을 때 토큰과 권한 Scope를 체크하는 Middleware 구현 예시입니다.

PHP
 
final class EnsureAppTokenScope
{
    public function handle($request, Closure $next, string $requiredScope)
    {
        $token = $request->user()?->currentAccessToken();

        // 1. 토큰 자체가 없는 경우 (401)
        if (!$token) {
            return ApiErrorResponse::make(
                'AUTH_REQUIRED',
                '로그인이 필요합니다.',
                401,
                requestId: $request->headers->get('X-Request-Id')
            );
        }

        // 2. 토큰은 있지만 필요한 Scope가 없는 경우 (403)
        if (!$token->can($requiredScope)) {
            return ApiErrorResponse::make(
                'FORBIDDEN',
                '접근 권한이 없습니다.',
                403,
                requestId: $request->headers->get('X-Request-Id')
            );
        }

        return $next($request);
    }
}

기본 프레임워크가 제공하는 인증 실패 응답을 그대로 쓰다 보면 가끔 HTML 뷰를 던질 때가 있는데, 앱 API 전용 Middleware 내에서는 무조건 공통 JSON 스펙으로 예외가 나가도록 꽉 잡아놔야 합니다.

5. 로그(Log)에서 토큰 원문 제거하기

디버깅 편하게 하겠다고 로그에 Request Header나 Body를 통째로 찍는 경우가 있습니다. 운영 환경에서 이거 정말 위험합니다. Authorization 헤더나 쿼리 스트링, Body에 포함된 토큰 정보는 로깅되기 전에 반드시 마스킹(***masked***) 처리를 거쳐야 합니다.

  • Authorization: Bearer ***masked***
  • access_token: ***masked***
  • refresh_token: ***masked***

로그 파일에는 문제 추적을 위한 최소한의 단서만 남기는 것이 원칙입니다.

  • 남겨도 안전한 정보: requestId, userId(일부 또는 내부 식별자 PK), route pattern, status code, error code
  • 절대 남기면 안 되는 정보: 토큰 원문, password, client_secret, Authorization 헤더 전체 원문, refresh token

6. 웹 관리자 Route 보호 체크리스트

앱 토큰이 웹 관리자 페이지에 어설프게 들이밀지 못하도록, 배포 전 아래 기준을 만족하는지 셀프 체크가 필요합니다.

  • [ ] /admin/* 경로는 오직 auth:admin 세션 Guard만 통과하는가?
  • [ ] /api/v1/* 경로는 auth:app-token 또는 사전에 정의된 Public Policy만 타는가?
  • [ ] 발급되는 앱 토큰에 admin:* 성격의 Scope가 부여될 가능성이 완전히 차단되었는가?
  • [ ] 관리자 페이지 인증 실패 시 발생하는 리다이렉트 동작이 앱 API 응답(JSON)에 영향을 주지 않는가?
  • [ ] 관리자 전용 Route 주소가 클라이언트 앱 소스코드나 Public API 문서에 노출되어 있지 않은가?

7. Public API와 인증 API 분리 운영

앱 API라고 해서 무조건 토큰을 요구할 필요는 없습니다. 비로그인 상태에서도 보여줘야 하는 데이터가 있으니까요. 성격에 따라 Middleware 적용 범위를 한 번 더 나누면 서버 자원을 아낄 수 있습니다.

API 유형 인증 필요 여부 예시 종류
공개 콘텐츠 API 불필요 (Public) 공지사항 리스트, 배너 이미지 목록, FAQ
사용자별 데이터 API 필수 (Private) 내 정보 수정, 즐겨찾기 목록, 장바구니
쓰기/수정 작업 API 필수 (Private) 댓글 작성, 1:1 문의 등록, 결제 요청
관리자 작업 API 앱 토큰 접근 불가 공지사항 등록/수정, 회원 강제 탈퇴 처리

Public API라고 하더라도 DDOS 공격이나 무분별한 크롤링을 막기 위해 최소한의 Rate Limit(디바이스당 요청 제한) 정책은 걸어두는 것이 안전합니다.

8. 제대로 분리되었는지 검증하는 테스트 기준

이론적으로 완벽해 보여도 코드가 꼬여있을 수 있으니, QA나 통합 테스트 시 아래 시나리오를 반드시 검증해 봅니다.

  1. 토큰 없이 인증 API 요청: 401 Unauthorized 상태코드와 함께 AUTH_REQUIRED JSON 코드가 떨어지는가?
  2. 만료된 토큰으로 요청: 401 상태코드와 AUTH_EXPIRED 코드가 명확히 반환되는가?
  3. 권한이 없는 권한(Scope)으로 요청: 403 Forbidden 상태코드와 FORBIDDEN 코드가 떨어지는가?
  4. 앱 토큰으로 관리자 Route 접근: 정상적인 앱 토큰을 헤더에 넣고 /admin/notices 같은 곳을 찔렀을 때, 세션이 없으므로 단호하게 막히는가?
  5. 관리자 세션으로 앱 API 접근: 브라우저에 관리자 세션 쿠키가 구워진 상태에서 앱 API 주소를 호출했을 때, 토큰 헤더가 없으므로 의도대로 401 에러를 뱉는가?
  6. 로그 검증: 의도적으로 인증 에러를 낸 뒤 서버 로그 파일(.log)을 열었을 때 Authorization 원문이나 토큰 문자열이 평문으로 찍혀있지 않은가?

실무에서 자주 하는 실수 요약

  • 실수 1: 관리자 화면이랑 앱 API를 '똑같은 인증 Middleware' 하나로 퉁쳐서 처리하는 것 (보안 경계 전멸)
  • 실수 2: 에러 응답 분기 안 해서 앱 화면에 느닷없이 HTML 웹 관리자 로그인 화면 코드가 출력되는 것
  • 실수 3: 개발할 때 디버깅 편하자고 로그에 토큰 문자열 평문으로 그대로 다 찍어놓고 운영 서버 올리는 것
  • 실수 4: 사용자 앱 토큰 하나로 내부 백오피스 API 기능까지 겸사겸사 호출할 수 있게 scope 범위를 넓게 잡아버리는 것

마무리

Backend/CMS 프로젝트를 빌드할 때 같은 데이터베이스와 서버 환경을 쓴다고 해서 인증 정책까지 한 바구니에 담으면 안 됩니다. 접근 목적, 권한의 깊이, 그리고 에러가 났을 때 클라이언트가 받아쳐야 하는 주체가 완전히 다르기 때문입니다.

처음 구조 잡을 때 Route 주소 명확히 쪼개고, 401/403 에러 맵핑 규칙만 제대로 고정해 놔도 나중에 앱 고도화되거나 보안 감사 들어왔을 때 야근할 일이 절반으로 줄어듭니다. 시스템 설계를 검토 중이시라면 위 기준들을 하나씩 대조해 보시길 권장합니다.