실시간 데이터베이스 보안 규칙의 조건 사용

이 가이드에서는 핵심 Firebase 보안 규칙 언어 알아보기 가이드를 바탕으로 Firestore 실시간 데이터베이스 보안 규칙에 조건을 추가하는 방법을 보여줍니다.

실시간 데이터베이스 보안 규칙의 기본 구성 요소는 조건입니다. 조건이란 특정 작업을 허용할지 아니면 거부할지를 결정하는 부울 표현식입니다. 기본 규칙의 경우 truefalse 리터럴을 조건으로 사용하면 원활하게 작동합니다. 하지만 실시간 데이터베이스 보안 규칙 언어를 사용하면 다음과 같은 작업이 가능한 좀 더 복잡한 조건을 작성할 수 있습니다.

  • 사용자 인증 확인
  • 기존 데이터를 새로 제출한 데이터와 비교하여 평가
  • 데이터베이스의 다양한 부분에 액세스하여 비교하기
  • 수신 데이터의 유효성 검사
  • 보안 로직에 수신 쿼리 구조 사용

$ 변수를 사용하여 경로 세그먼트 캡처

$ 프리픽스로 캡처 변수를 선언하여 읽기 또는 쓰기 경로의 일부를 캡처할 수 있습니다. 이 변수는 와일드카드 역할을 하며 규칙 조건 내에서 사용할 키 값을 저장합니다.

{
  "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 }
    }
  }
}

인증

가장 일반적인 보안 규칙 패턴 중 하나는 사용자의 인증 상태에 따라 액세스를 제어하는 것입니다. 예를 들어 앱에서 로그인한 사용자만 데이터를 기록하도록 허용할 수 있습니다.

앱에서 Firebase 인증을 사용하는 경우 request.auth 변수에는 데이터를 요청하는 클라이언트의 인증 정보가 포함됩니다. request.auth에 대한 자세한 내용은 참조 문서를 확인하세요.

Firebase AuthenticationFirebase Realtime Database와 통합되어 조건을 사용하여 사용자별 데이터 액세스를 제어하는 기능을 제공합니다. 사용자가 인증되면 '실시간 데이터베이스 보안 규칙' 규칙의 auth 변수에 사용자의 정보가 채워집니다. 이 정보에는 고유 식별자(uid)와 함께 Facebook ID 또는 이메일 주소와 같은 연결된 계정 데이터와 기타 정보가 포함됩니다. 커스텀 인증 제공업체를 구현하는 경우 사용자의 인증 페이로드에 자체 필드를 추가할 수 있습니다.

이 섹션에서는 Firebase 실시간 데이터베이스 보안 규칙 언어와 사용자의 인증 정보를 결합하는 방법을 설명합니다. 이러한 두 개념을 결합하여 사용자 ID에 따라 데이터 액세스를 제어할 수 있습니다.

auth 변수

규칙에서 사전 정의된 auth 변수는 인증이 완료되기 전에는 null입니다.

Firebase 인증을 통해 사용자가 인증되면 이 변수에 다음 속성이 포함됩니다.

제공업체 사용된 인증 방법('password', 'anonymous', 'facebook", 'github', 'google', 또는 'twitter')
uid 고유 사용자 ID(제공업체에 관계없이 항상 고유함)
token Firebase 인증 ID 토큰의 콘텐츠입니다. 자세한 내용은 auth.token 참조 문서를 확인하세요.

다음은 auth 변수를 사용하여 각 사용자가 자신에게 해당하는 경로에만 쓸 수 있도록 하는 규칙의 예시입니다.

{
  "rules": {
    "users": {
      "$user_id": {
        // grants write access to the owner of this user account
        // whose uid must exactly match the key ($user_id)
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

인증 조건 지원을 위한 데이터베이스 구조화

보통은 Rules을 작성하기 쉬운 방향으로 데이터베이스를 구조화하는 것이 도움이 됩니다. Realtime Database에 사용자 데이터를 저장하는 일반적인 패턴 중 하나는 모든 사용자를 하위 항목이 모든 사용자의 uid 값인 단일 users 노드에 저장하는 것입니다. 로그인한 사용자만 자신의 데이터를 볼 수 있도록 이 데이터에 대한 액세스를 제한하려면 다음과 같은 규칙을 사용합니다.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

인증 커스텀 클레임 작업

다른 사용자에 대한 커스텀 액세스 제어가 필요한 앱의 경우, 개발자가 Firebase Authentication을 사용하여 Firebase 사용자에 대한 클레임을 설정할 수 있습니다. 이러한 클레임은 규칙의 auth.token 변수에서 액세스할 수 있습니다. 다음은 hasEmergencyTowel 커스텀 클레임을 사용하는 규칙의 예시입니다.

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

개발자는 커스텀 인증 토큰을 만들 때 원하는 경우 클레임을 토큰에 추가할 수 있습니다. 이러한 클레임은 규칙의 auth.token 변수에서 사용할 수 있습니다.

기존 데이터와 새 데이터의 비교

사전 정의된 data 변수는 쓰기 작업이 실행되기 전의 데이터를 참조하는 데 사용됩니다. 반대로 newData 변수에는 쓰기 작업이 성공할 때 존재할 새 데이터가 포함됩니다. newData는 작성되는 새 데이터와 기존 데이터가 병합된 결과를 나타냅니다.

예를 들어 아래 규칙은 새 레코드를 만들거나 기존 레코드를 삭제하도록 허용하지만 null이 아닌 기존 데이터를 변경할 수 없도록 금지합니다.

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

다른 경로의 데이터 참조

임의의 데이터를 규칙의 기준으로 사용할 수 있습니다. 사전 정의된 변수 root, data, newData를 사용하여 쓰기 이벤트 전후에 존재하는 모든 경로에 액세스할 수 있습니다.

다음 예시에서는 /allow_writes/ 노드의 값이 true이고 상위 노드에 readOnly 플래그가 설정되지 않으며 새로 기록되는 데이터에 foo라는 하위 항목이 있을 때 쓰기 작업을 허용합니다.

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

데이터 검증

데이터 구조를 적용하고 데이터의 형식과 콘텐츠를 검증하는 데는 .validate 규칙이 사용되며, 이 규칙은 .write 규칙에 따라 권한이 부여된 후에만 실행됩니다. 다음은 1900~2099년 사이의 YYYY-MM-DD 형식의 날짜만 허용하는 샘플 .validate 규칙 정의이며 정규 표현식을 사용하여 확인합니다.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

.validate 규칙은 하위로 전파되지 않는 유일한 보안 규칙 유형입니다. 하위 레코드에서 검증 규칙이 하나라도 실패하면 전체 쓰기 작업이 거부됩니다. 또한 데이터가 삭제되는 경우(기록되는 새 값이 null인 경우) 검증 정의가 무시됩니다.

이러한 점은 중요하지 않아 보일 수 있지만, 실제로는 강력한 Firebase 실시간 데이터베이스 보안 규칙을 작성하는 데 매우 중요합니다. 예를 들어 다음 규칙을 살펴보세요.

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

이 규칙을 염두에 두고 다음 쓰기 작업의 결과를 살펴보세요.

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Swift
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

이제 동일한 구조에서 .validate 대신 .write 규칙을 사용해 보겠습니다.

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

이 경우에는 다음 작업이 모두 성공합니다.

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Swift
참고: 이 Firebase 제품은 앱 클립 대상에서는 사용할 수 없습니다.
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
자바
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

이를 통해 .write 규칙과 .validate 규칙의 차이를 알 수 있습니다. 보시다시피 이 모든 규칙은 .validate를 사용하여 작성해야 하지만, 삭제가 허용되는지 여부에 따라 예외적으로 newData.hasChildren() 규칙을 사용할 수 있습니다.

쿼리 기반 규칙

규칙을 필터로 사용할 수는 없지만, 규칙에서 쿼리 매개변수를 사용하면 데이터 중 일부만 액세스 가능하도록 제한할 수 있습니다. 규칙에서 query. 표현식을 사용하여 쿼리 매개변수를 기준으로 읽기 또는 쓰기 액세스 권한을 부여합니다.

예를 들어 다음 쿼리 기반 규칙은 사용자 기반 보안 규칙과 쿼리 기반 규칙을 사용하여 baskets 컬렉션의 데이터 중 활성 사용자가 소유한 장바구니에만 액세스할 수 있도록 제한합니다.

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

규칙에 쿼리 매개변수가 포함된 다음 규칙은 성공합니다.

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

그러나 규칙에 매개변수가 포함되지 않은 다음 쿼리는 PermissionDenied 오류와 함께 실패합니다.

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

쿼리 기반 규칙을 사용하여 클라이언트에서 읽기 작업을 통해 다운로드할 수 있는 데이터의 양을 제한할 수도 있습니다.

예를 들어 다음 규칙은 읽기 액세스를 쿼리 결과 중 우선순위에 따라 정렬한 처음 1,000개로 제한합니다.

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

실시간 데이터베이스 보안 규칙에 다음과 같은 query. 표현식을 사용할 수 있습니다.

쿼리 기반 규칙 표현식
표현식 유형 설명
query.orderByKey
query.orderByPriority
query.orderByValue
부울 쿼리가 키, 우선순위 또는 값으로 정렬되면 true입니다. 그렇지 않으면 false입니다.
query.orderByChild 문자열
null
하위 노드를 가리키는 상대 경로를 문자열로 표현합니다. 예: query.orderByChild === "address/zip". 쿼리가 하위 노드에 따라 정렬되지 않은 경우 이 값은 null입니다.
query.startAt
query.endAt
query.equalTo
문자열
숫자
부울
null
실행 중인 쿼리의 경계를 검색합니다. 경계가 설정되지 않은 경우 null을 반환합니다.
query.limitToFirst
query.limitToLast
숫자
null
실행 중인 쿼리의 한도를 검색합니다. 한도가 설정되지 않은 경우 null을 반환합니다.

다음 단계

지금까지 조건에 대해 학습하며 Rules에 대해 자세히 알아봤습니다. 이제 다음과 같은 단계를 진행할 수 있습니다.

핵심 사용 사례를 처리하는 방법을 알아보고 Rules 개발, 테스트, 배포를 위한 워크플로를 살펴봅니다.

Realtime Database와 관련된 Rules 기능을 알아봅니다.