Skip to content

권한 설정

관리자 화면의 권한 체계를 다룹니다.

전체 그림

핵심은 권한 코드를 별도로 관리하지 않고 라우트 설정에서 유도한다는 점입니다.

routes.ts 의 라우트 정의
   │  name / meta.id  → 메뉴 권한 코드      (예: term-list)
   │  meta.title      → 화면에 보이는 메뉴명 (예: 약관 관리)

[메뉴 권한 관리] 화면이 전체 메뉴 목록을 자동 생성

   ├─ 역할(ROLE) 또는 사용자에게 할당 → DB 저장 (term-list::r 형태)

   ├─ 프론트: 권한 없는 메뉴는 라우트가 등록되지 않음 → 접근 불가
   └─ 서버 : @PreAuthorize 가 같은 코드로 검사

왜 라우트에서 유도하나

권한 목록을 따로 관리하면 화면이 추가될 때마다 두 곳을 고쳐야 하고, 빠뜨리면 "메뉴는 보이는데 권한이 없는" 또는 그 반대 상태가 생깁니다.

라우트가 유일한 출처이므로 화면을 추가하면 권한 항목이 자동으로 따라옵니다.

프론트 제어는 편의일 뿐 보안이 아닙니다

권한 없는 메뉴를 감추는 것은 UX 입니다. 브라우저 쪽 코드는 사용자가 조작할 수 있으므로, 실제 차단은 서버의 @PreAuthorize 가 합니다.

프론트에서 메뉴를 숨겼다고 해서 서버 검사를 생략하면 안 됩니다 → API 권한

1. 화면 메뉴 제어

관리자 화면의 메뉴 제어는 Setting > 메뉴 권한 관리 메뉴에서 설정하며,
메뉴 권한 관리 는 route 설정을 기반으로 전체 메뉴 목록을 생성한다.

1.1 메뉴명과 메뉴 권한 코드

메뉴 권한 관리 에서 나오는 메뉴명은 route 설정의 meta.title 을 사용하며, 메뉴 권한 코드name 이 기본적으로 사용된다.
즉, 아래 route 설정에서는 메뉴명약관 관리가 된며, 메뉴 권한 코드term-list 가 된다.

json
{
    path: 'term', 
    name: 'term-list',
    component: () => import('src/views/term/term-list.vue'),
    meta: { title: '약관 관리' }
}

만약 메뉴 권한 코드로 route 설정의 name 이 아닌 다른값을 사용 하고 싶으면, meta 하위에 id 값을 정의하면, id 값을 메뉴 권한 코드로 사용하게 된다.
아래 route 설정에서는 menu:term 이 권한 코드가 된다.

json
{
    path: 'term', name: 'term-list',
    component: () => import('src/views/term/term-list.vue'),
    meta: {
        id: 'menu:term', 
        title: '약관 관리'
    }
}

1.2. 상세 권한 코드

기본으로 지원되는 상세 권한은 생성(c), 읽기(r), 수정(u), 삭제(d), 엑셀다운로드(x) 이다.
각 상세 권한은 메뉴 권한 코드 뒤에 :: 를 붙여서, 데이터베이스에 저장된다.

메뉴 권한 코드상세 권한 코드최종 코드
term-listcterm-list::c
term-listrterm-list::r
term-listuterm-list::u
term-listdterm-list::d
term-listxterm-list::x

1.3. 추가 확장 권한 코드

만약 위 권한들 외에, 해당 메뉴에 추가적인 별도의 권한이 필요하다면, 다음과 같이 meta.authExtTypes 에 해당 항목을 추가하면 된다.

json
{
    path: 'admin', name: 'admin-list',
    component: () => import('src/views/admin/admin-list.vue'),
    meta: {
        title: '관리자 관리',
        authExtTypes: [{ label: '역할 지정', value: 'roles' }, { label: '비밀번호 초기화', value: 'pwd-init' }]  
    }
},

위 예에서는 역할 지정(roles), 비밀번호 초기화(pwd-init) 이라는 두개의 상세 권한을 추가한 것이다.
이를 역할 또는 사용자 에게 할당을 하게되면 각각 admin-list::roles, admin-list::pwd-init 으로 디비에 저장되게 된다.

이렇게 추가한 확장 권한은 메뉴 권한 관리 화면에 다음과 같이 selectbox 형태로 노출되게 된다.

새 프로젝트 템플릿 버튼

1.4. API 권한

각각의 메뉴 권한은, 해당 메뉴를 구성하는 domain 에 대한 API 사용 권한이 자동으로 주어진다.

"자동으로 주어진다" 의 실제 의미

권한이 런타임에 파생되는 게 아닙니다. API 를 담당하는 Resource 클래스가 검사하는 권한 코드를 메뉴 권한 코드와 같게 맞춰 NeoSQL 이 생성하기 때문에, 결과적으로 메뉴 권한만 주면 API 도 통과합니다.

term-list 메뉴 권한을 가진 사용자는 TermResource@PreAuthorize 를 통과합니다 — 둘 다 term-list 라는 같은 문자열을 보기 때문입니다.

왜 메뉴 권한과 API 권한을 나눠 두었나

Resource 는 두 가지 코드를 검사합니다.

java
public static final String PERMISSION_MENU_ID = "member-list";   // 이 화면을 쓰는 사람
public static final String PERMISSION_API_ID  = "api:member";    // 이 API 만 쓰는 사람

hasPrefixAuth(MENU_ID, READ) || hasPrefixAuth(API_ID, READ)둘 중 하나면 통과입니다.

이렇게 나눈 이유는 다른 화면에서 이 API 만 필요한 경우 때문입니다. 메일 발송 화면에서 회원을 검색해야 한다고 해서 member-list(회원 관리 메뉴) 권한을 통째로 주면, 회원 관리 화면 전체가 열려버립니다. api:member::r 만 주면 필요한 API 만 열립니다.

만약 특정 메뉴를 사용하는데, 다른 메뉴에서 사용하는 API 기능 필요하다면,
예를 들어 아래와 같이 메일을 작성하는 메뉴에서, 대상자를 찾기 위해 회원 정보를 가져와야 한다면. 다음과 같이 meta.authDependsOn 항목에 해당 권한을 추가한다.

json
{
    path: 'message/mail', name: 'mail-list',
    component: () => import('src/views/message/mail-list.vue'),
    meta: {
        title: 'MAIL 내역',
        authExtTypes: [{ label: '발송', value: 'send' }],
        authDependsOn: [API_PERMISSION.MEMBER.READ]
    }
}

이렇게 하면 menu-list 권한을 가지는 사용자(또는 역할)는 API_PERMISSION.MEMBER.READ(api:member::r) 또한 자동으로 가지게 된다.

다음은 constants.ts 에 선언된 API_PERMISSION 내용이다.

json
export const API_PERMISSION = {
    META: {
        READ: 'api:meta::r'
    },
    MEMBER: {
        READ: 'api:member::r'
    }
}

서버단(MemberResource.java)에도 api:member 권한에 대한 허용 처리가 되어 있는지 확인해 보도록 한다.

java
@Slf4j
@RestController
public class MemberResource {

    public static final String PERMISSION_MENU_ID = "member-list";
    public static final String PERMISSION_API_ID = "api:member"; 

    @GetMapping(value = "/api/member", produces = MediaType.APPLICATION_JSON_VALUE)
    @PreAuthorize("""
                hasPrefixAuth(@accountResource.PERMISSION_MENU_ID, @permission.READ)
                || hasPrefixAuth(@accountResource.PERMISSION_API_ID, @permission.READ)""") 
    public ResponseEntity<List<Member>> listMember(@RequestParam(value = "q", required = false) Map<String, Object> q) {
    }
}