실시간 데이터베이스 보안 규칙 언어의 핵심 구문 알아보기

Firebase 실시간 데이터베이스 보안 규칙을 사용하면 데이터베이스에 저장된 데이터에 대한 액세스를 제어할 수 있습니다. 유연한 규칙 구문을 사용하면 데이터베이스에 대한 모든 쓰기 작업부터 개별 노드에 대한 작업까지 어떠한 상황에 맞는 규칙이라도 작성할 수 있습니다.

실시간 데이터베이스 보안 규칙은 데이터베이스에 대한 선언적 구성입니다. 즉, 제품 로직과 별도로 규칙이 정의됩니다. 이 방식은 여러 가지 장점을 갖습니다. 클라이언트가 보안 적용을 담당하지 않고, 구현 상의 버그로 인해 데이터의 보안이 침해되지 않으며, 가장 중요한 장점으로 데이터 보호를 위한 서버 등의 중간 관리 체계가 필요하지 않습니다.

이 주제에서는 전체 규칙 세트를 만드는 데 사용되는 기본 구문과 실시간 데이터베이스 보안 규칙에 대해 설명합니다.

보안 규칙 구조화

실시간 데이터베이스 보안 규칙은 JSON 문서에 포함된 자바스크립트와 유사한 표현식으로 작성됩니다. 규칙 구조는 데이터베이스에 저장한 데이터 구조를 따라야 합니다.

기본 규칙은 보호할 노드 집합, 관련된 액세스 방법(예: 읽기, 쓰기), 액세스가 허용되거나 거부되는 조건을 식별합니다. 다음 예에 나온 조건은 간단한 truefalse 문이지만 다음 번 주제에서는 조건을 표현한 동적 방식을 살펴볼 예정입니다.

예를 들어 parent_node에서 child_node를 보호할 때의 일반적인 구문은 다음과 같습니다.

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

이 패턴을 적용해 보겠습니다. 예를 들어 메시지 목록을 다음과 같은 형태의 데이터로 유지한다고 가정해 보겠습니다.

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

규칙도 비슷한 구조를 가져야 합니다. 다음은 이 데이터 구조에 사용할 수 있는 읽기 전용 보안 규칙 세트입니다. 이 예에서는 규칙을 적용할 데이터베이스 노드와 해당 노드에서 규칙을 평가하는 조건을 지정하는 방법을 보여줍니다.

{
  "rules": {
    // For requests to access the 'messages' node...
    "messages": {
      // ...and the individual wildcarded 'message' nodes beneath
      // (we'll cover wildcarding variables more a bit later)....
      "$message": {

        // For each message, allow a read operation if <condition>. In this
        // case, we specify our condition as "true", so read access is always granted.
        ".read": "true",

        // For read-only behavior, we specify that for write operations, our
        // condition is false.
        ".write": "false"
      }
    }
  }
}

기본 규칙 작업

데이터에 수행되는 작업 유형에 따라 보안을 적용하는 3가지 규칙 유형(.write, .read, .validate)이 있습니다. 각 유형의 간략한 용도는 다음과 같습니다.

규칙 유형
.read 사용자가 데이터를 읽을 수 있는 조건을 기술합니다.
.write 사용자가 데이터를 쓸 수 있는 조건을 기술합니다.
.validate 값의 올바른 형식, 하위 속성을 갖는지 여부, 데이터 유형을 정의합니다.

와일드 카드 캡처 변수

모든 규칙 문은 노드를 가리킵니다. 문은 특정 노드를 가리키거나 $ 와일드 카드 캡처 변수를 사용해 계층 수준의 노드 세트를 가리킬 수도 있습니다. 이러한 캡처 변수를 사용하여 후속 규칙 문 내에 사용할 노드 키의 값을 저장합니다. 이 기법을 사용하면 보다 복잡한 규칙 조건을 작성할 수 있습니다. 이에 대해서는 다음 주제에서 자세히 설명합니다.

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

동적 $ 변수를 상수 경로 이름과 함께 사용할 수도 있습니다. 이 예시에서는 $other 변수를 사용하여 widgettitlecolor 이외의 하위 항목이 없도록 하는 .validate 규칙을 선언합니다. 하위 항목을 추가로 생성하는 쓰기 작업은 실패합니다.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

읽기 및 쓰기 규칙의 하위 전파

.read.write 규칙은 하향식으로 작동하며 상위 규칙이 하위 규칙을 재정의합니다. 규칙이 특정 경로에서 읽기 또는 쓰기 권한을 부여하는 경우 그 아래의 모든 하위 노드에 대한 액세스 권한도 부여됩니다. 예를 들어 아래 구조를 살펴보세요.

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

이 보안 구조는 /foo/true 값이 있는 하위 baz가 포함되어 있으면 /bar/를 읽을 수 있도록 허용합니다. 하위 경로에서 액세스 권한을 취소할 수 없으므로 /foo/bar/ 아래의 ".read": false 규칙은 여기에 적용되지 않습니다.

이 방식은 다소 직관적이지 않을 수 있지만 규칙 언어의 강력한 기능이며 매우 복잡한 액세스 권한을 최소한의 노력으로 구현할 수 있게 해 줍니다. 이 내용은 이 가이드의 뒷부분에서 사용자 기반 보안을 살펴볼 때 설명합니다.

.validate 규칙은 하위로 전파되지 않습니다. 쓰기가 허용되려면 계층 구조의 모든 수준에서 모든 검증 규칙이 충족되어야 합니다.

규칙과 필터의 차이

규칙은 유기적으로 적용됩니다. 즉, 해당 위치나 상위 위치에 권한을 부여하는 규칙이 없으면 읽기 또는 쓰기 작업이 즉시 실패합니다. 영향을 받는 하위 경로에 모두 액세스할 수 있다고 해도 상위 위치의 읽기 작업은 모두 실패합니다. 예를 들어 아래 구조를 살펴보세요.

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

규칙이 원자적으로 평가된다는 점을 이해하지 못하면 /records/ 경로를 가져올 때 rec1은 반환되고 rec2는 반환되지 않는다고 생각할 수 있습니다. 그러나 실제로는 오류가 발생합니다.

자바스크립트
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

/records/의 읽기 작업은 원자적이며 /records/ 아래의 모든 데이터에 대한 액세스 권한을 부여하는 읽기 규칙이 없으므로 PERMISSION_DENIED 오류가 발생합니다. Firebase Console의 보안 시뮬레이터에서 이 규칙을 평가하면 /records/ 경로에 대한 액세스를 허용하는 읽기 규칙이 없어 읽기 작업이 거부됨을 알 수 있습니다. rec1에 대한 규칙은 요청된 경로에 없었으므로 평가되지 않았습니다. rec1을 가져오려면 다음과 같이 직접 액세스해야 합니다.

자바스크립트
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

중첩 문

노드 하나에 2개 이상의 규칙을 적용할 수 있습니다. 여러 규칙 표현식이 한 노드를 식별하는 경우 조건 중 하나라도 false이면 액세스 방법이 거부됩니다.

{
  "rules": {
    "messages": {
      // A rule expression that applies to all nodes in the 'messages' node
      "$message": {
        ".read": "true",
        ".write": "true"
      },
      // A second rule expression applying specifically to the 'message1` node
      "message1": {
        ".read": "false",
        ".write": "false"
      }
    }
  }
}

위의 예시에서는 첫 번째 규칙은 항상 true이지만 두 번째 규칙이 항상 false이므로 message1 노드에 대한 읽기가 거부됩니다.

다음 단계

Firebase 실시간 데이터베이스 보안 규칙에 대한 이해도를 높일 수 있습니다.

  • 규칙 언어의 주요 개념인 동적 조건에 대해 알아보세요. 동적 조건을 사용하면 규칙을 통해 사용자 승인을 확인하고, 기존 데이터와 수신 데이터를 비교하고, 클라이언트에서 들어온 쿼리의 구조를 검증할 수 있습니다.

  • 일반적인 보안 사용 사례와 이를 해결하는 Firebase 보안 규칙 정의를 알아보세요.